본문 바로가기
프로그래밍 놀이터/Tips

[도서 정리] 3. Git 브랜치 - ProGit

by 돼지왕 왕돼지 2020. 1. 7.
반응형

3. Git 브랜치 - ProGit




3.1. 브랜치란 무엇인가


-

Git 은 데이터를 변경사항(Diff)으로 기록하지 않고 일련의 스냅샷으로 기록한다.

커밋하면 Git 은 현 staging area 에 있는 데이터의 스냅샷에 대한 포인터, 저자나 커밋 메시지 같은 메타데이터, 이전 커밋에 대한 포인터 등을 포함하는 커밋 개체(커밋 Object)를 저장한다.

이전 커밋 포인터가 있어서 현재 커밋이 무엇을 기준으로 바뀌었는지를 알 수 있다.

최초 커밋을 제외한 나머지 커밋은 이전 커밋 포인터가 적어도 하나씩 있고 브랜치를 합친 merge 커밋 같은 경우에는 이전 커밋 포인터가 여러 개 있다.



-

파일을 stage 하면 git 저장소에 파일을 저장하고(git 은 이것을 blob 이라고 부른다.) staging area 에 해당 파일의 체크섬을 저장한다.

git commit 으로 커밋하면 먼저 루트 디렉터리와 각 하위 디렉터리의 트리 개체를 체크섬과 함꼐 저장소에 저장한다.

그 다음에 커밋 개체를 만들고 메타데이터와 루트 디렉터리 트리 개체를 가리키는 포인터 정보를 커밋 개체에넣어 저장한다.

그래서 필요하면 언제든지 스냅샷을 다시 만들 수 있다.



-

$ git add README test.rb LICENSE

$ git commit -m ‘initial commit’


위 명령어를 수행하면 총 다섯 개의 데이터 개체가 생긴다.

각 파일에 대한 blob 세 개, 파일과 디렉터리 구조가 들어 있는 트리 개체 하나, 메타 데이터와 루트 트리를 가리키는 포인터가 담긴 커밋 개체 하나이다.

3. Git 브랜치 - ProGit, Ahead, Behind, fast forward merge, fast-forward, file checksum, git 3 way merge, git 3-way merge, git @{upstream}, git @{u}, git blob, git branch, git branch *, git branch --set-upstream-to, git branch -d, git branch -uy, git branch -v, git branch -vv, git branch delete, git branch force delete, git branch tutorial, git branch workflow, git btanch, git check -b, git checkout, git checkout --track, git checkout -b, git checkout branch, git clone -o, git commit object, git commit pointer, git config credential.helper cache, git config pull.rebase, git conflict, git credential cache, git delete remote branch, git develop branch, git diff, git fastforward, git fetch, git hash length, git log --decorate, git long-running branch, git ls-remote, git master branch, git merge, git merge branch, git merge tutorial, git mergetool, git next branch, git patch, git pull, git pull --rebase, git push, git push --delete, git push --force, git push to different name branch, git rebase, git rebase --onto, git rebase history, git rebase [source_branch_name], git remote branch, git remote show, git snapshot, git topic branch, git tree object, git unmerged, git upstream branch, git when to rebase, git workflow, Head, head pointer, Master, pull.rebase, rebase fastforward, rebase vs merge, rebase 활용, save file as hash, snapshot tree, track, tree object, unmerged, 리모트 브랜치, 브랜치 워크플로우, 워킹 디렉터리, 커밋 객체, 커밋 오브젝트, 토픽 브랜치



-

이후 파일을 수정 한 후 commit 하면 아래와 같은 그래프가 된다.


3. Git 브랜치 - ProGit, Ahead, Behind, fast forward merge, fast-forward, file checksum, git 3 way merge, git 3-way merge, git @{upstream}, git @{u}, git blob, git branch, git branch *, git branch --set-upstream-to, git branch -d, git branch -uy, git branch -v, git branch -vv, git branch delete, git branch force delete, git branch tutorial, git branch workflow, git btanch, git check -b, git checkout, git checkout --track, git checkout -b, git checkout branch, git clone -o, git commit object, git commit pointer, git config credential.helper cache, git config pull.rebase, git conflict, git credential cache, git delete remote branch, git develop branch, git diff, git fastforward, git fetch, git hash length, git log --decorate, git long-running branch, git ls-remote, git master branch, git merge, git merge branch, git merge tutorial, git mergetool, git next branch, git patch, git pull, git pull --rebase, git push, git push --delete, git push --force, git push to different name branch, git rebase, git rebase --onto, git rebase history, git rebase [source_branch_name], git remote branch, git remote show, git snapshot, git topic branch, git tree object, git unmerged, git upstream branch, git when to rebase, git workflow, Head, head pointer, Master, pull.rebase, rebase fastforward, rebase vs merge, rebase 활용, save file as hash, snapshot tree, track, tree object, unmerged, 리모트 브랜치, 브랜치 워크플로우, 워킹 디렉터리, 커밋 객체, 커밋 오브젝트, 토픽 브랜치



-

git 의 master 는 특별하지 않다.

다른 브랜치와 다른 것이 없다.

다만 모든 저장소에서 master 브랜치가 존재하는 이유는 git init 명령으로 초기화할 때 자동으로 만들어진 이 브랜치를 애써 다른 이름으로 변경하지 않기 때문이다.




* 새 브런치 생성하기


-

git branch branch_name 으로 생성한다.

$ git branch [branch_name]



-

Git 은 HEAD 라는 특수 포인터가 있다.

이 포인터는 지금 작업하는 로컬 브랜치를 가리킨다.



-

git log 명령에 —decorate 옵션을 사용하면 쉽게 브랜치가 어떤 커밋을 가리키는지도 확인할 수 있다.

$ git log --decorate




* 브랜치 이동하기


-

git checkout branch_name 으로 브랜치를 이동할 수 있다.

$ git checkout [branch_name]



-

브랜치를 이동하면 워킹 디렉터리의 파일이 변경된다.

파일 변경 시 문제가 있어 브랜치를 이동시키는 게 불가능한 경우 git은 브랜치 이동 명령을 수행하지 않는다.



-

히스토리를 전체적으로 보여준다.

$ git log —oneline —decorate —graph —all 



-

git 의 브랜치는 어떤 한 커밋을 가리키는 40글자의 SHA-1 체크섬 파일에 불과하기 때문에 만들기도 쉽고 지우기도 쉽다.

새로 브랜치를 하나 만드는 것은 41바이트 크기의 파일(40자와 줄 바꿈 문자)을 하나 만드는 것에 불과하다.





3.2. 브랜치와 Merge 의 기초


* 브랜치의 기초


-

브랜치를 만들면서 checkout 까지 한번에하려면 -b 옵션을 준다.

$ git checkout -b [branch_name]



-

브랜치를 이동하려면 해야 할 일이 있다.

아직 커밋하지 않은 파일이 checkout 할 브랜치와 충돌 나면 브랜치를 변경할 수 없다.

브랜치를 변경할 때에는 워킹 디렉터리를 정리하는 것이 좋다.

이런 문제를 다루는 방법은 주로 stash 와 amend 를 사용한다.



-

git merge branch_name 을 통해 현재 branch 에 branch_name 의 내용을 merge 할 수 있다.

$ git merge [merge_source_branch_name]



-

merge 할(source) 브랜치가 가르키는 커밋이 현 브랜치 커밋의 upstream 브랜치일 경우 master 브랜치 포인터는 merge 과정 없이 그저 최신 커밋으로 이동한다

이런 머지 방식을 fast-forward 라고 부른다.



-

-d option 을 주어 branch 를 삭제할 수 있다.


ex) 

$ git branch -d hotfix




* Merge 의 기초


-

merge 할 브랜치가 현재 브랜치의 upstream 이 아니라면 fast-forward merge 를 할 수 없다.

이 때는 커밋 두 개와 공통 조상 하나를 사용하여 3-way merge 를 한다.


git 은 머지하는데 필요한 최적의 공통 조상을 조상으로 찾는다.




* 충돌의 기초


-

가끔 3-way merge 도 실패할 때가 있다.

merge 하는 두 브랜치에서 같은 파일의 한 부분을 동시에 수정하고 merge 하면 git 은 해당 부분을 merge 하지 못하기 때문이다.



-

머지 충돌이 일어나면 git 이 자동으로 머지하지 못했기에 새 커밋이 생기지 않는다.

머지 충돌이 일어났을 때 git 이 어떤 파일을 머지 할 수 없었는지 살펴보려면 git status 명령을 이용한다.


충돌이 일어난 파일은 unmerged 상태로 표시된다.

그리고 git 은 충돌이 난 부분을 표준 형식에 따라 표시해준다.



-

충돌을 해결하려면 위쪽(merge 당하는 쪽) 또는 아래쪽(merge 대상)을 선택하거나 아예 새로 작성해야 한다.

충돌을 해결한 후 git add 명령으로 다시 git 에 저장한다.

$ git add [confliect_resolved_file_name]



-

merge 도구로 충돌을 해결할 수도 있다. git mergetool 명령으로 실행한다.

$ git merge tool

merge 도구를 종료하면 git 은 잘 머지했는지 물어본다.

잘 마쳤다고 입력하면 자동으로 git add 가 수행되고 해당 파일이 staging area 에 저장된다.

git status 명령으로 충돌이 해결된 상태인지 다시 한 번 확인해 볼 수 있다.







3.3. 브랜치 관리


-

아무런 옵션 없이 git branch 를 수행하면 branch 목록을 보여준다.

* 가 붙어있는 브랜치는 현재 checkout 해서 작업하는 브랜치를 나타낸다.

$ git branch



-

git branch -v 를 수행하면 브랜치마다 마지막 커밋 메시지도 함께 보여준다.

—merged 와 —no-merged 옵션을 사용하여 merge 된 브랜치인지 그렇지 않은지 필터링해 볼 수 있다.

$ git branch -v



-

머지되지 않은 브랜치를 git branch -d branch_name 으로 삭제하려고 하면 삭제되지 않는다.

강제로 삭제해야 하는데 이 때는 -D 옵션을 사용한다.

$ git branch -D [branch_name]





3.4. 브랜치 워크플로우


* Long-Running 브랜치


-

git 개발자가 선호하는 워크플로가 하나 있다.

배포했거나 배포할 코드만 master 브랜치에 merge 해서 안정 버전의 코드만 master 브랜치에 둔다.

개발을 진행하고 안정화하는 브랜치는 develop 이나 next 라는 이름으로 추가로 만들어 사용한다.

이 브랜치는 언젠가 안정 상태가 되겠지만, 항상 안정 상태를 유지해야 하는 것이 아니다.

테스트를 거쳐 안정적이라고 판단되면 master 브랜치에 merge 한다.


토픽 브랜치(짧은 호흡 브랜치)에도 적용할 수 있는데, 해당 토픽을 처리하고 테스트해서 버그도 없고 안정적이면 그 때 머지한다.



-

중요한 개념은 브랜치를 이용해 여러 단계에 걸쳐서 안정화해 나아가면서 충분히 안정화가 됐을 때 안정 브랜치로 merge 한다는 점이다.

다시 말해서 long-running 브랜치가 여러 개일 필요는 없지만 정말 유용하다 할 수 있다.




* 토픽 브랜치


-

토픽 브랜치는 어떤 한 가지 주제나 작업을 위해 만든 짧은 호흡의 브랜치다.





3.5. 리모트 브랜치


-

리모트 Refs 는 리모트 저장소에 있는 포인터인 레퍼런스다.

리모트 저장소에 있는 브랜치, 태그 등등을 의미한다.



-

모든 리모트 Refs 를 조회할 수 있다.

$ git ls-remote


모든 리모트 브랜치와 그 정보를 보여준다.

$ git remote show [remote_name]




-

리모트 브랜치의 이름은 remote_name/branch_name 형식으로 되어 있다.

예를 들어 origin 의 master 브랜치를 보고 싶다면 origin/master 라는 이름으로 브랜치를 확인하면 된다.



-

아래와 같이 clone 하면 origin 대신 remote_name 을 사용한다.

$ git clone -o [remote_name]




* Push 하기


-

git push remote_name branch_name 으로 한다.

$ git push [remote_name] [branch_name]



-

$ git push origin serverfix 


위 명령을 수행하면 git 은 serverfix 라는 브랜치 이름을 refs/heads/serverfix:refs/heads/serverfix 로 확장한다.

이것은 serverfix 라는 로컬 브랜치를 서버로 push 하는데 리모트의 serverfix 브랜치로 업데이트한다는 것을 의미한다.

(refs/heads 의 의미는 나중에 알아본다.)


아래 명령어는 위 명령어와 동일하다.

$ git push origin serverfix:serverfix 


로컬 브랜치의 이름과 리모트 서버의 브랜치 이름이 다를 때는 이 문법이 필요하다.

ex) 

$ git push origin serverfix:awesomebranch



-

https url 로 시작하는 리모트 저장소를 사용하면 push 나 pull 할 때 인증을 위한 사용자 이름이나 비밀번호를 묻는다.

이 리모트에 접근할 때마다 매번 id 나 비밀번호를 입력하지 않도록 credential cache 기능을 이용할 수 있다.

이 기능을 활성화하면 git 은 몇 분 동안 입력한 id 나 비밀번호를 저장해준다.

이 기능을 활성화하려면 git config —global credential.helper cache 명령을 실행하여 환경설정을 추가한다.

$ git config --global credential.helper cache



-

리모트 트래킹 브랜치에서 시작하는 새 브랜치를 만들려면 아래와 같은 명령을 사용한다.


$ git checkout -b [local_branch_name] [remote_name]/[remote_branch_name]




* 브랜치 추적


-

리모트 트래킹 브랜치를 로컬 브랜치로 checkout 하면 자동으로 트래킹 브랜치가 만들어진다.

흔히 upstream 브랜치라고 부른다.

트래킹 브랜치는 리모트 브랜치와 직접적인 연결고리가 있는 로컬 브랜치이다.

트래킹 브랜치에서 git pull 명령을 내리면 리모트 저장소로부터 데이터를 내려받아 연결된 리모트 브랜치와 자동으로 merge 된다.



-

서버로부터 저장소를 clone 하면 git 은 자동으로 master 브랜치를 origin/master 브랜치의 트래킹 브랜치로 만든다.



-

—track 옵션을 사용하여 로컬 브랜치 이름을 자동으로 생성할 수 있다.


ex) 

$ git checkout —track [remote_name]/[remote_branch_name]


리모트 브랜치와 다른 이름으로 브랜치를 만들려면 로컬 브랜치의 이름을 다르게 지정한다.


ex) 

$ git checkout -b [local_branch_name] [remote_name]/[remote_branch_name]



-

이미 로컬에 있는 브랜치가 리모트의 특정 브랜치를 추적하게 하려면 git branch 명령에 -u 나 --set-upstream-to 옵션을 붙인다.


ex) 

$ git branch -u origin/serverfix



-

추적 브랜치를 설정했다면 추적 브랜치 이름을 @{upstream} 이나 @{u} 로 짧게 대체하여 사용할 수 있다.

master 브랜치가 origin/master 브랜치를 추적하는 경우라면 git merge origin/master 명령과 git merge @{u} 명령을 똑같이 사용할 수 있다.

$ git merge @{u}



-

추적 브랜치가 현재 어떻게 설정되어 있는지 확인하려면 git branch 명령에 -vv 옵션을 더한다.

로컬 브랜치 목록과 로컬 브랜치가 추적하고 있는 리모트 브랜치도 함께 보여준다.

게다가 로컬 브랜치가 앞서가는지 뒤처지는지에 대한 내용도 보여준다.

$ git branch -vv


ahead 는 로컬 브랜치가 앞서 있다는 것이다.

behind 는 로컬 브랜치가 뒤쳐져 있다는 뜻이다.


중요한 점은 명령을 실행했을 때 나타나는 결과는 모두 마지막으로 서버에서 데이터를 가져온(fetch) 시점을 바탕으로 계산한다는 점이다.

따라서 git fetch —all; git branch -vv 를 사용하는 것이 적당하다.




* Pull 하기


-

git pull 명령은 대부분 git fetch 명령을 실행하고 나서 자동으로 git merge 명령을 수행하는 것 뿐이다.

일반적으로 fetch 와 merge 명령을 명시적으로 사용하는 것이 pull 명령으로 한번에 두 작업을 하는 것보다 낫다.




* 리모트 브랜치 삭제


-

git push 명령에 —delete 옵션을 사용하면 된다.


ex) 

$ git push [remote_name] —delete [remote_branch_name]


위 명령을 실행하면 서버에서 브랜치(즉 커밋을 가리키는 포인터) 하나가 사라진다.

서버에서 가비지 컬렉터가 동작하지 않는 한 데이터는 사라지지 않기 때문에 종종 의도치 않게 삭제한 경우에도 커밋한 데이터를 살릴 수 있다.







3.6. Rebase 하기


-

git 에서 한 브랜치에서 다른 브랜치로 합치는 방법은 두가지다.

하나는 merge 이고 다른 하나는 rebase 이다.




* Rebase 의 기초


-

3-way merge 를 하는 경우에는 공통 조상을 사용하여 새로운 커밋을 만들어 낸다.

비슷한 결과를 만드는 방식으로, 변경 사항을 patch 로 만들고 이를 다른 branch 에 적용하는 방법이 있다.

git 에서는 이런 방식을 rebase 라고 한다.



-

reabase 명령으로 한 브랜치에서 변경된 사항을 다른 브랜치에 적용할 수 있다.


ex) 

$ git rebase [destination_branch_name] 


두 브랜치가 나뉘기 전인 공통 커밋으로 이동하고 나서, 그 커밋부터 지금 checkout 한 브랜치가 가리키는 커밋까지 diff 를 차례로 만들어 어딘가에 임시로 저장해 놓는다.

rebase 할(source) 브랜치가 합칠(destination) 브랜치가 가리키는 커밋을 가리키게 하고 아까 저장해 놓았던 변경사항을 차례대로 적용한다.


그 다음 rebase 되는(destination) branch 를 fast_forward 를 한다. 

(위의 예에서는 현재 branch 의 diff 들을 만들고, 현재 branch 가 destination_branch 를 바라보게 한 후, diff 를 적용시킨다. 그 다음 destination_branch 를 diff 가 적용된 최신 pointer 위치로 fast-forward 한다)



-

merge 이든 rebase 이든 둘 다 합치는 관점에서는 서로 다를 게 없다.

하지만 rebase 가 좀 더 깨끗한 히스토리를 만든다.

rebase 한 브랜치의 log 를 살펴보면 히스토리가 선형이다.

일을 병렬로 동시에 진행해도 rebase 하고 나면 모든 작업이 차례대로 수행된 것처럼 보인다.


rebase 는 보통 리모트 브랜치에 커밋을 깔끔하게 적용하고 싶을 때 사용한다.

메인 프로젝트에 patch 를 보낼 준비가 되면 하는 것이 rebase 니까 브랜치에서 하던 일을 완전히 마치고 origin/master 로 rebase 한다.

이렇게 rebase 하고 나면 프로젝트 관리자는 어떠한 통합작업도 필요 없다.

그냥 master 브랜치를 fast-forward 하면 된다.



-

rebase 를 하든지, merge 를 하든지 최종 결과물은 같고 커밋 히스토리만 다르다는 것이 중요하다.

rebase 의 경우는 브랜치의 변경사항을 순서대로 다른 브랜치에 적용하면서 합치고 merge 의 경우는 두 브랜치의 최종결과만 가지고 합친다.




* Rebase 활용


-

Rebase 는 브랜치 합치는 것이 아닌 다른 용도로도 사용할 수 있다.


$ git rebase —onto master server client

이 명령은 client 브랜치를 checkout 하고 server 와 client 의 공통 조상 이후의 patch 를 만들어 master 에 적용하라는 내용이다.



-

checkout 하지 않고 바로 topic_branch 를 base_branch 로 rebase 할 수 있다.

$ git rebase [dest_branch] [src_branch]




* Rebase 의 위험성


-

"이미 공개 저장소에 push 한 커밋을 rebase 하지 말라.”

이 지침만 지키면 rebase 하는 데 문제 될 게 없다.

애초에 서버로 데이터를 보내기 전에 커밋을 정리해야 한다.



-

서버의 히스토리를 새로 덮어씌우려면 git push —force 명령을 사용해야 한다.

$ git push --force




* Rebase 한 것을 다시 Rebase 하기


-

어떤 팀원이 강제로 내가 한 일을 덮어썼다고 하자.

그러면 내가 했던 일이 무엇이고 덮어쓴 내용이 무엇인지 알아내야 한다.

커밋 SHA 체크섬 외에도 Git 은 커밋에 Patch 할 내용으로 SHA 체크섬을 한번 더 구한다.

이 값을 patch-id 라고 한다.


덮어쓴 커밋을 받아서 그 커밋을 기준으로 rebase 할 때 git 은 원래 누가 작성한 코드인지 잘 찾아낸다.

그래서 patch 가 원래대로 잘 적용된다.



-

git pull —rebase 로 rebase 할 수 있다.

$ git pull --rebase

git fetch 와 git rebase [target_branch] 를 순서대로 실행한 것과 같다.



-

git pull 명령을 실행할 때 기본적으로 --rebase 옵션이 적용되도록 하려면 아래와 같이 하면 된다.

$ git config —global pull.rebase true




* Rebase vs. Merge


-

히스토리를 보는 관점 중에 하나는 작업한 내용을 기록으로 보는 것이다.

작업 내용을 기록한 문서이고, 각 기록은 각각 의미를 가지며, 변경할 수 없다.

이런 관점에서 커밋 히스토리를 변경한다는 것은 역사를 부정하는 꼴이 된다.

언제 무슨 일이 있었는지 기록에 대해 거짓말을 하게 되는 것이다.


히스토리를 프로젝트가 어떻게 진행되었냐에 대한 이야기로도 볼 수 있다.

나중에 다른 사람에게 들려주기 좋도록 rebase 나 filter-branch 같은 도구로 프로젝트의 진행 이야기를 다듬으면 좋다.


결과적으로 둘 중 어떤 녀석을 어떻게 쓸지는 각자의 상황과 각자의 판단에 달렸다.



-

일반적인 해답 중 하나는 로컬 브랜치에서 작어할 때는 히스토리를 정리하기 위해 rebase 할 수 있지만, 리모트 등 어딘가 밖으로 push 로 내보낸 커밋에 대해서는 절대 rebase 하지 말아야 한다.





3.7. 요약




반응형

댓글