fatal: Need to specify how to reconcile divergent branches. 원인과 해결 / Fast-Forward, rebase, merge 이해하기
git pull을 어떻게 할지 전략이 세가지가 있으니 선택해서 명시해달라는 내용이다.
타 블로그 왈 git 2.27부터 추가된 기능이라고 한다.
기존 git pull의 문제점 : 세 가지 전략으로 나뉘게 된 배경
git pull --help에서는 아래처럼 설명한다.
In its default mode, git pull is shorthand for git fetch followed by git merge FETCH_HEAD.
번역하자면, 디폴트모드에서는 git pull이 git fetch, 즉 git merge FETCH_HEAD의 작업에 대한 간단명령어(속기)라는 말이다.
다시 말해 git pull의 내부 동작이 git merge FETCH_HEAD처럼 동작한다는 말이다.
따라서 이 디폴트모드에서는 git pull을 수행했을 때 git merge에 대한 불필요한 commit이 자동으로 생긴다.
기존 git pull의 문제 상황 예시
실제로 기존의 default git pull을 할 때 어떤 문제가 생기는지, 불필요한 커밋은 어디서 생기는지 문제 상황이 정리된 그림이 있어서 가져왔다. Local Branch에 작업하고 있는 도중인데 누군가에 의해 remote에 또다른 commit이 생긴 아래 상황을 보자.
(1) 불필요한 커밋
여기서 별도의 옵션을 주지 않은 채로 default대로 git pull 또는 git pull origin master를 실행하게 되면 아래처럼 된다. local master랑 remote의 origin/master가 합쳐지면서 새로운 commit(불필요한)이 생기는 문제가 발생한다. 우리가 자주 보던 merge commit을 말하는 거임!
(2) 또다른 문제
이 상태에서 내가 another-branch라는 브랜치에 checkout 한 뒤, 거기서 다시 git pull이나 git pull origin master를 하면 당연히 local master에 merge되는 게 아니라 내가 작업 중인 another-branch에 remote의 origin/master가 merge된다. 아래 오른쪽 그림처럼 된다.
git pull 전략들
- git config pull.rebase false #merge (the default strategy)
- git config pull.rebase true #rebase
- git config pull.ff only # fast-forward only
앞서 위의 세가지 전략이 있다며 이 중에 고르라는 hint를 봤었다. 이 중에 뭘 선택할지 정하려면 각각이 뭔지 알아야 한다.
일단 1번은 위에서 말한 내용처럼 기존의 default git pull이라서 우리가 알던 merge처럼 새로운 merge commit이 생기는 전략이다.
그럼 이제 #rebase랑 #fast-forward only는 무슨 차이일까? 이걸 알기 전에 fast-forward랑 rebase부터 알아보고 가자..!
fast-forward 관계 + Merge vs Rebase
fast-forward란?
분기한 브랜치의 커밋 히스토리가 기존 브랜치의 커밋 히스토리를 포함하고 있는가에 따라 fast forward 관계가 정해진다.
[ Fast-Forward 관계 (O) ]
master에서 feature로 분기된 브런치의 커밋 히스토리는 A-B-X-Y이다. 글고 기존 브랜치인 master의 커밋 히스토리는 A-B이다. 따라서 분기된 브랜치인 feature의 히스토리는 기존 master 브랜치의 히스토리를 포함하고 있으니 이는 Fast-Forward 관계다. 이런 관계에서 master 브랜치에서 git merge [feature브랜치명]을 실행해서 병합하면 merge에 대한 new commit이 생기지 않고 HEAD의 위치만 변하게 된다. 브랜치를 그대로 따라가게 된다.
[ Fast-Forward 관계 (X) ]
분기된 feature 브랜치의 커밋 히스토리는 A-B-X-Y인데 기존 브랜치인 master의 현재까지의 커밋 히스토리가 A-B-C이다. feature의 히스토리가 master의 히스토리를 모두 담고 있지는 않다. C가 새로 생겼다. 여기서 둘을 merge하면 새로운 merge 커밋이 생기는 것이 어쩌면 당연한 것 같다. feature 입장에서는 인지하지 못한 새 커밋이 생겼으니까 말이다...! 아무튼 merge commit을 생성하면서 병합된다.
git rebase, merge란?
git rebase랑 merge 모두 어떤 브랜치의 변경 사항을 다른 브랜치로 통합하도록 설계됐다. 근데 이 통합하는 방식이 다르다.
merge
main 브랜치를 feature에 merge로 합쳐보자.
git checkout feature
git merge main
// 위 두 줄을 한 줄로 압축하면 git merge feature main 이다.
merge commit이 새로 생겼다. merge는 비파괴적인 방식이라 어떤 방식으로든 기존 브랜치가 변경되지 않는다. 잠재적인 위험을 방지할 수 있다.
rebase
main 브랜치를 feature에 rebase로 합쳐보자.
git checkout feature
git rebase main
merge commit을 새로 만들지 않고 원본 브랜치에서 각 커밋에 대해 새로운 커밋을 만들어서 프로젝트 기록 자체를 다시 작성한다. 이로 얻을 수 있는 이점은 프로젝트 히스토리를 더 명확하게 얻을 수 있다는 것이다.
- git merge에서 생기는 불필요한 merge commit을 제거한다.
- 완벽한 선형 히스토리를 만들 수 있다. git log 등의 명령으로 더 쉽게 탐색할 수 있다.
근데 2번 같은 경우 더 치명적일 수도 있다. 협업 워크 플로우에서는 history가 중요한데 이걸 임의로 변경시키기 때문에 rebase를 쓸 땐 주의가 필요하다. 나중에 아래에서 전략 선택할 때 다시 언급할 예정이다. 일단은 이게 뭔지 알고만 가자..!
Git Merge의 종류 네 가지
- 1) git merge (-ff)
- 그냥 보통의 병합으로, 융통성이 있음.
- 현 브랜치랑 병합할 브랜치가 fast-forward 관계라면 새로 merge commit을 만들지 않고 병합함.
- 현 브랜치랑 병합할 브랜치가 fast-forward하지 않은 관계라면 새로 merge commit을 만들고 병합함.
- 2) git merge --no-ff
- 현 브랜치랑 병합할 브랜치가 fast-forward이냐 아니냐에 상관 없이 무조건 merge commit을 새로 만듦.
- 3) git merge --ff-only
- 현 브랜치랑 병합할 브랜치가 fast-forward일 경우에만 병합을 진행하고, 이때 merge commit을 만들지 않음.
- ff 관계가 가능할 때에만 하고 그렇지 않을 때는 merge를 거절한댄다!
거절할 때 이런 에러가 난다.
* branch master -> FETCH_HEAD 593a5e1..208efa3 master -> origin/master fatal: Not possible to fast-forward, abortin
- 4) git merge --squash
- 현 브랜치랑 병합할 브랜치에 대해서 서로 다른 commit이 2-3개 있다고 가정할 때 이 2-3개의 커밋 내용을 하나의 커밋으로 합쳐서 커밋함.
docs에서 정리하는 말은 아래와 같다.
추가로 git PR에 있는 merge 버튼들을 보면 세 가지가 있는데 각각 해석하자면 아래와 같다.
(1) Merge Commit -> 그냥 일반 merge. merge 커밋을 때때로 만드는 그런 merge
(2) Squash and Merge -> 위 squash 그림처럼 되고
(3) Rebase and Merge -> 는 아래처럼 합치려던 브랜치의 커밋들을 master에게 복사한다. 아래 그림처럼.
#rebase와 #fast-forward only의 차이
- git config pull.rebase true(#rebase)
- git config pull.ff only(#fast-forward-only)
이제 위의 글들을 다 봤으면 이 둘의 차이는 금방 이해가 되는 것 같다.
rebase의 결과는
- 가져올 대상 브랜치의 commit들을 병합의 목적지인 브랜치로 복사했으니까 새로운 merge commit이 생기지 않는다. 어찌보면 병합이라고 하기도 좀 그런 것 같다.
- 아래 그림에서 rebase하기 전 모습을 보면 master 브랜치에 이미 feature가 모르는 새로운 커밋들이 있으므로 fast-forwad하지 않은 상태다. 그러모르 ff only를 주면 merge가 되지 않을 것이다.
ff only의 결과는
- 일단 fast-forward하지 않은 바로 위 같은 상황에서는 병합이 안된다.
- fast-forward한 상태에서 그때서야 병합이 가능하다.
- 위에 첨부했던 fast-forward 상태에서의 merge 그림을 다시 가져와봤다.
선택 & 기본 환경 세팅
그래서 셋 중에 뭘 선택해야 할지 다시한번 정리해보자.
- git config pull.rebase false #merge (the default strategy)
- git config pull.rebase true #rebase
- git config pull.ff only # fast-forward only
1은 ff가 아니면 그냥 merge commit 만들면서 병합하고, ff이면 merge commit 없이 병합한다.
2는 merge가 아니라 커밋 자체를 복사해온다.
3은 ff가 아니면 에러 내면서 병합 자체를 refuse한다. ff일 때만 merge commit 없이 병합한다.
내가 본 외국 블로그에서는 여전히 --ff-only를 더 추천한다고 한다. rebase해버리면 현재 브랜치가 있었다는 기록을 영구적으로 알 수 없어서이다. 근데 --ff-only는 ff관계가 아닐 때마다 refuse 나면서 날 혼란스럽게 할 것 같다.
내 생각으로는, 협업 프로세스에서는 '이 브랜치가 있었다' 라는 기록 정도는 남아있는 게 좋을 것 같다. 그래서 나라면 ff관계가 아닐 때에도 merge는 수행하면서 새로운 merge commit을 만들어 두는 1번(git config pull.rebase false)를 선택할 것 같다. 아니면 그냥 rebase를 하거나..!
뭐가 됐든 --ff-only는 좀 꺼두는 게 좋을 듯 하다. 이 경우는 fast-forward만 하겠다는 말이라서 위에서 말했듯이 안되는 경우마다 다 refuse 나니까.. 얘를 꺼두려고 한다. 아래 명령어로 끄면 된다고 한다. (난 놑북 다시 세팅한 뒤로 git 버전이 낮아서 이런 에러가 안나타나서... 적용 안했다ㅎㅎ)
git config --unset pull.ff
[참고]
https://blog.sffc.xyz/post/185195398930/why-you-should-use-git-pull-ff-only
https://otzslayer.github.io/git/2021/12/05/git-merge-fast-forward.html
https://www.atlassian.com/ko/git/tutorials/merging-vs-rebasing
https://minemanemo.tistory.com/46
https://velog.io/@injoon2019/Git-Merge-%EC%A2%85%EB%A5%98