티스토리 뷰

728x90
반응형

Git Branch

모든 버전 관리 시스템은 브랜치(Branch)를 지원한다. 개발을 하다 보면 코드를 여러 개로 복사해야 하는 일이 자주 생긴다. 코드를 통째로 복사하고 나서 원래 코드와는 상관없이 독립적으로 개발을 진행할 수 있는데, 이렇게 독립적으로 개발하는 것이 브랜치다.

Git의 브랜치 모델은 다른 VCS들과 다르며, 최고의 장점으로 손꼽힌다. Git의 브랜치는 매우 가볍다. 순식간에 브랜치를 새로 만들고 브랜치 사이를 이동할 수 있다. 때문에 이전의 코드를 그대로 가져와 독립적인 개발을 진행할 수 있다는 브랜치의 생성 목적에 맞게 활용할 수 있으면서도 리스크를 줄일 수 있었다.

브랜치(Branch)란 무엇인가

Git이 브랜치를 다루는 방법을 알아보기 위해 예시를 들어 보겠다.

➜  branch-ex git:(master) vim file1.txt
➜  branch-ex git:(master) ✗ git add file1.txt 
➜  branch-ex git:(master) ✗ git commit -m "commit 1"
[master (최상위-커밋) 7633576] commit 1
 1 file changed, 1 insertion(+)
 create mode 100644 file1.txt
➜  branch-ex git:(master) vim file2.txt
➜  branch-ex git:(master) ✗ git add file2.txt 
➜  branch-ex git:(master) ✗ git commit -m "commit 2"
[master 4f8bbe5] commit 2
 1 file changed, 1 insertion(+)
 create mode 100644 file2.txt
➜  branch-ex git:(master) mkdir directory1
➜  branch-ex git:(master) vim directory1/file3.txt
➜  branch-ex git:(master) ✗ git add directory1/file3.txt            
➜  branch-ex git:(master) ✗ git commit -m "commit 3"
[master cb76639] commit 3
 1 file changed, 1 insertion(+)
 create mode 100644 directory1/file3.txt

위 예시 코드는 총 3개의 커밋을 생성하는 작업이다. 그 결과 로그를 보면 아래와 같다.

* cb76639 (HEAD -> master) commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

Git은 커밋을 객체로 관리한다는 것을 이전 포스팅에서 배웠다. 그리고 이 커밋 객체를 나타내는 링크 주소인 sha-1 체크섬 값을 가진다는 것도 알고 있다. 때문에 각 커밋 객체는 이전 커밋을 나타내는 체크섬 값을 저장할 수 있고, 그로 인해 우리는 언제든지 원하는 커밋으로 되돌아갈 수 있다. 40개의 16진수로 이루어진 체크섬 값을 알고 있다면 말이다. 하지만 당연히 그건 쉽지 않다.

개발을 할 때도 직접 메모리 위치로 데이터를 접근하는 것이 아니라 변수를 생성한다. 브라우저에 IP 주소 값을 직접 입력하지 않고 도메인 주소를 입력한다. 마찬가지로 우리에게 좀 더 친숙한 언어로 커밋을 가리키고 싶을 것이다. 그게 바로 브랜치의 역할이다.

브랜치(Branch) : 커밋 사이를 가볍게 이동할 수 있는 포인터 역할로 별칭을 가질 수 있음

실제로 Git의 브랜치는 어떤 한 커밋을 가리키는 40글자의 SHA-1 체크섬 파일에 불과하기 때문에 만들기도 쉽고 지우기도 쉽다. 새로 브랜치를 하나 만드는 것은 41바이트 크기의 파일을(40자와 줄 바꿈 문자) 하나 만드는 것에 불과하다.

git init 명령으로 초기화할 때 자동으로 master 브랜치가 생성된다. 처음 커밋하면 이 master 브랜치가 생성된 커밋을 가리킨다. 이후 커밋을 새로 생성할 때마다 master 브랜치는 자동으로 가장 마지막 커밋을 가리킨다.

커밋 히스토리와 브랜치 - 1

따라서 우리는 master라는 이름의 브랜치로 최신의 커밋인 cb76639를 접근할 수 있고, 해당 커밋 객체의 Index(최상위 디렉토리)를 가져와 워킹 디렉토리에서 마치 이전 코드를 전부 복사한 것처럼 그대로 이어서 작업할 수 있다.

HEAD Refs

만약 새로운 브랜치를 하나 더 생성하면 어떻게 될까? 당연히 그 브랜치도 최신의 커밋을 가리키게 된다.

➜  branch-ex git:(master) git branch testing
➜  branch-ex git:(master) git log --oneline --decorate --graph --all

* cb76639 (HEAD -> master, testing) commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

testing 이란 이름의 브랜치를 새로 생성했다. 그리고 로그를 보면 cb76639 커밋을 2개의 브랜치가 모두 가리키고 있는 것을 확인할 수 있다. 지금 작업 중인 브랜치가 무엇인지 Git은 어떻게 파악할까. 눈치 챘겠지만, masterHEAD 라는 녀석이 화살표로 가리키고 있다. 다른 버전 관리 시스템과는 달리 Git은 HEAD라는 특수한 포인터가 있다. 이 포인터는 지금 작업하는 로컬 브랜치를 가리킨다.

HEAD : 현재 작업 중인 브랜치나 커밋을 가리키고 있는 포인터(Refs)

 

커밋 히스토리와 브랜치 - 2

브랜치 이동하기 : checkout vs switch

위 상황에서 testing 브랜치로 이동하고 싶으면 어떻게 해야 할까? 이전에는 checkout 명령어만을 사용해야 했지만, 이제는 2가지 방법이 있다.

checkout

checkout은 브랜치를 이동할 때 주로 사용하지만, 사실 특정 커밋으로 워킹 디렉터리를 변경할 수 있는 다목적 명령어다. 예를 들어 단순히 브랜치를 이동하고 싶으면 아래와 같이 사용하면 된다.

➜  branch-ex git:(master) git checkout testing 
'testing' 브랜치로 전환합니다
➜  branch-ex git:(testing) git log --oneline --decorate --graph --all

* cb76639 (HEAD -> testing, master) commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

HEADtesting 브랜치를 가리키게 변경됐다. 이런 단순한 기능도 있지만, 특정한 커밋에 있는 파일을 가져와 복원하거나 커밋으로 이동할 수도 있다. 이때 이동하고 싶은 커밋의 체크섬을 업력해 주면 된다.

➜  branch-ex git:(testing) git checkout 4f8bbe54cbc2bdd5386a17099ef0116b06aaa76d
Note: switching to '4f8bbe54cbc2bdd5386a17099ef0116b06aaa76d'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD의 현재 위치는 4f8bbe5 commit 2
➜  branch-ex git:(4f8bbe5) ls -al
total 16
drwxr-xr-x   5 on1ystar  staff  160 10 30 19:18 .
drwxr-xr-x   9 on1ystar  staff  288 10 30 17:58 ..
drwxr-xr-x  12 on1ystar  staff  384 10 30 19:18 .git
-rw-r--r--   1 on1ystar  staff   15 10 30 17:59 file1.txt
-rw-r--r--   1 on1ystar  staff   15 10 30 18:01 file2.txt
➜  branch-ex git:(4f8bbe5) git log --oneline --decorate --graph --all           

* cb76639 (testing, master) commit 3
* 4f8bbe5 (HEAD) commit 2
* 7633576 commit 1

위 예시의 경우, 2번째 커밋으로 이동했다. 때문에 워킹 디렉토리를 확인해 보면, directory1/test3.txt 는 존재하지 않는다. 로그를 확인해 보면 재밌는 점을 발견할 수 있는데, HEAD 가 브랜치를 가리키는 것이 아니라 단독으로 2번째 커밋(4f8bbe5)을 가리키고 있다. 이 상태를 detached HEAD라고 한다.

💡 detached HEAD 상태란 HEAD가 특정 브랜치를 가리키는 것이 아니라, 특정 커밋이나 태그를 직접 가리키고 있는 상태를 말한다. 이 상태에서 작업 후 커밋을 하면 그 커밋은 아무 브랜치에도 연결되지 않은 고립된 상태가 된다. 그래서 보통 과거의 커밋을 일시적으로 점검하거나, 임시 테스트 같은 상황에서 실험적인 코드를 작성할 때 사용된다.

이처럼 checkout은 너무 다양한 기능을 가지고 있기 때문에 잘못 사용될 우려도 크다. 그래서 Git은 단순한 브랜치 이동을 위한 switch 명령어를 추가했다.

switch

switch브랜치 전환에만 집중한 명령어로, checkout의 브랜치 이동 기능을 대체하기 위해 도입됐다.

➜  branch-ex git:(testing) git switch master 
'master' 브랜치로 전환합니다
➜  branch-ex git:(master) git switch testing
'testing' 브랜치로 전환합니다

Git Merge

브랜치 분기

브랜치를 변경한 상태에서 새로운 커밋을 추가하게 되면 각각의 브랜치는 서로 다른 커밋을 가리키게 된다. 약간의 시나리오를 가정해 보겠다.

  • master : 현재 서비스 중인 브랜치
  • feature2 : 새로운 기능을 위한 브랜치

현재 서비스는 master 브랜치의 최신 커밋 상태에서 서비스 중이다. 때문에 새로운 기능을 위해 master 브랜치에 새로운 커밋을 추가하는게 아닌 feature2 브랜치에 추가 개발을 진행함으로써, 실 서비스 환경과 간단하게 분리시킬 수 있다(물론 상당히 간단한 시나리오다).

➜  branch-ex git:(master) git switch feature2
'feature2' 브랜치로 전환합니다
➜  branch-ex git:(feature2) vim test4.txt 
➜  branch-ex git:(feature2) ✗ git add --all
➜  branch-ex git:(feature2) ✗ git commit -m "commit 4"
[feature2 bc531ae] commit 4
 1 file changed, 1 insertion(+)
 create mode 100644 file4.txt
➜  branch-ex git:(feature2) git log --oneline --decorate --graph --all 

* bc531ae (HEAD -> feature2) commit 4
* cb76639 (master) commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

브랜치 분기 시나리오 - 1

feature2 브랜치는 최신 커밋인 bc531ae 를 가리키게 되고, master 브랜치는 여전히 이전 3번째 커밋을 가리키고 있다.

위 상황에서 서비스에 문제가 생겨 급히 수정할 사항이 생겼다고 가정해 보자. 실 서비스 환경이 필요한데, 간단하게 master 브랜치로 돌아가면 된다. 이전에 작업했던 브랜치로 이동하면 워킹 디렉토리의 파일은 그 브랜치에서 가장 마지막으로 했던 작업 내용으로 변경된다. 커밋이 그 때 당시의 스냅샷을 가지고 있기 때문이다. 그 다음 버그를 해결할 때까지 사용할 hotfix 브랜치가 새로 필요하다.

💡 아직 커밋하지 않은 파일이 Checkout 할 브랜치와 충돌 나면 브랜치를 변경할 수 없다. 브랜치를 변경할 때는 워킹 디렉토리를 정리하는 것이 좋다.
➜  branch-ex git:(testing) git switch master 
'master' 브랜치로 전환합니다
➜  branch-ex git:(master) git switch -c hotfix
새로 만든 'hotfix' 브랜치로 전환합니다
➜  branch-ex git:(hotfix) vim file1.txt
➜  branch-ex git:(hotfix) ✗ git add file1.txt 
➜  branch-ex git:(hotfix) ✗ git commit -m "commit 5 for hotfix"
[hotfix 6de8f2a] commit 5 for hotfix
 1 file changed, 1 insertion(+)
➜  branch-ex git:(hotfix) git log --oneline --decorate --graph --all

* 6de8f2a (HEAD -> hotfix) commit 5 for hotfix
| * bc531ae (feature2) commit 4
|/  
* cb76639 (master) commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

master 브랜치에서 hotfix 브랜치를 새로 생성한 뒤, 브랜치를 전환한다. 그 다음, file1.txt를 수정 후 커밋한다.

브랜치 분기 시나리오 - 2

이걸로 급한 버그가 수정됐다. 이제 hotfix된 버전으로 서비스를 배포하기 위해 master 브랜치를 hotfix 브랜치와 병합해야 한다.

브랜치 병합 : Merge

Fast-forward

브랜치 병합에는 merge 명령어가 사용되는데, 병합에 기준이 되는 브랜치로 전환한 후 사용해야 한다. 위 상황같은 경우 master 브랜치로 전환한 후 hotfix 브랜치를 병합시킨다.

➜  branch-ex git:(hotfix) git checkout master 
'master' 브랜치로 전환합니다
➜  branch-ex git:(master) git merge hotfix
업데이트 중 cb76639..6de8f2a
Fast-forward
 file1.txt | 1 +
 1 file changed, 1 insertion(+)

➜  branch-ex git:(master) git log --oneline --decorate --graph --all

* 6de8f2a (HEAD -> master, hotfix) commit 5 for hotfix
| * bc531ae (feature2) commit 4
|/  
* cb76639 commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

Merge 메시지에서 “Fast-forward” 라고 표시해 준다. hotfix 브랜치가 가리키는 6de8f2a 커밋이 cb76639 커밋에 기반한 브랜치이기 때문에 사실 브랜치 포인터는 Merge 과정 없이 그저 최신 커밋으로 이동한다. 이런 Merge 방식을 Fast forward 라고 부른다.

Fast-forward : A 브랜치에서 다른 B 브랜치를 Merge 할 때, B 브랜치가 A 브랜치 이후의 커밋을 가리키고 있으면, 그저 A 브랜치가 B 브랜치와 동일한 커밋을 가리키도록 이동

그림으로 확인해 보면, 단순히 master 브랜치 포인터가 가리키는 커밋이 바뀌었을 뿐이다.

mater 브랜치에서 hotfix로 Fast-forward

이후 필요 없어진 hotfix 브랜치는 삭제해 주면 된다.

➜  branch-ex git:(master) git branch -d hotfix
hotfix 브랜치 삭제 (과거 6de8f2a).

3-way Merge

이제 급한 버그가 수정됐으니, 새로운 기능 개발을 마저 하기 위해 feature2 브랜치로 돌아가면 된다.

➜  branch-ex git:(feature2) vim file4.txt
➜  branch-ex git:(feature2) ✗ git add --all
➜  branch-ex git:(feature2) ✗ git commit -m "feature 2 complete"
[feature2 b623610] feature 2 complete
 1 file changed, 1 insertion(+)

➜  branch-ex git:(feature2) git log --oneline --decorate --graph --all

* b623610 (HEAD -> feature2) feature 2 complete
* bc531ae commit 4
| * 6de8f2a (master) commit 5 for hotfix
|/  
* cb76639 commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

이후 기능이 다 완성되면, 이제 feature2master 브랜치에 병합해야 할 것이다. 이 때는 이전의 Fast-forward 상황과는 다르다. master 브랜치가 가리키는 커밋이 feature2 브랜치가 가리키고 있는 커밋의 이전 커밋이 아니기 때문이다.

feature2를 master로 merge 해야하는 상황

위와 같은 경우에는 각 브랜치가 가리키는 커밋 두 개와 공통 조상 하나를 사용하여 3-way Merge를 한다.

3-way Merge를 위한 3개의 커밋

즉, 각 브랜치가 가리키고 있는 6de8f2a, b62361O 커밋과 두 브랜치의 공통 조상인 cb76639 커밋이 해당된다. 병합을 한 뒤, 결과를 확인해 보자.

➜  branch-ex git:(feature2) git switch master 
'master' 브랜치로 전환합니다
➜  branch-ex git:(master) git merge feature2 
Merge made by the 'recursive' strategy.
 file4.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 file4.txt

➜  branch-ex git:(master) git log --oneline --decorate --graph --all

*   f7cb86a (HEAD -> master) Merge branch 'feature2'
|\  
| * b623610 (feature2) feature 2 complete
| * bc531ae commit 4
* | 6de8f2a commit 5 for hotfix
|/  
* cb76639 commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

단순히 브랜치 포인터를 최신 커밋으로 옮기는 게 아니라, 3-way Merge 의 결과를 별도의 커밋으로 만들고 나서 해당 브랜치가 그 커밋을 가리키도록 이동시킨다. 그래서 이런 커밋은 부모가 여러 개고 Merge 커밋이라고 부른다.

3-way Merge 결과

이런 식으로 3개의 커밋을 참조하는 이유는 두 브랜치가 같은 조상 커밋에서 어떻게 발전했는지 비교하기 위함이다. 어떤 부분이 다른지 알아야 병합할 때 빠진 부분 없이 반영할 수 있고, 특히나 같이 작성한 파일은 병합 과정에서 충돌이 발생할 수 있다. 위 예시에서는 feature2 브랜치에서 새로 생성한 파일에서만 작업했기 때문에 충돌 없이 바로 병합됐다. 하지만 만약 master 브랜치에서 작업하고 있던 파일을 feature2 브랜치에서도 작업했다면, 충돌 확률이 매우 높을 것이다.

충돌(Conflict)

가끔씩 3-way Merge가 실패할 때도 있다. Merge 하는 두 브랜치에서 같은 파일의 한 부분을 동시에 수정하고 Merge 하면 Git은 해당 부분을 Merge 하지 못한다. feature2 브랜치에서 hotfix 당시 수정헀던 file1.txt 파일을 다른 방식으로 수정한 뒤, 다시 병합을 시도해 충돌하는 상황을 만들어 보자.

➜  branch-ex git:(master) git merge feature2 
자동 병합: file1.txt
충돌 (내용): file1.txt에 병합 충돌
자동 병합이 실패했습니다. 충돌을 바로잡고 결과물을 커밋하십시오.

메세지를 보면 file1.txt 에 병합 충돌이 일어나 자동 병합에 실패했다고 표시된다. Git은 자동으로 Merge 하지 못해서 새 커밋이 생기지 않는다. 변경사항의 충돌을 개발자가 해결하지 않는 한 Merge 과정을 진행할 수 없다. Merge 충돌이 일어났을 때 Git이 어떤 파일을 Merge 할 수 없었는지 살펴보려면 git status 명령을 이용하면 된다.

➜  branch-ex git:(master) ✗ git status
현재 브랜치 master
병합하지 않은 경로가 있습니다.
  (충돌을 바로잡고 "git commit"을 실행하십시오)
  (병합을 중단하려면 "git merge --abort"를 사용하십시오)

병합하지 않은 경로:
  (해결했다고 표시하려면 "git add <파일>..."을 사용하십시오)
    양쪽에서 수정:  file1.txt

커밋할 변경 사항을 추가하지 않았습니다 ("git add" 및/또는 "git commit -a"를
사용하십시오

충돌이 일어난 파일은 unmerged 상태로 표시된다. Git은 충돌이 난 부분을 표준 형식에 따라 표시해준다. 그러면 개발자는 해당 부분을 수동으로 해결한다. 충돌 난 부분은 아래와 같이 표시된다.

➜  branch-ex git:(master) ✗ vim file1.txt

This is file 1
<<<<<<< HEAD
update for hotfix
=======
hihihihi~ update for conflict test in feature2 branch~~~~
>>>>>>> feature2

======= 위쪽의 내용은 HEAD 버전(merge 명령을 실행할 때 작업하던 master 브랜치)의 내용이고 아래쪽은 feature2 브랜치의 내용이다. 충돌을 해결하려면 위쪽이나 아래쪽 내용 중에서 고르거나 새로 작성하여 Merge 한다. 즉, 개발자가 해당 파일을 열어보고 직접 수정해 줘야 한다는 것이다(간단한 것들은 Merge 도구를 사용해 해결할 수도 있다).

파일을 수정한 뒤 git add 후 커밋하면 중단됐던 병합이 마무리된다.

➜  branch-ex git:(master) ✗ git add file1.txt 
➜  branch-ex git:(master) git status      
➜  branch-ex git:(master) git commit
[master 43ef359] Merge branch 'feature2'

이때 아래와 같이 커밋 메세지를 작성해 주는데, 어떻게 충돌을 해결했고 좀 더 확인해야 하는 부분은 무엇이고 왜 그렇게 해결했는지에 대해서 자세하게 기록하면, 나중에 이 Merge 커밋을 이해하는데 도움을 준다.

Merge branch 'feature2'

Conflicts:
      file1.txt
#
# It looks like you may be committing a merge.
# If this is not correct, please run
#       git update-ref -d MERGE_HEAD
# and try again.

# 변경 사항에 대한 커밋 메시지를 입력하십시오. '#' 문자로 시작하는
# 줄은 무시되고, 메시지를 입력하지 않으면 커밋이 중지됩니다.
#
# 현재 브랜치 master
# 모든 충돌을 바로잡았지만 아직 병합하는 중입니다.
#

사실 위 예제는 충돌 상황 중에서도 지극히 간단한 예제다. 일반적으로 충돌이 발생하게 되면 더 복잡하고 귀찮아 진다. 특히나 오랫동안 병합하지 않은 두 브랜치를 병합할 때는 더 위험하다. 때문에 자주 병합을 진행하면서 조금씩 충돌들을 해결해 나가야 하고, 프로젝트에 맞는 브랜치 워크플로우를 선택해 활용하는 것도 방법이다.

Git Remote Branch

리모트 Refs는 리모트 저장소에 있는 포인터인 레퍼런스다. 리모트 저장소에 있는 브랜치, 태그, 등등을 의미한다. git ls-remote [remote] 명령으로 모든 리모트 Refs를 조회할 수 있다. git remote show [remote] 명령은 모든 리모트 브랜치와 그 정보를 보여준다. 리모트 Refs가 있지만 보통은 리모트 트래킹 브랜치를 사용한다.

리모트 트래킹 브랜치

리모트 트래킹 브랜치는 리모트 브랜치를 추적하는 레퍼런스이며 브랜치다. 리모트 트래킹 브랜치는 로컬에 있지만 임의로 움직일 수 없다. 리모트 서버에 연결할 때마다 리모트의 브랜치 업데이트 내용에 따라서 자동으로 갱신될 뿐이다. 리모트 트래킹 브랜치는 일종의 북마크라고 할 수 있다. 리모트 저장소에 마지막으로 연결했던 순간에 브랜치가 무슨 커밋을 가리키고 있었는지를 나타낸다.

리모트 트래킹 브랜치(remote tracking branch) : 리모트 브랜치를 추적하고 있는 로컬 브랜치

리모트 트래킹 브랜치의 이름은 <remote>/<branch> 형식으로 되어 있다. 예를 들어 리모트 저장소 originmaster 브랜치를 보고 싶다면 origin/master 라는 이름으로 브랜치를 확인하면 된다.

💡 origin이란 이름은 git clone 명령이 자동으로 만들어주는 리모트 이름이다. git clone -o [원하는 리모트 이름] 라고 옵션을 주고 명령을 실행하면 사용자가 정한 대로 리모트 이름을 생성해 준다.

예제를 살펴보자. git.ourcompany.com 이라는 Git 서버가 있고 이 서버의 저장소를 하나 Clone 하면 Git은 자동으로 origin 이라는 이름을 붙인다. origin 으로부터 저장소 데이터를 모두 내려받고 master 브랜치를 가리키는 포인터를 만든다. 이 포인터는 origin/master 라고 부르고 멋대로 조종할 수 없다. 그리고 Git은 로컬의 master 브랜치가 origin/master 를 가리키게 한다. 이제 이 master 브랜치에서 작업을 시작할 수 있다.

Clone 이후 서버와 로컬의 master 브랜치

로컬 저장소에서 어떤 작업을 하고 있는데 동시에 다른 팀원이 git.ourcompany.com 서버에 Push 하고 master 브랜치를 업데이트한다. 그러면 이제 팀원 간의 히스토리는 서로 달라진다. 서버 저장소로부터 어떤 데이터도 주고받지 않아서 origin/master 포인터는 그대로다.

리모트 저장소와 로컬 저장소 간의 커밋 히스토리가 달라짐

이런 경우에는 작업을 중단하고 적당한 시기에 동기화를 시켜줘야 한다.

리모트 저장소를 동기화 시키기 : Fetch

git fetch는 Git에서 원격 저장소의 변경 사항을 로컬로 가져오는 명령어다. 이 명령어는 현재 작업 중인 로컬 브랜치에 영향을 주지 않고, 원격 저장소에서 변경된 커밋, 브랜치, 태그 등의 메타데이터를 업데이트하는 데 사용된다. 위 예제에서는 git fetch origin 명령을 사용한다.

리모트 브랜치 정보 업데이트

명령이 수행되면 리모트 트래킹 브랜치인 origin/master 가 리모트 저장소의 master 브랜치와 같이 최신 커밋을 가리키게 된다.

로컬 브랜치를 서버로 전송하기 : Push

로컬의 브랜치를 서버로 전송하려면 쓰기 권한이 있는 리모트 저장소에 Push 해야 한다. 로컬 저장소의 브랜치는 자동으로 리모트 저장소로 전송되지 않는다. 명시적으로 브랜치를 Push 해야 정보가 전송된다.

아래와 같이 git push <remote> <branch> 명령을 사용한다.

➜  branch-ex git:(master) git push origin master                     
오브젝트 나열하는 중: 25, 완료.
오브젝트 개수 세는 중: 100% (25/25), 완료.
Delta compression using up to 8 threads
오브젝트 압축하는 중: 100% (17/17), 완료.
오브젝트 쓰는 중: 100% (25/25), 2.05 KiB | 2.05 MiB/s, 완료.
Total 25 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), done.
To https://github.com/on1ystar/git-study.git
 * [new branch]      master -> master

git push origin master:master 라고 Push 하는 것도 같은 의미인데 이것은 “로컬의 master 브랜치를 리모트 저장소의 master 브랜치로 Push 하라” 라는 뜻이다. 로컬 브랜치의 이름과 리모트 서버의 브랜치 이름이 다를 때 필요하다. 리모트 저장소에 master 라는 이름 대신 다른 이름을 사용하려면 git push origin master:my-master-branch 처럼 사용한다.

이 상황에서 누군가가 serverfix 브랜치를 만들어 작업을 한 뒤, 서버에 Push 했다고 해보자. 그럼 우리는 리모트 저장소를 Fetch 한 뒤, origin/serverfix 라는 이름으로 접근할 수 있다.

➜  branch-ex git:(master) git branch -a

  feature2
* master
  remotes/origin/master
  remotes/origin/serverfix

이제 새로 받은 브랜치를 병합해서 사용하거나, 새로운 브랜치를 만들어서 이어가도 된다. 단, origin/serverfix 라는 브랜치 포인터는 수정할 수 없다는 것만 기억하면 된다.

트래킹 브랜치

리모트 트래킹 브랜치를 로컬 브랜치로 Checkout 하면 자동으로 트래킹(Tracking) 브랜치가 만들어진다. 이때 트래킹 하는 대상 브랜치를 Upstream 브랜치 라고 부른다.

  • 트래킹 브랜치 : 리모트 브랜치와 직접적인 연결고리가 있는 로컬 브랜치
  • Upstream 브랜치 : 트래킹 브랜치가 트래킹하는 대상 브랜치(리모트 브랜치)

서버로부터 저장소를 Clone 하면 Git은 자동으로 master 브랜치를 origin/master 브랜치의 트래킹 브랜치로 만든다. 예를 들어 새로운 디렉토리에서 git clone 명령을 수행하면 다음과 같아진다.

➜  remote-ex git:(master) git clone https://github.com/on1ystar/git-study.git
'git-study'에 복제합니다...
remote: Enumerating objects: 28, done.
remote: Counting objects: 100% (28/28), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 28 (delta 7), reused 28 (delta 7), pack-reused 0 (from 0)
오브젝트를 받는 중: 100% (28/28), 완료.
델타를 알아내는 중: 100% (7/7), 완료.

➜  git-study git:(master) git log --oneline --decorate --graph --all         

* 27ace59 (origin/serverfix) server fix
*   43ef359 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'feature2'
|\  
| * 91c9c82 update for conflict test
* | f7cb86a Merge branch 'feature2'
|\| 
| * b623610 feature 2 complete
| * bc531ae commit 4
* | 6de8f2a commit 5 for hotfix
|/  
* cb76639 commit 3
* 4f8bbe5 commit 2
* 7633576 commit 1

master 브랜치가 origin/master 브랜치의 최신 커밋을 가리키고 있는 걸 확인할 수 있다. 이게 리모트 저장소가 그랬으니까 똑같이 복제된거 아니냐고 생각할 수 있는데, 트래킹 브랜치를 직접 만들 수도 있다.

git checkout -b <branch> <remote>/<branch> 명령으로 간단히 트래킹 브랜치를 만들 수 있다. --track 옵션을 사용하여 로컬 브랜치 이름을 자동으로 생성할 수도 있다.

➜  git-study git:(serverfix) git checkout -b my-master origin/master
'my-master' 브랜치가 리모트의 'master' 브랜치를 ('origin'에서) 따라가도록 설정되었습니다.
새로 만든 'my-master' 브랜치로 전환합니다

➜  git-study git:(master) git checkout --track origin/serverfix
'serverfix' 브랜치가 리모트의 'serverfix' 브랜치를 ('origin'에서) 따라가도록 설정되었습니다.
새로 만든 'serverfix' 브랜치로 전환합니다

➜  git-study git:(my-master) git log --oneline --decorate --graph --all

* 27ace59 (origin/serverfix, serverfix) server fix
*   43ef359 (HEAD -> my-master, origin/master, origin/HEAD, master) Merge branch 'feature2'
|\  
| * 91c9c82 update for conflict test
* | f7cb86a Merge branch 'feature2'

...

그리고 이미 로컬에 존재하는 브랜치가 리모트의 특정 브랜치를 추적하게 하려면 git branch 명령에 -u--set-upstream-to 옵션을 붙여서 설정하면 된다.

➜  git-study git:(my-master) git branch sf
➜  git-study git:(my-master) git switch sf 
'sf' 브랜치로 전환합니다
➜  git-study git:(sf) git branch -u origin/serverfix
'sf' 브랜치가 리모트의 'serverfix' 브랜치를 ('origin'에서) 따라가도록 설정되었습니다.

추적 브랜치가 현재 어떻게 설정되어 있는지 확인하려면 git branch 명령에 -vv 옵션을 더한다. 이 명령을 실행하면 로컬 브랜치 목록과 로컬 브랜치가 추적하고 있는 리모트 브랜치도 함께 보여준다. 게다가, 로컬 브랜치가 앞서가는지 뒤쳐지는지에 대한 내용도 보여준다.

➜  git-study git:(sf) git branch -vv

  master    43ef359 [origin/master] Merge branch 'feature2'
  my-master 43ef359 [origin/master] Merge branch 'feature2'
  serverfix 27ace59 [origin/serverfix] server fix
* sf        43ef359 [origin/serverfix: 1개 뒤] Merge branch 'feature2'

위 결과를 보면, 우리가 생성한 트래킹 브랜치 4개가 있다. 리모트 브랜치마다 각각 2개씩 생성됐는데, sf 브랜치를 보면 origin/serverfix 브랜치보다 “1개 뒤”라고 표시된다. 이는 서버의 브랜치에서 아직 로컬 브랜치로 병합하지 않은 커밋이 1개 있다는 말이다.

로그를 확인해 보자.

* 27ace59 (origin/serverfix, serverfix) server fix
*   43ef359 (HEAD -> sf, origin/master, origin/HEAD, my-master, master) Merge branch 'feature2'
|\  
| * 91c9c82 update for conflict test

...

sf 브랜치를 만들 당시, master 브랜치로 Checkout 된 상태였기 때문에, 43ef359 커밋을 가리키고 있는 상태로 sf 브랜치가 만들어져 1개의 커밋이 뒤쳐지게 된 것이다.

💡 여기서 중요한 점은 명령을 실행했을 때 나타나는 결과는 모두 마지막으로 서버에서 데이터를 가져온(fetch) 시점을 바탕으로 계산한다는 점이다. 단순히 이 명령만으로는 서버의 최신 데이터를 반영하지는 않으며 로컬에 저장된 서버의 캐시 데이터를 사용한다. 따라서 아래처럼 두 명령을 이어서 사용하는 것이 좋다.

$ git fetch --all; git branch -vv

그럼 이렇게 트래킹 브랜치를 만드는 이유가 뭘까?

트래킹 브랜치를 사용하면 원격 브랜치의 최신 상태를 확인하고 필요한 경우 간편하게 변경 사항을 가져오거나(push), 병합(pull) 할 수 있다.

서버의 브랜치를 로컬 브랜치로 가져와 병합하기 : Pull

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

➜  git-study git:(sf) git pull
업데이트 중 43ef359..27ace59
Fast-forward
 file6.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 file6.txt

➜  git-study git:(sf) git log --oneline --decorate --graph --all

 * 27ace59 (HEAD -> sf, origin/serverfix, serverfix) server fix
*   43ef359 (origin/master, origin/HEAD, my-master, master) Merge branch 'feature2'
|\  
| * 91c9c82 update for conflict test

git pull 로그를 보면 Fast-forward 로 병합된 것을 확인할 수 있다. 커밋 로그에서도 sf 브랜치가 origin/serverfix 브랜치와 같은 커밋을 가리키고 있다.

➜  git-study git:(sf) git branch -vv                            

  master    43ef359 [origin/master] Merge branch 'feature2'
  my-master 43ef359 [origin/master] Merge branch 'feature2'
  serverfix 27ace59 [origin/serverfix] server fix
* sf        27ace59 [origin/serverfix] server fix

이제 모든 트래킹 브랜치가 리모트 브랜치와의 차이가 없어졌다.

💡 만약 트래킹 브랜치가 설정되어 있지 않았다면, git pull 이 아니라 git pull origin/serverfix sf 라고 해야 한다.

git fetch 명령을 실행하면 서버에는 존재하지만, 로컬에는 아직 없는 데이터를 받아와서 저장한다. 이 때 워킹 디렉토리의 파일 내용은 변경되지 않고 그대로 남는다. 서버로부터 데이터를 가져와서 저장해두고 사용자가 Merge 하도록 준비만 해둔다. 간단히 말하면 git pull 명령은 대부분 git fetch 명령을 실행하고 나서 자동으로 git merge 명령을 수행하는 것 뿐이다.

만약 작업하던 것이 있었는데 git pull 명령을 실행해 버리면, 자동 병합을 진행하기 때문에 작업하던 것들이 모두 손실될 수 있다. 따라서 일반적으로 fetchmerge 명령을 명시적으로 사용하는 것이 pull 명령으로 한번에 두 작업을 하는 것보다 낫다.

Reference

728x90
반응형
댓글