2023. 11. 9. 23:50ㆍ프로그래밍/Git
Git을 사용할 때 pull을 시도할 경우 자주 보게 되는 fast forward는 무엇일까
예전에 git을 처음 공부할 때 배웠던 개념에 더해 추가적인 정리를 하고자한다.
https://goldfish2679.tistory.com/33
* 글을 읽기에 앞서 필자의 주관적이고 추상적인 비유와 함께 설명하므로 참고할 것
1. fast forward
나무가 있다고 가정해보자. 이 나무에서는 하나의 기둥에서부터 여러개의 가지(branch)가 생긴다. 그리고 그 가지로부터 또 여러개의 가지, 여러개의 가지...로 나뉘어지고 잎사귀가 달리게 된다.
fast forward방식은 쉽게 말해서 하나의 가지가 다른 가지로 뻗쳐나가는 것이 아닌 쭉 이어지는 형태라는 뜻이다.
예를 들어 내가 A, B라는 두 개의 커밋을 가지고 있다. 이 때의 브랜치는 feature1이라고 하자.
여기서 이제부터 개발하는 부분은 다른 기능이라 브랜치 이름을 바꾸고 싶다. 그리고 C라는 작업부터는 feature2 브랜치에 commit을 한 상황이다. 이럴 경우에는 하나의 가지에서 쭉 이어지는 형태이기 때문에 이름만 바뀐 것일뿐 서로 같은 브랜치라고 본다.
즉 어떤 가지가 자라는데 그 가지가 올곧게 자라다가 일정 길이가 됐을 때 그 가지의 이름을 바꾼 것과 마찬가지다. 그러나 가지의 이름만 바뀐다고 해서 그 가지가 본질 자체가 바뀐 것은 아니다.
즉 이것이 브랜치의 History라는 개념이다. History가 서로 같은 경우 git에서는 fast forward관계라고 한다.
fast forward관계에 있을 시에는 결국 한개의 가지에서 어떤 시점에 있느냐만 다를 뿐이기 때문에 아래쪽 커밋에서 최신 브랜치를 pull(merge)하더라도 별도의 병합 커밋을 생성하지 않는다. 어차피 브랜치의 최신 상태는 이전의 커밋의 히스토리를 가지고 있기 때문에 같은 것이라고 보고 conflict없이 깔끔하게 병합이 되는 것이다.
2. rebase
rebase는 분기된 서로 다른 브랜치가 있다고 했을 때 두개의 브랜치를 병합하는 것이 아니라 A라는 브랜치의 변경사항을 B라는 브랜치의최신 상태로 이어주고 싶을 떄 사용하는 것이다. 이렇게 두 개의 브랜치를 합쳐버리면 히스토리를 선형적으로 깔끔하게 관리할 수 있게 된다. 이는 팀 프로젝트의 규모가 크고 팀원이 많으며 많을 수록 더욱 강력한 기능으로 사용할 수 있을 것이다.
3. merge
숱하게 들어본 merge이다. merge의 종류에는 fast forward방식과 3-way방식이 있다.
fast forward는 앞에서 언급했듯이 같은 히스토리를 가지는 서로 다른 두개의 브랜치를 합치는 것이고 이는 별도의 병합 커밋을 생성하지 않고 단순히 merge, pull하고자 하는 HEAD의 변경사항만 반영되는 것과 마찬가지다.
3-way merge가 우리가 자주 사용하는 방식이라고 할 수 있다.
3-way라고 표현하는 이유는 하나의 분기점에서 서로 다른 두개의 브랜치가 분기됐을 때 이것이 3way이기 때문이다(뇌피셜).
즉 같은 분기점에서 분기가 됐다고 하더라도 서로 다른 변경사항을 가지고 있다면 이는 fast forward방식이 아니게 된다.
따라서 git pull을 시도할 때 not fast forward과 같이 pull하려는 브랜치와 현재 로컬의 브랜치의 관계가 fast forward방식이 아니리서 병합 하지 못한 경우가 꽤 많을 것이다. 이는 다음과 같이 Hint에서도 알려주듯이 ff(fast forward)방식으로 3way의 브랜치를 병합하고자 할 때 생기는 오류이다.
git config pull.ff only # fast forward방식으로만 merge를 허용하겠다
git config pull.reabase false # 3way merge(default)
git config pull.rebase true # rebase방식으로 병합하겠다
git config --unset pull.ff # fast forward 방식의 merge를 하지 않겠다.
위와 같이 다양한 옵션을 사용해 merge / pull 시의 이슈를 해결할 수 있다.
4. pull
pull은 fetch와 merge를 합친 명령어이다.
fetch는 쉽게 말해서 현재 원격저장소의 변경사항을 읽어들인 다음에 merge를 실행하겠다는 것이다.
왜 fetch를 해야하는지는 당연히 알 것이다. 원격저장소(remote)에 최신 사항이 반영돼있다면 당연하게도 최신 상태와 맞춰서 병합하는 편이 이후에 병합된 사항을 다른 사용자가 적용하기에도 편하다. 만약 최신 사항을 적용시키지 않고 merge하게 된다면 다른 사용자에게 변경사항이 많이 생긴 후라면 그 사용자가 병합된 부분을 pull했을 때 conflict(충돌)가 많이 발생할 것이다.
5. checkout
checkout은 여러모로 유용한 기능을 가진 명령어이다. checkout은 여러가지 기능을 제공하는데 대표적으로는 다음과 같다.
1. 특정 commit log로 이동
2. 특정 브랜치(최신 상태)로 이동 -> 원격 브랜치가 있다면 원격브랜치로, 원격은 없고 로컬만 있다면 로컬브랜치로 이동
3. git checkout -b 를 이용해서 로컬 branch를 생성함과 동시에 해당 브랜치로 이동
git checkout 22e1e56c1dafcd15e1818c**33e4d3ac1d**82 # 로그명을 이용해 이동
git checkout <branch> # 원격의 브랜치가 있을 경우 remote에 설정한 고유 이름(일반적으로 origin)을 딴 origin/<branch>로
이동하고 로컬환경에 해당 원격 브랜치의 변경사항이 모두 반영된다.
git checkout -b <branch> # 로컬에 브랜치를 새로 생성하면서 해당 브랜치로 현재 브랜치를 바꾼다.
checkout 기능은 여러모로 복잡하고 다양한 기능들을 제공하기 때문에 헷갈릴 수 있다. 따라서 후술할 switch기능을 이용하는 편이 브랜치 간 변경이 훨씬 수월하다.
git branch명령어는 여러가지 제약 사항이 있다.
git branch <branch> : 로컬 브랜치 생성, 이 브랜치를 이용해 원격에 push할 경우 원격에 같은 이름의 브랜치가 있다면 거기 push가 되는 거고 없다면 새로운 브랜치가 생성되는 것 ex)origin/<branch>
git branch -m <branch> : 현재 브랜치명을 다음과 같이 바꾸겠다는 것. 그러나 현재 로컬에 생성해놓은 브랜치가 있다면 바꿀 수 없음.
git branch -d 혹은 git branch -D를 이용해 제거해야 한다.
git branch -d, git branch -D : 로컬 브랜치를 삭제하는데 조건에 따라 다른 듯 하다. 삭제하려는 브랜치와 현재 브랜치의 관계에 따라 -d를 쓰고 -D 옵션을 써야한다.
* git checkout -b <branch>는 특정 커밋으로 checkout했을 때 git branch명령어를 사용하면 HEAD detached at <log name>과 같이 나와있을 것이다. 이 때 checkout을 했기 때문에 해당 커밋의 변경사항은 모두 가지고 있으면서 -b <branch> 옵션을 통해 로컬 브랜치를 새로 생성하면서 그 브랜치로 브랜치를 바꿀 수 있게 된다.
또한 git checkout을 통해 특정 커밋 지점 혹은 브랜치로 이동하게 되면 그쪽으로 HEAD가 이동하게 된다.
HEAD에 대한 개념은 후술한다.
요약 : git checkout은 단순히 브랜치만 바꾸는 것이 아닌 원격 저장소의 변경사항을 로컬에 가지고 올 때, 혹은 병합이 필요할 때 HEAD를 돌리기 위한 경우에 사용한다(필자는 이 두가지 경우에 가장 많이 사용했다)
6. switch
git 2.24 버전부터 추가된 기능. 위에서 봤듯 checkout 기능은 여러가지 기능을 포함하고 있기 때문에 조금 난해할 수 있다.
또한 git branch 기능도 여러모로 로컬의 브랜치를 삭제했다가 바꿔줬다가 생성했다가 등 귀찮은 점이 많다.
git switch는 이같은 단점을 보완한 기능으로 생성돼있는 로컬 브랜치로 바로 브랜치를 변경할 수 있다
git switch <local branch name>
7. HEAD
아마 git에 있어서 손가락에 꼽히는 중요한 개념이 아닐까 생각된다.
HEAD와 로컬브랜치, 리모트 브랜치의 관계만 잘 파악하면 간단한 병합정도는 쉽게 할 수 있다.
HEAD는 아주 쉽게 설명하자면 그냥 현재 브랜치, 혹은 특정 커밋을 가르키는 포인터의 역할을 한다.
그럼 포인터가 중요한 이유는 무엇인가? pull이나 push, merge 등의 작업을 할 때는 모두 이 HEAD를 기준으로 하기 때문이다.
특별한 상황이 아니고서야 내가 작업하고 있는 최신 commit 혹은 브랜치에 HEAD가 위치해있을 것이다. 그러나 어떠한 이유로 HEAD가 제 위치에 있지 않다면 checkout을 통해 변경해야함을 잊지말자.
위 내용을 모두 종합해 아래 커밋을 병합해보자.
다음과 같은 커밋 히스토리가 있을 때 CRUD complete 커밋으로 checkout을 한 후 변경사항을 만들고 commit하게 되면 다음과 같이 HEAD가 commit 내역으로 이동하게 된다. 보면 현재 로컬브랜치가 main으로 설정돼있는데 이 같은 경우에는 push할 수 없게 된다(non-fast-forward에러 발생). 위에서 설명했듯 같은 히스토리를 가진 것이 아닌 3way로 분기된 커밋이기 때문이다.
따라서 새 브랜치를 판 후 git config pull.rebase false를 이용하고 이래도 병합이 안되면 git config --unset pull.ff 를 설정하게 되면 3way 병합이 가능해진다.
추가적으로 git log를 통해 커밋 로그를 확인해보면 현재 로컬 브랜치, remote 브랜치가 뭐가 있는지 HEAD가 어디에 위치해있는지 모두 확인할 수 있다.
배운 내용을 토대로 브랜치를 병합해보는 시간을 가져보자.
'프로그래밍 > Git' 카테고리의 다른 글
[Git] "fatal: could not read username for no such device or address" 해결 (0) | 2024.11.20 |
---|---|
Git stash로 다른 브랜치에 로컬 변경사항 옮겨서 적용하기 (0) | 2023.11.22 |