Git의 기본 흐름과 병합 이해
Git의 3가지 공간
Git을 제대로 이해하려면, 명령어보다 먼저 코드가 이동하는 3가지 공간을 알아야 한다.
| 공간 | 역할 | 관련 명령어 |
|---|---|---|
| Working Directory | 현재 코드를 수정하는 작업 공간 | 파일 편집 |
| Staging Area | 커밋할 변경사항을 골라 담는 공간 | git add |
| Repository | 커밋이 저장되는 로컬 저장소 | git commit |
여기에 하나를 더 붙이면 git push 로 Remote Repository 에 반영하는 흐름이 된다.
# 파일 수정 (Working Directory)
vim app.js
# 장바구니에 담기 (Staging Area)
git add app.js
# 로컬 저장소에 커밋 생성 (Repository)
git commit -m "feat: add login feature"
# 원격 저장소에 전송
git push origin main
그렇다면 왜 Staging Area 같은 중간 단계가 있는 것일까 하는 의문이 들 수 있다.
수정한 파일이 10개라고 해서 항상 10개를 한 번에 커밋해야 하는 것은 아니다.
그 중 2개만 먼저 커밋하고 싶을 때, Staging Area가 커밋 단위를 직접 제어할 수 있게 해준다.
그리고 작업하던 변경을 잠깐 치워두고 싶을 때는 git stash 도 자주 사용한다.
예를 들어 merge 나 rebase 전에 Working Directory 를 잠시 비워야 할 때 유용하다.
브랜치를 합치는 방식 - Fast-Forward 와 3-Way Merge
브랜치를 merge 할 때 Git은 항상 같은 방식으로 합치지 않는다.
상황에 따라서 Fast-Forward 로 끝날 수도 있고, 새로운 병합 커밋(Merge Commit) 이 생길 수도 있다.
Fast-Forward
main 에서 feature 브랜치를 딴 뒤, main 에 새로운 커밋이 하나도 없다면 Fast-Forward 가 가능하다.
이 경우 Git은 별도의 병합 커밋을 만들지 않는다.
단순히 main 브랜치 포인터를 feature 의 최신 커밋으로 앞으로 이동시키면 되기 때문이다.
git switch main
git merge feature
결과적으로 히스토리는 일직선처럼 남는다.
3-Way Merge
보통은 이 경우를 더 자주 보게 된다.
내가 feature 브랜치에서 작업하는 동안 다른 사람이 main 에 커밋을 추가했다면, 이제 두 브랜치는 서로 다른 방향으로 진화한 상태가 된다.
이때는 Fast-Forward 가 불가능해서 Git이 새로운 병합 커밋을 생성한다.
그래서 3-Way Merge 라고 부르는데, 비교 기준이 아래 3개이기 때문이다.
- 두 브랜치가 갈라지기 전 마지막 공통 조상 커밋
main의 최신 커밋feature의 최신 커밋
이 세 지점을 비교해서 자동으로 합칠 수 있으면 merge 되고, 자동으로 합칠 수 없는 부분이 바로 충돌(conflict) 이 된다.
히스토리 조작과 충돌 해결
Cherry-pick - 원하는 커밋만 가져오기
다른 브랜치의 모든 변경이 아니라, 특정 커밋 하나만 내 브랜치로 가져오고 싶을 때 cherry-pick 을 사용한다.
대표적인 예시는 이런 경우다.
develop에서 만든 핫픽스 커밋 하나만release에 반영해야 할 때- 여러 기능 커밋 중 일부 수정만 운영 브랜치에 가져와야 할 때
# 특정 커밋 하나 가져오기
git cherry-pick <커밋해시>
# 여러 개 가져오기
git cherry-pick <해시1> <해시2>
# 범위로 가져오기
# 시작해시는 끝해시보다 더 과거의 커밋이어야 한다.
git cherry-pick <시작해시>^..<끝해시>
Q. Cherry-pick 중 충돌이 났다면?
A. 작업은 일시 정지되고, 충돌 파일을 수정한 뒤 이어서 진행하면 된다.
git cherry-pick <커밋해시>
# 충돌 발생 시 상태 확인
git status
# 파일 수정 후 스테이징
git add <충돌해결파일>
# 계속 진행
git cherry-pick --continue
어떻게 해도 정리가 안 되면 아래 명령으로 시도 전 상태로 돌아갈 수 있다.
git cherry-pick --abort
Rebase 는 왜 하는 걸까?
merge 가 두 갈래의 브랜치를 하나로 합치는 작업이라면, rebase 는 내 브랜치의 시작점(base)을 다른 커밋 위로 옮긴 뒤 내 커밋들을 다시 얹는 작업이다.
보통 이런 상황에서 많이 사용한다.
feature브랜치에서 며칠째 작업 중이다.- 그 사이
main에 다른 사람들의 커밋이 여러 개 추가되었다. - 내 작업을 최신
main위 기준으로 다시 정리하고 싶다.
git switch feature
git rebase main
이 명령은 "내 브랜치 커밋들을 최신 main 뒤로 다시 배치해라"라는 의미다.
그래서 히스토리가 더 일직선처럼 보이게 된다.
| 항목 | Merge | Rebase |
|---|---|---|
| 히스토리 | 분기 흔적이 남음 | 일직선처럼 정리됨 |
| 병합 커밋 | 생길 수 있음 | 보통 생기지 않음 |
| 커밋 해시 | 유지 | 변경됨 |
| 이미 공유한 브랜치 | 비교적 안전 | 주의 필요 |
충돌 해결 - Merge 와 Rebase 의 차이
충돌이 나면 파일 안에 보통 아래와 같은 표시가 생긴다.
<<<<<<< HEAD
현재 기준 코드
=======
들어오는 코드
>>>>>>>
여기서 가장 많이 헷갈리는 부분이 있는데, 바로 Merge 와 Rebase 는 충돌을 바라보는 기준이 다르다 는 점이다.
Merge 충돌일 때
예를 들어 현재 feature 브랜치에 있는 상태에서 git merge main 을 했다면,
보통 아래처럼 이해하면 된다.
| 구분 | 가리키는 코드 |
|---|---|
| HEAD / Current Change | 현재 체크아웃한 feature 브랜치의 코드 |
| Incoming Change | 가져오려는 main 브랜치의 코드 |
즉, merge 에서는 보통 HEAD 를 "내 현재 브랜치 코드"로 이해해도 된다.
하지만 이 값이 항상 고정되는 것은 아니다.
예를 들어 main 브랜치에서 git merge feature 를 했다면, 그때는 HEAD 가 main 쪽 코드가 된다.
즉, 어느 브랜치에서 merge 를 실행했느냐에 따라 HEAD 와 Incoming 의 방향이 달라진다.
Rebase 충돌일 때
반면 feature 브랜치에서 git rebase main 을 하고 충돌이 난다면 이야기가 달라진다.
Rebase 는 먼저 기준점을 main 으로 옮긴 뒤, 내 커밋을 하나씩 다시 적용하는 과정이기 때문이다.
| 구분 | 가리키는 코드 |
|---|---|
| 현재 기준 코드 | 최신 main 브랜치 쪽 코드 |
| 다시 적용 중인 코드 | 내 브랜치의 개별 커밋 |
즉, Rebase 충돌에서는 "현재 기준은 main 이고, 내 커밋을 그 위에 다시 얹는 중"이라고 이해하는 편이 안전하다.
Rebase 중 충돌 해결 흐름
Rebase 중 conflict 가 났을 때의 흐름을 명령어 기준으로 정리하면 아래와 같다.
# 1. feature 브랜치에서 최신 main 위로 재배치
git rebase main
# 2. 충돌 발생
git status
# 3. 충돌 파일 수정
# <<<<<<< HEAD
# main 쪽 코드
# =======
# 내 커밋 코드
# >>>>>>> commit-hash
# 4. 수정 후 스테이징
git add <충돌해결파일>
# 5. rebase 계속 진행
git rebase --continue
내 커밋이 여러 개라면 이 과정이 여러 번 반복될 수 있다.
여기서 중요한 점은, 충돌을 해결했다고 해서 git commit 을 직접 하는 것이 아니라는 점이다.
Rebase 는 Git이 멈춰둔 작업을 다시 이어가는 흐름이므로 git rebase --continue 를 사용해야 한다.
중간에 포기하고 싶다면 언제든 아래 명령으로 돌아갈 수 있다.
git rebase --abort