Pro git - 7.8

Updated:

Pro git - 고급 Merge

1. Merge 충돌

  • Merge 할 때는 충돌이 날 수 있어서 Merge 하기 전에 워킹 디렉토리를 깔끔히 정리하는 것이 좋다.
  • 워킹 디렉토리에 작업하던 게 있다면 임시 브랜치에 커밋하거나 Stash 해둔다.
  • 그래야 어떤 일이 일어나도 다시 되돌릴 수 있다.
  • 작업 중인 파일을 저장하지 않은 채로 Merge 하면 작업했던 일부를 잃을 수도 있다.
  • 매우 간단한 예제를 따라가 보자. 현재 ‘hello world’를 출력하는 Ruby 파일을 하나 가지고 있다.
#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()
  • 저장소에 whitespace 브랜치를 생성하고 모든 Unix 개행을 DOS 개행으로 바꾸어 커밋한다.
  • 파일의 모든 라인이 바뀌었지만, 공백만 바뀌었다.
  • 그 후 “hello world” 문자열을 “hello mundo” 로 바꾼 다음에 커밋한다.
$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'converted hello.rb to DOS'
[whitespace 3270f76] converted hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 1 file changed, 1 insertion(+), 1 deletion(-)
  • master 브랜치로 다시 이동한 다음에 함수에 대한 설명을 추가한다.
$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'document the function'
[master bec6336] document the function
 1 file changed, 1 insertion(+)
  • 이때 whitespace 브랜치를 Merge 하면 공백변경 탓에 충돌이 난다.
$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

1.1 Merge 취소하기

  • Merge 중에 발생한 충돌을 해결하는 방법은 몇 가지가 있다.
  • 첫 번째는 그저 이 상황을 벗어나는 것이다.
  • 예상하고 있던 일도 아니고 지금 당장 처리할 일도 아니라면 git merge --abort 명령으로 간단히 Merge 하기 전으로 되돌린다.
$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master
  • git merge --abort 명령은 Merge 하기 전으로 되돌린다.
  • 완전히 뒤로 되돌리지 못하는 유일한 경우는 Merge 전에 워킹 디렉토리에서 Stash 하지 않았거나 커밋하지 않은 파일이 존재하고 있었을 때뿐이다. 그 외에는 잘 돌아간다.

  • 어떤 이유로든 Merge를 처음부터 다시 하고 싶다면 git reset --hard HEAD 명령으로 되돌릴 수 있다.
  • 이 명령은 워킹 디렉토리를 그 시점으로 완전히 되돌려서 저장하지 않은 것은 사라진다는 점에 주의하자.

1.2 공백 무시하기

  • 공백 때문에 충돌이 날 때도 있다.
  • 단순한 상황이고 실제로 충돌난 파일을 살펴봤을 때 한 쪽의 모든 라인이 지워지고 다른 쪽에는 추가됐기 때문에 간단하다고 할 수 있다.
  • 기본적으로 Git은 이런 모든 라인이 변경됐다고 인지하여 Merge 할 수 없다.

  • 기본 Merge 전략은 공백의 변화는 무시하도록 하는 옵션을 주는 것이다.
  • Merge 할 때 무수한 공백 때문에 문제가 생기면 그냥 Merge를 취소한 다음 -Xignore-all-space-Xignore-space-change 옵션을 주어 다시 Merge 한다.
  • 첫 번째 옵션은 모든 공백을 무시하고 두 번째 옵션은 뭉쳐 있는 공백을 하나로 취급한다.
$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
  • 위 예제는 모든 공백 변경 사항을 무시하면 실제 파일은 충돌 나지 않고 모든 Merge가 잘 실행된다.

  • 팀원 중 누군가 스페이스를 탭으로 바꾸거나 탭을 스페이스로 바꾸는 짓을 했을 때 이 옵션이 그대를 구원해 준다.

1.3 수동으로 Merge 하기

  • Merge 작업할 때 공백 처리 옵션을 사용하면 Git이 꽤 잘해준다.
  • 하지만, Git이 자동으로 해결하지 못하는 때도 있다.
  • 이럴 때는 외부 도구의 도움을 받아 해결한다.
  • 예를 들어 Git이 자동으로 해결해주지 못하는 상황에 부닥치면 직접 손으로 해결해야 한다.

  • 파일을 dos2unix 로 변환하고 Merge 하면 된다.
  • 이걸 Git에서 어떻게 하는지 살펴보자.

  • 먼저 Merge 충돌 상태에 있다고 치자.
  • 현 시점의 파일과 Merge 할 파일, 공통 조상의 파일이 필요하다.
  • 이 파일들로 어쨌든 잘 Merge 되도록 수정하고 다시 Merge를 시도해야 한다.

  • 우선 세 가지 버전의 파일을 얻는 건 쉽다.
  • Git은 세 버전의 모든 파일에 “stages” 숫자를 붙여서 Index에 다 가지고 있다.
  • Stage 1는 공통 조상 파일, Stage 2는 현재 개발자의 버전에 해당하는 파일, Stage 3은 MERGE_HEAD 가 가리키는 커밋의 파일이다.

  • git show 명령으로 각 버전의 파일을 꺼낼 수 있다.
$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb
  • 좀 더 저수준으로 파고들자면 ls-files -u 명령을 사용한다.
  • 이 명령은 Plumbing 명령으로 각 파일을 나타내는 Git Blob의 SHA-1를 얻을 수 있다.
$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb
  • :1:hello.rb 는 그냥 Blob SHA-1를 지칭하는 줄임말이다.

  • 이제 워킹 디렉토리에 세 버전의 파일을 모두 가져왔다.
  • 공백 문제를 수동으로 고친 다음에 다시 Merge 한다.
  • Merge 할 때는 git merge-file 명령을 이용한다.
$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()
  • 이렇게 해서 멋지게 Merge가 완료된 파일을 얻었다.
  • 사실 이것이 ignore-all-space 옵션을 사용하는 것보다 더 나은 방법이다.
  • 왜냐면 공백을 무시하지 않고 실제로 고쳤기 때문이다.
  • ignore-all-space 옵션을 사용한 Merge 에서는 여전히 DOS의 개행 문자가 남아서 한 파일에 두 형식의 개행문자가 뒤섞인다.

  • Merge 커밋을 완료하기 전에 양쪽 부모에 대해서 무엇이 바뀌었는지 확인하려면 git diff 를 사용한다.
  • 이 명령을 이용하면 Merge 의 결과로 워킹 디렉토리에 무엇이 바뀌었는지 알 수 있다.

  • Merge 후의 결과를 Merge 하기 전의 브랜치와 비교하려면, 다시 말해 무엇이 합쳐졌는지 알려면 git diff --ours 명령을 실행한다.
$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()
  • 위의 결과에서 Merge를 했을 때 현재 브랜치에서는 무엇을 추가했는지를 알 수 있다.

  • Merge 할 파일을 가져온 쪽과 비교해서 무엇이 바뀌었는지 보려면 git diff --theirs 를 실행한다.
  • 아래 예제에서는 공백을 빼고 비교하기 위해 -b 옵션을 같이 써주었다.
$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end
  • 마지막으로 git diff --base 를 사용해서 양쪽 모두와 비교하여 바뀐 점을 알아본다.
$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()
  • 수동 Merge를 위해서 만들었던 각종 파일은 이제 필요 없으니 git clean 명령을 실행해서 지워준다.
$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

1.4 충돌 파일 Checkout

  • 앞서 살펴본 여러가지 방법으로 충돌을 해결했지만 바라던 결과가 아닐 수도 있고 심지어 결과가 잘 동작하지 않아 충돌을 직접 수동으로 더 많은 정보를 살펴보며 해결해야 하는 경우도 있다.

  • 예제를 조금 바꿔보자. 이번 예제에서는 긴 호흡의 브랜치 두 개가 있다.
  • 각 브랜치에는 몇 개의 커밋이 있는데 양쪽은 Merge 할 때 반드시 충돌이 날 만한 내용이 들어 있다.
$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed text to hello mundo
|/
* b7dcc89 initial hello world code
  • master 에만 있는 세 개의 커밋과 mundo 브랜치에만 존재하는 또 다른 세 개의 커밋이 있다. master 브랜치에서 mundo 브랜치를 Merge 하면 충돌이 난다.
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.
  • 해당 파일을 열어서 충돌이 발생한 내용을 보면 아래와 같다.
#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()
  • 양쪽 브랜치에서 추가된 부분이 이 파일에 다 적용됐다. 적용한 커밋 중 파일의 같은 부분을 수정해서 위와 같은 충돌이 생긴다.

  • 충돌을 해결하는 몇 가지 도구에 대해 알아보자. 어쩌면 이 충돌을 어떻게 해결해야 하는지 명확하지 않을 수도 있다. 맥락을 좀 더 살펴봐야 하는 상황 말이다.

  • git checkout 명령에 --conflict 옵션을 붙여 사용하는 게 좋은 방법이 될 수 있다.
  • 이 명령은 파일을 다시 Checkout 받아서 충돌 표시된 부분을 교체한다.
  • 충돌 난 부분은 원래의 코드로 되돌리고 다시 고쳐보려고 할 때 알맞은 도구다.

  • --conflict 옵션에는 diff3merge 를 넘길 수 있고 merge 가 기본 값이다.
  • --conflict 옵션에 diff3 를 사용하면 Git은 약간 다른 모양의 충돌 표시를 남긴다.
  • “ours” 나 “theirs” 말고도 “base” 버전의 내용까지 제공한다.
$ git checkout --conflict=diff3 hello.rb
  • 위 명령을 실행하면 아래와 같은 결과가 나타난다.
#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()
  • 이런 형태의 충돌 표시를 계속 보고 싶다면 기본으로 사용하도록 merge.conflictstyle 설정 값을 diff3 로 설정한다.
$ git config --global merge.conflictstyle diff3
  • git checkout 명령도 --ours--theirs 옵션을 지원한다.
  • 이 옵션은 Merge 하지 않고 둘 중 한쪽만을 선택할 때 사용한다.

  • 이 옵션은 바이너리 파일이 충돌 나서 한쪽을 선택해야 하는 상황이나 한쪽 브랜치의 온전한 파일을 원할 때 사용할 수 있다.
  • 일단 Merge 하고 나서 특정 파일만 Checkout 한 후에 커밋하는 방법도 있다.

1.5 Merge 로그

  • git log 명령은 충돌을 해결할 때도 도움이 된다.
  • 로그에는 충돌을 해결할 때 도움이 될만한 정보가 있을 수 있다.
  • 과거를 살짝 들춰보면 개발 당시에 같은 곳을 고쳐야만 했던 이유를 밝혀내는 데 도움이 된다.

  • “Triple Dot” 문법을 이용하면 Merge 에 사용한 양 브랜치의 모든 커밋의 목록을 얻을 수 있다.
$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo
  • 위와 같이 총 6개의 커밋을 볼 수 있다. 커밋이 어떤 브랜치에서 온 것인지 보여준다.

  • 맥락에 따라 필요한 결과만 추려 볼 수도 있다.
  • git log 명령에 --merge 옵션을 추가하면 충돌이 발생한 파일이 속한 커밋만 보여준다.
$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo
  • --merge 대신 -p 를 사용하면 충돌 난 파일의 변경사항만 볼 수 있다.
  • 이건 왜 충돌이 났는지 또 이를 해결하기 위해 어떻게 해야 하는지 이해하는 데 진짜로 유용하다.

1.6 Combined Diff 형식

  • Merge가 성공적으로 끝난 파일은 Staging Area에 올려놓았다.
  • 이 상태에서 충돌 난 파일들이 그대로 있을 때 git diff 명령을 실행하면 충돌 난 파일이 무엇인지 알 수 있다.
  • 어떤 걸 더 고쳐야 하는지 아는 데에 도움이 된다.

  • Merge 하다가 충돌이 났을 때 git diff 명령을 실행하면 꽤 생소한 Diff 결과를 보여준다.
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()
  • 이런 형식을 “Combined Diff” 라고 한다.
  • 각 라인은 두 개의 컬럼으로 구분할 수 있다.
  • 첫 번째 컬럼은 “ours” 브랜치와 워킹 디렉토리의 차이(추가 또는 삭제)를 보여준다.
  • 두 번째 컬럼은 “theirs” 와 워킹 디렉토리사이의 차이를 나타낸다.

  • 이 예제에서 <<<<<<<>>>>>>> 충돌 마커 표시는 어떤 쪽에도 존재하지 않고 추가된 코드라는 것을 알 수 있다.
  • 이 표시는 Merge 도구가 만들어낸 코드이기 때문이다. 물론 이 표시는 지워야 하는 라인이다.

  • 충돌을 다 해결하고 git diff 명령을 다시 실행하면 아래와 같이 보여준다. 이 결과도 유용하다.
$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()
  • 이 결과는 세 가지 사실을 보여준다.
  • “hola world” 는 Our 브랜치에 있었지만 워킹 디렉토리에는 없다.
  • “hello mundo” 는 Their 브랜치에 있었지만 워킹 디렉토리에는 없다.
  • “hola mundo” 는 어느 쪽 브랜치에도 없고 워킹 디렉토리에는 있다.
  • 충돌을 해결하고 마지막으로 확인하고 나서 커밋하는 데 유용하다.

  • 이 정보를 git log 명령을 통해서도 얻을 수 있다.
  • Merge 후에 무엇이 어떻게 바뀌었는지 알아야 할 때 유용하다.
  • Merge 커밋에 대해서 git show 명령을 실행하거나 git log -p--cc 옵션을 추가해도 같은 결과를 얻을 수 있다.
  • git log -p 명령은 기본적으로 Merge 커밋이 아닌 커밋의 Patch를 출력한다.
$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

2. Merge 되돌리기

  • Merge 할 때 실수할 수도 있다.
  • Git에서는 실수해도 된다.
  • 실수해도 (대부분 간단하게) 되돌릴 수 있다.

  • Merge 커밋도 예외는 아니다.
  • 토픽 브랜치에서 일을 하다가 master 로 잘못 Merge 했다고 생각해보자.
  • 커밋 히스토리는 아래와 같다.

  • 접근 방식은 원하는 결과에 따라 두 가지로 나눌 수 있다.

2.1 Refs 수정

  • 실수로 생긴 Merge 커밋이 로컬 저장소에만 있을 때는 브랜치를 원하는 커밋을 가리키도록 옮기는 것이 쉽고 빠르다.
  • 잘못 Merge 하고 나서 git reset --hard HEAD~ 명령으로 브랜치를 되돌리면 된다.
  • Merge 하고 나서 다른 커밋을 생성했다면 제대로 동작하지 않는다.
  • HEAD를 이동시키면 Merge 이후에 만든 커밋을 잃어버린다.

2.2 커밋 되돌리기

  • 브랜치를 옮기는 것을 할 수 없는 경우는 모든 변경사항을 취소하는 새로운 커밋을 만들 수도 있다.
  • Git에서 이 기능을 “revert” 라고 부른다. 지금의 경우엔 아래처럼 실행한다.
$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"
  • -m 1 옵션은 부모가 보호되어야 하는 “mainline” 이라는 것을 나타낸다.
  • HEAD 로 Merge를 했을 때(git merge topic1) Merge 커밋은 두 개의 부모 커밋을 가진다.
  • 첫 번째 부모 커밋은 HEAD (C6)이고 두 번째 부모 커밋은 Merge 대상 브랜치(C4)이다.
  • 두 번째 부모 커밋(C4)에서 받아온 모든 변경사항을 되돌리고 첫 번째 부모(C6)로부터 받아온 변경사항은 남겨두고자 하는 상황이다.

  • 변경사항을 되돌린 커밋은 히스토리에서 아래와 같이 보인다.

  • 새로 만든 커밋 ^MC6 과 내용이 완전히 똑같다.
  • 잘못 Merge 한 커밋까지 HEAD 의 히스토리에서 볼 수 있다는 것 말고는 Merge 하지 않은 것과 같다.
  • topic 브랜치를 master 브랜치에 다시 Merge 하면 Git은 아래와 같이 어리둥절해한다.
$ git merge topic
Already up-to-date.
  • 이미 Merge 했던 topic 브랜치에는 더는 master 브랜치로 Merge 할 내용이 없다.
  • 상황을 더 혼란스럽게 하는 경우는 topic 에서 뭔가 더 일을 하고 다시 Merge를 하는 경우이다.
  • Git은 Merge 이후에 새로 만들어진 커밋만 가져온다.

  • 이러면 가장 좋은 방법은 되돌렸던 Merge 커밋을 다시 되돌리는 것이다.
  • 이후에 추가한 내용을 새 Merge 커밋으로 만드는 게 좋다.
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
  • 위 예제에서는 M^M 이 상쇄됐다.
  • ^^MC3C4 의 변경 사항을 담고 있고 C8C7 의 내용을 훌륭하게 Merge 했다. 이리하여 현재 topic 브랜치를 완전히 Merge 한 상태가 됐다.

3. 다른 방식의 Merge

  • 지금까지 두 브랜치를 평범하게 Merge 하는 방법에 대해 알아보았다.
  • Merge는 보통 “recursive” 전략을 사용한다.
  • 브랜치를 한 번에 Merge 하는 방법은 여러 가지다. 그 중 몇 개만 간단히 알아보자.

3.1 Our/Their 선택하기

  • 먼저 일반적인 “recursive” 전략을 사용하는 Merge 작업을 할 때 유용한 옵션을 소개한다.
  • 앞에서 ignore-all-spaceignore-space-change 기능을 -X 옵션에 붙여 쓰는 것을 보았다.
  • -X 옵션은 충돌이 났을 때 어떤 한 쪽을 선택할 때도 사용한다.

  • 아무 옵션도 지정하지 않고 두 브랜치를 Merge 하면 Git은 코드에 충돌 난 곳을 표시하고 해당 파일을 충돌 난 파일로 표시해준다.
  • 충돌을 직접 해결하는 게 아니라 미리 Git에게 충돌이 났을 때 두 브랜치 중 한쪽을 선택하라고 알려줄 수 있다.
  • merge 명령을 사용할 때 -XoursXtheirs 옵션을 추가하면 된다.

  • Git에 이 옵션을 주면 충돌 표시가 남지 않는다.
  • Merge가 가능하면 Merge 될 것이고 충돌이 나면 사용자가 명시한 쪽의 내용으로 대체한다.
  • 바이너리 파일도 똑같다.

  • “hello world” 예제로 돌아가서 다시 Merge를 해보자.
  • Merge를 하면 충돌이 나는 것을 볼 수 있다.
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
  • 하지만 -Xours-Xtheirs 옵션을 주면 충돌이 났다는 소리가 없다.
$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh
  • 한쪽 파일에는 “hello mundo” 가 있고 다른 파일에는 “hola world” 가 있다.
  • 이 Merge에서 충돌 표시를 하는 대신 간단히 “hola world” 를 선택한다.
  • 충돌 나지 않은 나머지는 잘 Merge 된다.

  • 이 옵션은 git merge-file 명령에도 사용할 수 있다.
  • 앞에서 이미 git merge-file --ours 같이 실행해서 파일을 따로따로 Merge 했다.

  • 이런 식의 동작을 원하지만 애초에 Git이 Merge 시도조차 하지 않는 자비 없는 옵션도 있다.
  • “ours” Merge 전략 이다. 이 전략은 Recursive Merge 전략의 “ours” 옵션 과는 다르다.

  • 이 작업은 기본적으로 거짓으로 Merge 한다.
  • 그리고 양 브랜치를 부모로 삼는 새 Merge 커밋을 만든다.
  • 하지만, Their 브랜치는 참고하지 않는다.
  • Our 브랜치의 코드를 그대로 사용하고 Merge 한 것처럼 기록할 뿐이다.
$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$
  • 지금 있는 브랜치와 Merge 결과가 다르지 않다는 것을 알 수 있다.

  • ours 전략을 이용해 이미 Merge가 되었다고 Git을 속이고 실제로는 Merge를 나중에 수행한다.
  • 예를 들어 release 브랜치을 만들고 여기에도 코드를 추가했다.
  • 언젠가 이것을 master 브랜치에도 Merge 해야 하지만 아직은 하지 않았다.
  • 그리고 master 브랜치에서 bugfix 브랜치를 만들어 버그를 수정하고 이것을 release 브랜치에도 적용(Backport)해야 한다.
  • bugfix 브랜치를 release 브랜치로 Merge 하고 이미 포함된 master 브랜치에도 merge -s ours 명령으로 Merge 해 둔다.
  • 이렇게 하면 나중에 release 브랜치를 Merge 할 때 버그 수정에 대한 커밋으로 충돌이 일어나지 않게끔 할 수 있다.

3.2 서브트리 Merge

  • 서브트리 Merge 의 개념은 프로젝트 두 개가 있을 때 한 프로젝트를 다른 프로젝트의 하위 디렉토리로 매핑하여 사용하는 것이다.
  • Merge 전략으로 서브트리(Subtree)를 사용하는 경우 Git은 매우 똑똑하게 서브트리를 찾아서 메인 프로젝트로 서브프로젝트의 내용을 Merge 한다.

  • 한 저장소에 완전히 다른 프로젝트의 리모트 저장소를 추가하고 데이터를 가져와서 Merge 까지 하는 과정을 살펴보자.

  • 먼저 Rack 프로젝트 현재 프로젝트에 추가한다.
  • Rack 프로젝트의 리모트 저장소를 현재 프로젝트의 리모트로 추가하고 Rack 프로젝트의 브랜치와 히스토리를 가져와(Fetch) 확인한다.
$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"
  • (역주 - git fetch rack_remote 명령의 결과에서 warning: no common commits 메시지를 주목해야 한다.) Rack 프로젝트의 브랜치인 rack_branch 를 만들었다.
  • 원 프로젝트는 master 브랜치에 있다.
  • checkout 명령으로 두 브랜치를 이동하면 전혀 다른 두 프로젝트가 한 저장소에 있는 것처럼 보인다.
$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README
  • 상당히 요상한 방식으로 Git을 활용한다.
  • 저장소의 브랜치가 꼭 같은 프로젝트가 아닐 수도 있다.
  • Git에서는 전혀 다른 브랜치를 쉽게 만들 수 있다. 물론 이렇게 사용하는 경우는 드물다.

  • Rack 프로젝트를 master 브랜치의 하위 디렉토리로 만들 수 있다.
  • 이는 git read-tree 명령을 사용한다.
  • 간단히 말하자면 read-tree 명령은 어떤 브랜치로부터 루트 트리를 읽어서 현재 Staging Area나 워킹 디렉토리로 가져온다.
  • master 브랜치로 다시 Checkout 하고 rack_branch 브랜치를 rack 이라는 master 브랜치의 하위 디렉토리로 만들어보자.
$ git read-tree --prefix=rack/ -u rack_branch
  • 이제 커밋하면 Rack 프로젝트의 모든 파일이 Tarball 압축파일을 풀어서 소스코드를 포함한 것 같이 커밋에 새로 추가된다.
  • 이렇게 쉽게 한 브랜치의 내용을 다른 브랜치에 Merge 시킬 수 있다는 점이 흥미롭지 않은가?
  • Rack 프로젝트가 업데이트되면 Pull 해서 master 브랜치도 적용할 수 있을까?
$ git checkout rack_branch
$ git pull
  • 위의 명령을 실행하고 업데이트된 결과를 master 브랜치로 다시 Merge 한다.
  • Recursive Merge 전략 옵션인 -Xsubtree 옵션과 --squash 옵션을 함께 사용하면 동일한 커밋 메시지로 업데이트할 수 있다. (Recursive 전략이 기본 전략이지만 설명을 위해서 사용한다)
$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
  • 위 명령을 실행하면 Rack 프로젝트에서 변경된 모든 부분이 master 브랜치로 반영되고 커밋할 준비가 완료된다.
  • 반대로 rack 하위 디렉토리에서 변경한 내용을 rack_branch 로 Merge 하는 것도 가능하다.
  • 변경한 것을 메인테이너에게 보내거나 Upstream에 Push 한다.

  • 이런 방식은 서브모듈을 사용하지 않고 서브모듈을 관리하는 또 다른 워크플로이다.
  • 한 저장소 안에 다른 프로젝트까지 유지하면서 서브트리 Merge 전략으로 업데이트도 할 수 있다.
  • 프로젝트에 필요한 코드를 한 저장소에서 관리할 수 있다.
  • 다만, 이렇게 저장소를 관리하는 방법은 저장소를 다루기 좀 복잡하고 통합할 때 실수하기 쉽다.
  • 엉뚱한 저장소로 Push 해버릴 가능성도 있다.

  • diff 명령으로 rack 하위 디렉토리와 rack_branch 의 차이를 볼 때도 이상하다.
  • Merge 하기 전에 두 차이를 보고 싶어도 diff 명령을 사용할 수 없다.
  • 대신 git diff-tree 명령이 준비돼 있다.
$ git diff-tree -p rack_branch
  • 혹은 rack 하위 디렉토리가 Rack 프로젝트의 리모트 저장소의 master 브랜치와 어떤 차이가 있는지 살펴보고 싶을 수도 있다.
  • 마지막으로 Fetch 한 리모트의 master 브랜치와 비교하려면 아래와 같은 명령을 사용한다.
$ git diff-tree -p rack_remote/master