merge와rebase를 이력 그림으로 비교하고, 언제 무엇을 쓸지 정리한 날. 그리고main에서 실수로 작업했을 때 수습하는 순서.
Day.2 마지막에 merge와 rebase의 차이를 간단히 봤다. 오늘은 커밋 이력이 어떻게 달라지는지를 그림으로 이해하고, 실무에서 어느 쪽을 쓰는지까지 정리했다.
두 브랜치가 첫 커밋 A에서 갈라져서 각자 커밋을 쌓았다고 하자.
A - B - C : 나 (me)
A - D : main (친구가 추가한 커밋)
$ git log --oneline --graph --all
* 3c37e34 D
| * 608de9a C
| * b086d41 B
|/
* b67a30e A
이 두 갈래를 하나로 합치는 방법이 merge와 rebase다. (해시값은 실습할 때마다 다르다)
분리되어 있던 브랜치가 main에 통합되는 느낌.
merge는 두 브랜치의 끝을 하나로 합치는 새 커밋(머지 커밋)을 만든다.
$ git switch main # 합쳐질 곳(받는 쪽)으로 이동
$ git merge me -m "Merge me into main"
$ git log --oneline --graph
* 5b8cb26 Merge me into main ← 두 갈래를 잇는 새 커밋
|\
| * 608de9a C
| * b086d41 B
* | 3c37e34 D
|/
* b67a30e A
B, C, D는 그대로 보존되고 해시도 바뀌지 않는다.필기에는 "merge는 보통 main에서 실행한다. 합쳐질 브랜치에서 실행해야 하기 때문"이라고 적었다. 정확히 말하면 merge는 "지금 있는 브랜치"에 "지정한 브랜치"를 가져와 합친다. 그래서 결과를 받을 쪽 브랜치로 먼저 이동한 뒤 실행한다.
$ git switch main
$ git merge me # main 에 me 를 합친다 → main 이 바뀐다
반대로 me에서 git merge main을 하면 me에 main을 합치는 것이라서 me 브랜치에 머지 커밋이 생기고 main은 그대로다.
$ git switch me
$ git merge main -m "Merge main into me"
* 3c3a7be Merge main into me ← me 브랜치에 생긴다
|\
| * 3c37e34 D (main 은 D 에 그대로 머물러 있다)
* | 608de9a C
* | b086d41 B
|/
* b67a30e A
어느 쪽이 맞는 것은 아니고 "어디에 합칠 것인가"에 따라 정한다. 기능 브랜치를 main에 합치는 경우가 많아서 main에서 실행하는 일이 많은 것이다.
베이스를 다시 잡는다고 이해하면 좋다.
rebase는 내 브랜치가 갈라져 나온 지점(베이스)을 최신 커밋 위로 옮기는 작업이다. 내 커밋들을 최신 커밋 뒤에 다시 이어 붙인다.
rebase 전 rebase 후 (me 에서 git rebase main)
A - B - C : 나 A - D - B' - C' : 나
A - D : main A - D : main
나에서 git rebase main을 하면 A(기존 베이스) → D(새로 잡힌 베이스) → B → C 순서로 이어진다.
$ git switch me
$ git rebase main
Successfully rebased and updated refs/heads/me.
$ git log --oneline --graph
* 5d49e3f C
* a5ab0df B
* 3c37e34 D
* b67a30e A
이력이 일직선이 되었다. 그리고 해시를 비교해 보면 이렇다.
| 커밋 | rebase 전 | rebase 후 |
|---|---|---|
B | b086d41 | a5ab0df |
C | 608de9a | 5d49e3f |
D | 3c37e34 | 3c37e34 (그대로) |
내 커밋(B, C)의 해시코드가 새 값으로 바뀐다. 커밋은 "부모가 누구냐"까지 포함해서 해시가 정해지는데, rebase로 부모가 A에서 D로 바뀌었기 때문에 내용은 같아도 새 커밋으로 다시 만들어지는 것이다.
이후 main에 합치면 이렇게 된다.
$ git switch main
$ git merge me
Updating 3c37e34..5d49e3f
Fast-forward ← 머지 커밋 없이 main 이 앞으로 이동만 한다
rebase를 먼저 해 두면 main은 이미 내 브랜치의 조상이라서 새 커밋 없이 포인터만 앞으로 이동(fast-forward)한다. 최종 이력이 일직선으로 깔끔하게 남는다.
| 구분 | merge | rebase |
|---|---|---|
| 새 커밋 | 머지 커밋이 생긴다 | 머지 커밋이 생기지 않는다 |
| 기존 커밋 해시 | 그대로 | 새 값으로 바뀐다 |
| 이력 모양 | 갈라지고 합쳐지는 흔적이 남는다 | 일직선 |
| 이력의 의미 | 실제로 일어난 일 그대로 기록 | 깔끔하게 다시 쓴 이력 |
필기에는 "새 커밋이 생기느냐, 아니냐의 차이"라고 정리돼 있다. 보충하면 rebase도 내 커밋을 복제해서 새로 만든다는 점에서 커밋이 새로 생기긴 한다. 다만 merge처럼 두 갈래를 잇는 합치기용 커밋이 따로 생기지 않는다는 뜻으로 이해하면 맞다.
기준은 "그 브랜치를 다른 사람이 가져갔는가"다.
| 상황 | 선택 | 이유 |
|---|---|---|
| GitHub에 push해서 다른 사람이 pull 받은 브랜치 | merge | rebase는 해시를 바꾸기 때문에 남들의 이력과 꼬일 수 있다 |
| 혼자 가지고 있는 push 안 한 로컬 브랜치 | rebase | 이력이 깔끔하게 남는다 |
이미 push한 브랜치를 rebase하면 어떻게 되는지 확인했다. 원격에 있는 feat 브랜치를 main 위로 rebase하고 push해 보았다.
$ git rebase main # 해시가 바뀐다
$ git push
! [rejected] feat -> feat (non-fast-forward)
error: failed to push some refs to '../origin.git'
hint: Updates were rejected because the tip of your current branch is behind ...
거부된다. 원격에는 옛 해시(608de9a)의 커밋이 있고 로컬에는 새 해시(5d49e3f)의 커밋이 있어서 서로 이어지지 않기 때문이다. 억지로 올리려면 강제 푸시가 필요한데, 그러면 같은 브랜치를 받아 간 다른 사람의 이력이 어긋난다.
$ git push --force-with-lease # 강제 푸시 (원격이 내가 아는 상태일 때만 덮어씀)
+ 608de9a...5d49e3f feat -> feat (forced update)
Day.2의 push -f가 위험하다고 한 이유가 바로 이것이다. --force-with-lease는 -f보다 안전한 강제 푸시 옵션으로, 내가 마지막으로 본 이후 원격에 다른 사람이 새로 올린 커밋이 있으면 거부한다. 그래도 혼자 쓰는 브랜치에서만 쓰는 것이 좋다.
같은 파일의 같은 부분을 두 브랜치가 다르게 고쳤으면 rebase 도중에 멈춘다.
$ git rebase main
CONFLICT (content): Merge conflict in f.txt
error: could not apply 20b6447... me edit
hint: Resolve all conflicts manually, mark them as resolved with ...
git add 파일, git rebase --continue로 이어 간다.git rebase --abort로 rebase를 시작하기 전 상태로 되돌린다. 시작 전으로 안전하게 돌아오니 처음에는 --abort를 기억해 두면 마음이 편하다.main에서 작업하면 안 되는데 커밋하기 전에 깨달았다면 이 순서로 한다.
main에서 실수로 작업했다. (커밋하기 전)git add, git commit 한다.$ git status -s # main 에서 작업 중인 상태
M a.txt
?? feature.txt
$ git branch --show-current
main
$ git switch -c feature/login # 새 브랜치 생성 + 전환
Switched to a new branch 'feature/login'
$ git status -s # 작업 내용이 그대로 따라왔다
M a.txt
?? feature.txt
$ git add .
$ git commit -m "feature: login"
$ git push -u origin feature/login # 새 브랜치를 GitHub에 올린다
핵심은 git switch -c가 아직 커밋하지 않은 변경 사항을 그대로 가지고 새 브랜치로 넘어간다는 점이다. 커밋 전이라서 아직 main에는 아무 기록도 남지 않았고, 새 브랜치에서 커밋하면 이 작업은 새 브랜치에만 기록된다. 확인해 보면 main으로 돌아갔을 때 git status가 깨끗하고 feature.txt도 보이지 않는다.
이미
main에 커밋까지 해 버린 경우는 이 방법만으로는 부족하다. 그때는 Day.1의reset으로main의 커밋을 되돌리는 작업이 같이 필요하다. 이 날 노트는 커밋 전 상황까지만 다뤘다.
실습할 때 쓰는 echo 리다이렉트도 한 번 더 정리했다. (Day.2의 echo 참고)
| 명령어 | 의미 |
|---|---|
echo 텍스트 > 파일명 | 파일을 만들고 내용을 쓴다 (이미 있으면 덮어씀) |
echo 텍스트 >> 파일명 | 파일 끝에 내용을 추가 |
--continue, 포기할 때는 --abort다.main에서 커밋 전에 실수로 작업했다면 git switch -c 새브랜치로 옮겨서 커밋하고 push한다.Tags: Git merge rebase 브랜치 GitHub 커밋이력 협업 개발자 학습기록