Git
Chapters ▾ 2nd Edition

7.9 Git 도구 - Rerere

Rerere

git rerere 기능은 약간 숨겨진 기능이다. “reuse recorded resolution” 이라고 해서 기록한 해결책 재사용하기란 뜻의 이름이고 이름 그대로 동작한다. Git은 충돌이 났을 때 각 코드 덩어리를 어떻게 해결했는지 기록을 해 두었다가 나중에 같은 충돌이 나면 기록을 참고하여 자동으로 해결한다.

이 기능을 사용하면 재미있는 시나리오가 가능하다. 문서에서 드는 예제 중 하나는 긴 호흡의 브랜치를 깔끔하게 Merge 하고 싶은데 Merge 커밋은 많이 만들고 싶지 않을 때 사용하는 것이다. rerere 기능을 켜고 자주 Merge를 해서 충돌을 해결하고 Merge 이전으로 돌아간다. 이 과정을 반복해서 기록을 쌓아두면 rerere 기능은 나중에 한 번에 Merge 할 때 기록을 참고한다. 자동으로 충돌이 날 만한 부분을 다 해결해주시니 몸과 마음이 평안하다.

브랜치를 Rebase 할 때도 같은 전략을 사용할 수 있다. 쌓인 충돌 해결 기록을 참고하여 Git은 Rebase 할 때 발생한 충돌도 최대한 해결한다. 충돌 덩어리들을 해결하고 Merge 했는데 다시 Rebase 하기로 마음을 바꿨을 때 같은 충돌을 두 번 해결할 필요 없다.

또 다른 상황을 생각해보자. 뭔가를 개선한 토픽 브랜치가 여러 개 있을 때 이것을 테스트 브랜치에 전부 다 Merge 해야 한다. Git 프로젝트 자체에서 자주 이렇게 한다. 테스트가 실패하면 해당 Merge를 취소하고 테스트가 실패한 토픽 브랜치만 빼고 다시 Merge한다. 한 번 해결한 충돌은 다시 손으로 해결하지 않아도 된다.

rerere 기능은 간단히 아래 명령으로 설정하여 활성화한다.

$ git config --global rerere.enabled true

저장소에 .git/rr-cache 디렉토리를 만들어 기능을 켤 수도 있다. config 명령을 사용하는 방법이 깔끔하고 Global로 설정할 수도 있다.

간단한 예제를 하나 더 살펴보자. 위에서 살펴본 예제와 비슷하다. 아래와 같은 hello.rb 파일 하나가 있다.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

이전 예제와 마찬가지로 한 브랜치에서는 “hello” 를 “hola” 로 바꿨다. 그리고 다른 브랜치에서는 “world” 를 “mundo” 로 바꿨다.

rerere1

이런 상황에서 이 두 브랜치를 Merge 하면 당연히 충돌이 발생한다.

$ git merge i18n-world
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Recorded preimage for 'hello.rb'
Automatic merge failed; fix conflicts and then commit the result.

Merge 명령을 실행한 결과에 Recorded preimage for FILE 라는 결과를 눈여겨봐야 한다. 저 말이 없으면 평소처럼 그냥 충돌이 난다. 지금은 rerere 기능 때문에 몇 가지 정보를 더 출력했다. 보통은 git status 명령을 실행해서 어떤 파일에 충돌이 발생했는지 확인한다.

$ git status
# On branch master
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add <file>..." to mark resolution)
#
#	both modified:      hello.rb
#

git rerere status 명령으로 충돌 난 파일을 확인할 수 있다.

$ git rerere status
hello.rb

그리고 git rerere diff 명령으로 해결 중인 상태를 확인할 수 있다. 얼마나 해결했는지 비교해서 보여준다.

$ git rerere diff
--- a/hello.rb
+++ b/hello.rb
@@ -1,11 +1,11 @@
 #! /usr/bin/env ruby

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

rerere 기능에 포함된 것은 아니지만 git ls-files -u 명령으로 이전/현재/대상 버전의 해시를 확인할 수도 있다.

$ git ls-files -u
100644 39804c942a9c1f2c03dc7c5ebcd7f3e3a6b97519 1	hello.rb
100644 a440db6e8d1fd76ad438a49025a9ad9ce746f581 2	hello.rb
100644 54336ba847c3758ab604876419607e9443848474 3	hello.rb

이제는 puts 'hola mundo' 내용으로 충돌을 해결하자. 마지막으로 git rerere diff 명령을 실행하면 rerere가 기록할 내용을 확인할 수 있다.

$ git rerere diff
--- a/hello.rb
+++ b/hello.rb
@@ -1,11 +1,7 @@
 #! /usr/bin/env ruby

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

간단하게 말해서 Git은 hello.rb 파일에서 충돌이 발생했을 때 한쪽엔 “hello mundo” 이고 다른 한쪽에는 “hola world” 이면 이를 “hola mundo” 로 해결한다.

이제 이 파일을 해결한 것으로 표시한 다음에 커밋한다.

$ git add hello.rb
$ git commit
Recorded resolution for 'hello.rb'.
[master 68e16e5] Merge branch 'i18n'

커밋을 쌓고 나면 "Recorded resolution for FILE" 이라는 메시지를 결과에서 볼 수 있다.

rerere2

이제 Merge를 되돌리고 Rebase를 해서 master 브랜치에 쌓아 보자. Reset 명확히 알고 가기에서 살펴본 대로 git reset 명령을 사용하여 브랜치가 가리키는 커밋을 되돌린다.

$ git reset --hard HEAD^
HEAD is now at ad63f15 i18n the hello

이렇게 Merge 하기 이전 상태로 돌아왔다. 이제 토픽 브랜치를 Rebase 한다.

$ git checkout i18n-world
Switched to branch 'i18n-world'

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: i18n one word
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Failed to merge in the changes.
Patch failed at 0001 i18n one word

예상대로 Merge 했을 때와 같은 충돌이 발생한다. 하지만, Rebase를 실행한 결과에 Resolved 'hello.rb' using previous resolution 메시지가 있다. 이 파일을 열어보면 이미 충돌이 해결된 것을 볼 수 있다. 파일 어디에도 충돌이 발생했다는 표시를 찾아볼 수 없다.

#! /usr/bin/env ruby

def hello
  puts 'hola mundo'
end

git diff 명령을 실행해보면 Git이 자동으로 해결한 결과도 확인할 수 있다.

$ git diff
diff --cc hello.rb
index a440db6,54336ba..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
rerere3

git checkout 명령으로 충돌이 발생한 시점의 상태로 파일 내용을 되돌릴 수도 있다.

$ git checkout --conflict=merge hello.rb
$ cat hello.rb
#! /usr/bin/env ruby

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

고급 Merge에서 이러한 명령을 사용하는 예제를 보았다. 이때 git rerere 명령을 실행하면 충돌이 발생한 코드를 자동으로 다시 해결한다.

$ git rerere
Resolved 'hello.rb' using previous resolution.
$ cat hello.rb
#! /usr/bin/env ruby

def hello
  puts 'hola mundo'
end

강제로 충돌이 발생한 상황으로 되돌리고 rerere 명령으로 자동으로 충돌을 해결했다. 이제 충돌을 해결한 파일을 추가하고 Rebase를 완료하기만 하면 된다.

$ git add hello.rb
$ git rebase --continue
Applying: i18n one word

이처럼 여러 번 Merge 하거나, Merge 커밋을 쌓지 않으면서도 토픽 브랜치를 master 브랜치의 최신 내용으로 유지하거나, Rebase를 자주 한다면 rerere 기능을 켜두는 게 여러모로 몸과 마음에 도움이 된다.