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
옵션에는diff3
나merge
를 넘길 수 있고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
)로부터 받아온 변경사항은 남겨두고자 하는 상황이다. - 변경사항을 되돌린 커밋은 히스토리에서 아래와 같이 보인다.
- 새로 만든 커밋
^M
은C6
과 내용이 완전히 똑같다. - 잘못 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
이 상쇄됐다. ^^M
는C3
와C4
의 변경 사항을 담고 있고C8
은C7
의 내용을 훌륭하게 Merge 했다. 이리하여 현재topic
브랜치를 완전히 Merge 한 상태가 됐다.
3. 다른 방식의 Merge
- 지금까지 두 브랜치를 평범하게 Merge 하는 방법에 대해 알아보았다.
- Merge는 보통 “recursive” 전략을 사용한다.
- 브랜치를 한 번에 Merge 하는 방법은 여러 가지다. 그 중 몇 개만 간단히 알아보자.
3.1 Our/Their 선택하기
- 먼저 일반적인 “recursive” 전략을 사용하는 Merge 작업을 할 때 유용한 옵션을 소개한다.
- 앞에서
ignore-all-space
와ignore-space-change
기능을-X
옵션에 붙여 쓰는 것을 보았다. -
이
-X
옵션은 충돌이 났을 때 어떤 한 쪽을 선택할 때도 사용한다. - 아무 옵션도 지정하지 않고 두 브랜치를 Merge 하면 Git은 코드에 충돌 난 곳을 표시하고 해당 파일을 충돌 난 파일로 표시해준다.
- 충돌을 직접 해결하는 게 아니라 미리 Git에게 충돌이 났을 때 두 브랜치 중 한쪽을 선택하라고 알려줄 수 있다.
-
merge
명령을 사용할 때-Xours
나Xtheirs
옵션을 추가하면 된다. - 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