Pro git - 7.9

Updated:

Pro git - Rerere

1. 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” 로 바꿨다.

  • 이런 상황에서 이 두 브랜치를 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” 이라는 메시지를 결과에서 볼 수 있다.

  • 이제 Merge를 되돌리고 Rebase를 해서 master 브랜치에 쌓아 보자
  • 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

  • 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
  • 이때 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 기능을 켜두는 게 여러모로 몸과 마음에 도움이 된다.