Git_Day.3

조현기·3일 전

Git_Day.3

merge와 rebase를 이력 그림으로 비교하고, 언제 무엇을 쓸지 정리한 날. 그리고 main에서 실수로 작업했을 때 수습하는 순서.

Day.2 마지막에 merge와 rebase의 차이를 간단히 봤다. 오늘은 커밋 이력이 어떻게 달라지는지를 그림으로 이해하고, 실무에서 어느 쪽을 쓰는지까지 정리했다.


1. 상황 설정

두 브랜치가 첫 커밋 A에서 갈라져서 각자 커밋을 쌓았다고 하자.

A - B - C   : 나 (me)
A - D       : main (친구가 추가한 커밋)
$ git log --oneline --graph --all
* 3c37e34 D
| * 608de9a C
| * b086d41 B
|/
* b67a30e A

이 두 갈래를 하나로 합치는 방법이 merge와 rebase다. (해시값은 실습할 때마다 다르다)


2. Merge: 합치기

분리되어 있던 브랜치가 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에서 실행하는 일이 많은 것이다.


3. Rebase: 베이스를 다시 잡기

베이스를 다시 잡는다고 이해하면 좋다.

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 후
Bb086d41a5ab0df
C608de9a5d49e3f
D3c37e343c37e34 (그대로)

내 커밋(B, C)의 해시코드가 새 값으로 바뀐다. 커밋은 "부모가 누구냐"까지 포함해서 해시가 정해지는데, rebase로 부모가 A에서 D로 바뀌었기 때문에 내용은 같아도 새 커밋으로 다시 만들어지는 것이다.

이후 main에 합치면 이렇게 된다.

$ git switch main
$ git merge me
Updating 3c37e34..5d49e3f
Fast-forward                               ← 머지 커밋 없이 main 이 앞으로 이동만 한다

rebase를 먼저 해 두면 main은 이미 내 브랜치의 조상이라서 새 커밋 없이 포인터만 앞으로 이동(fast-forward)한다. 최종 이력이 일직선으로 깔끔하게 남는다.


4. Merge와 Rebase 비교

구분mergerebase
새 커밋머지 커밋이 생긴다머지 커밋이 생기지 않는다
기존 커밋 해시그대로새 값으로 바뀐다
이력 모양갈라지고 합쳐지는 흔적이 남는다일직선
이력의 의미실제로 일어난 일 그대로 기록깔끔하게 다시 쓴 이력

필기에는 "새 커밋이 생기느냐, 아니냐의 차이"라고 정리돼 있다. 보충하면 rebase도 내 커밋을 복제해서 새로 만든다는 점에서 커밋이 새로 생기긴 한다. 다만 merge처럼 두 갈래를 잇는 합치기용 커밋이 따로 생기지 않는다는 뜻으로 이해하면 맞다.


5. 실무에서는 무엇을 쓰나

기준은 "그 브랜치를 다른 사람이 가져갔는가"다.

상황선택이유
GitHub에 push해서 다른 사람이 pull 받은 브랜치mergerebase는 해시를 바꾸기 때문에 남들의 이력과 꼬일 수 있다
혼자 가지고 있는 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 중 충돌이 나면

같은 파일의 같은 부분을 두 브랜치가 다르게 고쳤으면 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를 기억해 두면 마음이 편하다.

6. 실수 수습: main에서 작업해 버렸을 때

main에서 작업하면 안 되는데 커밋하기 전에 깨달았다면 이 순서로 한다.

  1. main에서 실수로 작업했다. (커밋하기 전)
  2. 새 브랜치로 이동해서 git add, git commit 한다.
  3. GitHub에 올린다.
$ 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의 커밋을 되돌리는 작업이 같이 필요하다. 이 날 노트는 커밋 전 상황까지만 다뤘다.


7. 파일 만들기 명령어

실습할 때 쓰는 echo 리다이렉트도 한 번 더 정리했다. (Day.2의 echo 참고)

명령어의미
echo 텍스트 > 파일명파일을 만들고 내용을 쓴다 (이미 있으면 덮어씀)
echo 텍스트 >> 파일명파일 끝에 내용을 추가

마무리

  • merge는 두 브랜치를 잇는 머지 커밋을 만들고, 기존 커밋은 그대로 보존된다. 이력에 갈라진 흔적이 남는다.
  • rebase는 베이스를 최신 커밋 위로 옮겨서 내 커밋을 다시 이어 붙인다. 이력이 일직선이 되지만 내 커밋의 해시가 바뀐다.
  • 이미 push해서 남들이 받아 간 브랜치는 merge, 혼자 쓰는 push 전 로컬 브랜치는 rebase를 쓴다.
  • merge는 결과를 받을 브랜치로 이동해서 실행한다. rebase 중에 충돌이 나면 해결 후 --continue, 포기할 때는 --abort다.
  • main에서 커밋 전에 실수로 작업했다면 git switch -c 새브랜치로 옮겨서 커밋하고 push한다.

Tags: Git merge rebase 브랜치 GitHub 커밋이력 협업 개발자 학습기록

profile
난 뭘까

0개의 댓글