2편 - 브랜치

2021. 9. 21. 01:13배움엔 끝이없다/Git 배워보기

챕터3. 브랜치

브랜치 개념

깃에서의 브랜치와 커밋은 포인터입니다.

하나의 커밋은 스냅샷을 가리키는 포인터이고, 스냅샷은 각 blob(각 파일들의 스냅샷)들을 가리키는 포인터들을 가집니다.

새로운 커밋은 하면 가장 최근 커밋을 가리키는 포인터를 가집니다.

브랜치는 마지막으로 한 커밋을 가리키는 포인터입니다. (정말 41바이트짜리 포인터만으로 구성돼있다고 합니다.)

또 HEAD라는 포인터는 현재 로컬 브랜치를 가리키고, 새로운 커밋을 하면 이 HEAD가 가리키는 브랜치가 그 커밋을 가리킵니다. 여러 브랜치를 사용하면 HEAD 브랜치는 새로운 커밋을 가리키고, 다른 브랜치는 이전 커밋을 가리키면서 분기가 생깁니다. (diverged history)

이와 관련된 개념이 https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell 에 정말 잘 정리돼있습니다.

 

브랜치 관련 기본 명령어

git branch <이름> : 새로운 브랜치 만듭니다. 이 브랜치는 현재 커밋을 가리킵니다.

git log --decorate : 어떤 브랜치가 어떤 커밋을 가리키는지 확인할 수 있습니다.

git log --oneline --decorate --graph --all : 브랜치 상황을 그래피컬 하게 볼 수 있습니다.

git checkout <브랜치> / git switch <브랜치> : 로컬 브랜치를 해당 브랜치로 바꿉니다. (HEAD가 가리키게 합니다.) working directory도 해당 브랜치의 상태로 바뀝니다. 브랜치를 바꿀 땐 working tree를 clean한 상태(모든 변화가 commit된 상태)여야 합니다.

git merge <브랜치> : 로컬 브랜치를 해당 브랜치와 merge 합니다.

git branch -d <브랜치> : 해당 브랜치를 삭제합니다.

 

Fast-forward Merge

브랜치 A가 커밋3을 가리키고, 브랜치 B가 커밋3에 추가사항을 반영한 커밋 4를 가리키는 상황이라고 할 때,

브랜치 A를 브랜치 B에 Merge 시키는 동작을 Fast-forward라고 합니다. (A가 B를 따라잡게 하기)

이렇게 브랜치 B의 커밋의 history (커밋 포인터)로 접근할 수 있는 커밋을 가리키는 브랜치 A를 머지하는 상황을 Fast-forward라고 하며, 단순히 브랜치 A의 포인터를 브랜치 B로 바꾸는 것으로 해결됩니다.

이는 아래와 같은 명령으로 수행 가능합니다.

git checkout A

git merge B

이렇게 한 후엔 브랜치 A와 브랜치 B가 같은 커밋을 가리키기 때문에 하나를 없애도 됩니다.

git branch -d B

 

기본 Three-way Merge

커밋3으로부터 분기된 히스토리를 가진 두 개의 브랜치 master(커밋4)와 issue(커밋5)가 있는 상황이라고 할 때,

issue 브랜치를 master 브랜치로 merge 하려면 아래와 같은 명령을 사용합니다. (issue의 내용을 master에 적용)

git checkout issue

git merge master

이렇게 할 경우 issue의 커밋5가 master의 커밋4를 가리키지 않기 때문에 깃에서 three way merge를 시행합니다.

공통의 부모커밋인 커밋3, 각자의 마지막 커밋인 커밋4와 5 3가지 커밋을 바탕으로 merge를 수행한 후 그 변경 사항을 적용한 스냅샷을 새로운 커밋으로 만듭니다.

이 커밋은 부모커밋이 2개라는 점에서 특별합니다. (이전 커밋을 가리키는 포인터가 2개입니다.)

 

Conflict 해결

두 브랜치를 merge 하는 중 같은 파일을 수정했을 경우 conflict가 발생하여 merge가 일시중지됩니다.

conflict가 발생해 merge가 중단됐을 때 git status 명령어로 conflict가 난 파일을 볼 수 있습니다.

conflict가 발생한 파일을 보면

<<<<<<< HEAD:example.c

HEAD 브랜치에 있던 내용

=======

Merge를 실행한 대상 브랜치에 있던 내용

>>>>>>> issue:example.c

의 형식으로 conflict가 표시되며, 이 부분을 손수 고쳐주고 git add 명령어로 해결됐음을 표시합니다.

모든 conflict를 해결한 후 git commit 명령어로 merge commit을 마무리할 수 있습니다.

 

브랜치 조회

git branch 명령어로 브랜치의 목록과 HEAD를 확인할 수 있습니다.

-v : 브랜치들의 커밋을 볼 수 있습니다.

--merged / --no-merged : HEAD에 merge를 한 브랜치나 하지 않은 브랜치를 확인할 수 있습니다.

 

브랜치 이름 변경

브랜치 이름을 변경할 땐 다른 collaborater가 사용하고 있지 않아야 합니다.

git branch --move a b : 로컬에서 브랜치 a의 이름을 b로 바꿉니다.

git push --set-upstream <리모트이름> b : 새로 바뀐 브랜치 이름을 리모트에 등록합니다. 이 때 이전 이름을 가진 브랜치가 남아있습니다.

git push <리모트이름> --delete  a : 기존 브랜치 이름을 리모트에서 삭제합니다.

기본 브랜치인 master(main)브랜치의 이름을 바꾸는 데에는 더 많은 작업이 필요합니다.

 

Topic Branching

깃은 다른 버전관리 시스템과 다르게 브랜칭에 드는 비용이 매우 적기 때문에 브랜치를 만들고 삭제하는 것을 장려합니다.

Topic branching은 브랜치를 활용하는 방법 중 하나로, 언제든지 master 브랜치에서 분기해 이슈들을 해결하고 원할 때 원하는 브랜치를 merge 할 수 있습니다.

 

Remote Branches

Remote branch는 리모트 리포지토리에 있는 브랜치에 대한 읽기전용 레퍼런스로, 로컬에서 사용하는 브랜치들과 구분됩니다.

이름은 <remote>/<branch>의 형태를 가지며, remote branch는 네트워크 커뮤니케이션을 할 때마다 자동으로 업데이트되어 로컬 브랜치와 다른 값을 가질 수 있습니다.

이런 특징 때문에 내가 master브랜치에 커밋을 추가하고, 다른 사용자가 master 브랜치에 커밋을 추가한 후 push 하고, 내가 fetch를 하면 origin/master과 로컬의 master는 서로 다른 commit을 가리키게 되어 분기가 생깁니다.

git ls-remote <remote> / git remote show <remote> : 해당 리모트의 리모트 브랜치들에 대한 정보를 볼 수 있습니다.

git push <remote> --delete <branch> : remote 서버에 있는 브랜치를 삭제합니다. 

 

Pushing

브랜치를 서버에 올려 사용자들과 공유하려면 해당 로컬 브랜치를 서버에 push 해야 합니다.

git push <remote> <branch> : 로컬의 branch를 remote/branch에 push합니다.

git push <remote> <branch>:<branch2> : 로컬의 branch를  remote의 branch2로 push합니다.

 

Tracking Branch

만약 이렇게 새로운 브랜치를 푸쉬해 다른 사용자가 fetch로 받아왔다면, 그 사용자는 remote branch를 받은 것이므로 아래와 같은 명령어로 working tree에 있는 브랜치에 (혹은 새로 만들어서) merge 해야합니다.

이렇게 리모트 브랜치의 내용을 항상 업데이트하도록 만든 로컬 브랜치를 보고 리모트 브랜치를 tracking 한다고 합니다.

git checkout -b <로컬브랜치명> <리모트브랜치명> : 리모트 브랜치를 tracking하는 로컬 브랜치를 새로 만들고 checkout 합니다.

git checkout --track <리모트브랜치명> / git checkout <리모트브랜치의 브랜치명만> : 위와 같이 tracking 브랜치를 만드는 약식 표현입니다.

git branch -u <리모트브랜치명> : 로컬 브랜치가 리모트 브랜치를 tracking 하게 합니다.

git branch -vv : 로컬 브랜치들이 어떤 리모트 브랜치를 tracking 중인지 확인 할 수 있습니다.

 

Pulling

git pull은 fetch와 merge를 연달아 실행하는 것과 비슷한 동작을 합니다. 하지만 웬만하면 명시적으로 두 명령어를 사용하는 것이 좋다고 합니다.

 

Rebasing

Rebasing은 두 브랜치를 합칠 때 merge 대신 선택할 수 있는 방법으로, 이전 커밋들을 복사해 대상 브랜치에 똑같이 적용하는 방식입니다.

git checkout A

git rebase B   : A를 B에 rebase 합니다. 그러면 A의 커밋들이 B에도 적용된 상태의 스냅샷이 새로운 커밋으로 생깁니다.

git checkout B

git merge A  : rebase를 한 경우 B가 A보다 뒤쳐져있으므로 Fast-forward merge를 해줍니다.

 

Rebasing을 이용한 방법과 일반적인 Three-way merge는 결과로 생성되는 스냅샷은 똑같습니다. 차이점은 히스토리에서 오는데, Three-way merge는 분기됐다가 합쳐지는 모양이지만, Rebase는 마치 한번 에 쭉 진행한 것처럼 히스토리가 작성됩니다.

 

이걸 사용하는 경우는 내가 다른 프로젝트에 일시적으로 패치를 브랜치를 만들어 진행했다면, 프로젝트의 관리자가 Three-way merge를 할 경우 conflict들을 해결해야 하지만 내가 rebase를 해놨다면 Fast-forward merge만 하면 됩니다.

 

분기된 브랜치를 훨씬 이전에 분기된 브랜치에 rebase 할 수도 있습니다.

git rebase --onto A B C : B와 C가 분기된 지점부터 C까지의 커밋들을 A에 reabase 합니다.

 

Rebasing 사용 시 주의사항

Do not rebase commits that exist outside your repository and that people may have based work on.

라고 되어있었는데, 이게 무슨 뜻인지 잘 모르겠네요.

저 아래에 예시가 나와 있는데, 이미 three-way merge로 커밋한 내용을 push 했고 다른 사용자가 그 커밋A 기반으로 작업을 하고 있는데, 그 merge 커밋을 rebasing 방식으로 바꾼다면 새로운 커밋B가 생겨 커밋 A를 기반으로 작업하던 다른 사용자는 커밋 B와도 merge를 해야 하고, 이는 똑같은 내용을 담은 커밋 두 개를 merge 하는 작업이 됩니다. (커밋 B가 커밋A의 내용을 가지고 있기 때문입니다.) 이렇게 히스토리가 꼬이는 문제가 발생하기 때문에 rebasing을 사용할 때 주의하라는 것 같습니다.

 

Rebase vs Merge

rebase와 merge의 차이점은 히스토리가 어떻게 남느냐입니다.

실제로 어떤 일이 있었는지 기록하는 히스토리를 만드는 게 merge라면, 만약 merge 관련 실수 등으로 복잡하게 꼬인 히스토리를 보기 좋게 만들어주는 게 rebase입니다.

반응형

'배움엔 끝이없다 > Git 배워보기' 카테고리의 다른 글

상황별 git 유용한 명령 모음  (0) 2022.03.27
1편 - 깃 기초  (0) 2021.09.20