yg log --graph --oneline --all

Home Stack Notes AboutRSS

3. 같은 변경도 커밋 기록은 달라진다

작성 · 3분 읽기 · 조회수 —

Git 학습 기록 · 3 / 6

코드가 합쳐졌다는 말의 여러 모양

Advanced-Branch 노트에는 merge를 네 갈래로 나눠 적어두었다. 그중 fast-forward 설명에는 이런 비유가 있다.

그냥 분기한 브랜치를 main브랜치로 바꾸는 거랑 똑같다고 생각하면 됨

main이 멈춰 있는 동안 feature에만 commit이 추가됐다면 main은 feature의 끝까지 포인터를 앞으로 옮길 수 있다. 새 merge commit 없이 branch 위치만 전진하므로 결과 그래프가 직선이다. 정확한 표기는 노트의 fast-foward가 아니라 fast-forward다.

하지만 main과 feature가 모두 움직인 뒤 같은 변경을 통합하면 선택지가 생긴다. 이번에는 base에서 main commit 하나, feature commit 두 개를 만든 같은 저장소를 세 벌 복제했다. 하나는 merge commit, 하나는 rebase 후 fast-forward, 하나는 squash를 적용했다. 파일 내용이 같은지만 보면 차이가 사라지므로 git log --graph --decorate --oneline --all을 기준으로 읽었다.

갈라졌다는 사실을 남기는 merge

일반 merge 모형에서는 --no-ff로 merge commit을 명시적으로 만들었다.

*   b35153f (HEAD -> main) merge feature
|\
| * f443747 (feature) feature two
| * ae57193 feature one
* | f0a10c6 main work
|/
* 3451f32 base

노트에는 양쪽에 새 commit이 있으면 “두 브랜치의 코드를 합쳐서 새로운 commit을 자동으로 생성”한다고 적혀 있다. 이 예에서는 맞지만 merge 전체의 규칙으로 넓히면 부족하다. fast-forward가 가능하면 기본 merge는 새 commit 없이 포인터만 옮길 수 있고, --ff-only는 그것이 불가능할 때 아예 중단한다. --no-ff는 가능한 경우에도 merge commit을 남긴다. (git-merge)

merge commit에는 부모가 둘이다. 그래서 feature의 두 commit과 main에서 갈라졌다가 다시 만난 지점이 그대로 보인다. “어떤 작업 묶음이 어느 시점에 main으로 들어왔는가”가 그래프에 남는 모양이다.

commit을 새 base 위에 다시 놓는 rebase

노트의 rebase 설명은 “분기된 브랜치의 시작점을 다른 commit으로 옮겨주는 행위”라고 되어 있다. 실제 그래프에서는 feature의 두 commit이 main work 뒤에 다시 만들어졌다.

* 9e2a0ed (HEAD -> main, feature) feature two
* ea79ac7 feature one
* f0a10c6 main work
* 3451f32 base

원래 feature commit hash는 ae57193, f443747이었는데 rebase 뒤에는 ea79ac7, 9e2a0ed가 됐다. 변경을 새 base 위에 차례로 재적용하면서 부모가 달라졌고 commit 객체도 새로 생겼기 때문이다. 그 뒤 main에서 feature를 merge하면 fast-forward가 가능해져 직선 history가 된다. (git-rebase)

노트에는 “충돌이 많이 발생할 수 있는데 해결하기 매우 귀찮음”이라는 판단도 남아 있다. 충돌 횟수는 변경이 겹치는 방식에 따라 다르다. 다만 merge 한 번과 달리 rebase는 commit을 하나씩 재적용하므로, 같은 부분의 충돌을 여러 commit 단계에서 풀 수도 있다. 이미 다른 사람이 기반으로 삼은 공개 branch를 rebase하면 commit ID가 바뀐 history를 다시 맞춰야 한다는 영향도 있다.

변경 묶음 하나만 남기는 squash

노트는 squash를 “다른 브랜치의 커밋 내역을 하나로 합쳐서 merge”한다고 적었다. 이번 실행에서 git merge --squash feature 직후에는 다음 상태만 생겼다.

Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
A  feature.txt

여기서 중요한 빈칸은 commit이 아직 없다는 점이다. merge --squash는 feature의 최종 변경을 working tree와 index에 준비하지만 HEAD를 옮기거나 merge commit을 만들지 않는다. 별도의 git commit을 실행한 뒤 그래프는 이렇게 됐다.

* b6d5145 (HEAD -> main) squash feature
* f0a10c6 main work
* 3451f32 base

feature의 두 commit은 main history에 부모로 연결되지 않고, 효과를 모은 새 commit 하나만 올라갔다. 공식 문서도 squash가 실제 merge commit을 만들지 않는다고 구분한다. (git-merge의 squash 옵션)

같은 tree와 다른 history

세 저장소에서 마지막 main^{tree}를 확인하자 모두 같은 값이었다.

212191943dd5005973b3e93062d01bdae9e80015

최종 파일 snapshot은 같았지만 그곳까지 온 경로는 달랐다. merge는 갈라지고 합쳐진 관계를 보존했고, rebase는 feature의 개별 commit을 새 base 위에 직선으로 놓았으며, squash는 feature 변경 전체를 commit 하나로 줄였다.

노트에서 시작한 “코드를 합친다”는 표현만으로는 이 차이를 볼 수 없다. 무엇을 고를지는 코드가 같아지는가보다 어떤 history를 남겨야 하는가에 달려 있다. 작업 단위의 경계를 보존할지, 개별 commit을 직선으로 유지할지, 최종 변경 하나만 남길지를 먼저 정해야 한다.

세 방식을 같은 시작점에서 비교하는 전체 과정은 03-history.sh에 있다. 스크립트는 서로 다른 임시 clone에서 실행되므로 실제 저장소 history를 바꾸지 않는다. commit SHA는 실행 시각에 따라 달라질 수 있으므로 숫자 자체보다 부모 관계와 세 main tree가 같은지를 비교하면 된다.