Pro git - 7.13

Updated:

Pro git - Replace

1. Replace

  • 히스토리(혹은 데이터베이스)에 일단 저장한 Git의 개체는 기본적으로 변경할 수 없다.
  • 하지만 변경된 것처럼 보이게 하는 재밌는 기능이 숨어 있다.

  • Git의 replace 명령은 “어떤 개체를 읽을 때 항상 다른 개체로 보이게” 한다.
  • 히스토리에서 어떤 커밋이 다른 커밋처럼 보이도록 할 때 이 명령이 유용하다(git filter-branch를 사용하여 전체 히스토리를 다시 작성할 필요가 없는 것이다).

  • 예를 들어 현재 프로젝트의 히스토리가 아주 방대한 상태다.
  • 히스토리를 둘로 나누어서 새로 시작하는 개발자에게는 히스토리를 아주 간단한 몇 개의 커밋으로 만들어서 제공하고, 프로젝트 히스토리를 분석할 사람에게는 전체 히스토리를 제공하는 상황을 생각해보자.
  • replace 명령으로 간단해진 히스토리를 전체 히스토리의 마지막 부분에 연결해서 사용할 수 있다.
  • 이렇게 히스토리를 변경하는 데도 커밋을 새로 쓰지 않는 매우 훌륭한 기능이다(Rebase를 생각해보자. 한 부모를 변경하면 이후의 커밋은 모두 재작성된다).

  • 위와 같은 상황을 한번 해보자.
  • 히스토리가 어느 정도 쌓여 있는 Git 저장소를 두 저장소로 분리해서 하나는 최신 커밋 몇 개만 유지하도록 하고 다른 하나는 전체 히스토리를 유지하기로 한다.
  • 이렇게 분리한 두 히스토리를 커밋을 재작성하지 않고 replace 명령을 사용하여 연결한다.

  • 아래 예제로 사용하는 저장소는 히스토리에 커밋 5개가 있다.
$ git log --oneline
ef989d8 fifth commit
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
  • 예제의 히스토리를 둘로 나누어보자.
  • 하나는 첫 번째부터 네 번째 커밋까지 히스토리로 만들어 원래의 히스토리를 그대로 유지한다.
  • 다른 새 히스토리는 네 번째 커밋과 다섯 번째 커밋만을 포함하도록 한다.

  • 원래의 히스토리를 유지하는 히스토리를 만들기는 쉽다.
  • 원래 히스토리 상에 기준점을 잡아 새 브랜치를 만들고 히스토리를 유지할 리모트 저장소로 Push 하면 간단히 해결된다.
$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

  • history 브랜치를 새 저장소에 master 브랜치로 Push 한다.
$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master
  • 원래 히스토리를 유지하는 히스토리를 Push 했다.
  • 이제 남은 어려운 부분은 최신 커밋만 유지하도록 히스토리를 중간에 끊고 새로 만드는 작업이다.
  • 새로 만든 히스토리와 원래 히스토리를 나중에 연결해서 사용할 때 네 번째 커밋을 연결하도록 작업한다.
  • 따라서 새로 만든 히스토리는 네 번째 이후의 커밋만 유지한다.
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
  • 이런 예제 같은 경우 히스토리를 어떻게 연결하는지 설명하는 커밋을 만들어 나중에 개발자든 누구든 전체 히스토리를 볼 수 있도록 하는 것이 좋다.
  • 이런 내용과 함께 네 번째 커밋 이전의 상태를 담을 새 커밋을 하나 만들고 네 번째 이후 커밋을 이 새 커밋 위에 Rebase 하기로 한다.

  • 기준으로 삼을 커밋을 선택하고 새 커밋을 만든다.
  • 예제는 9c68fdc 해시 값을 갖는 세 번째 커밋이 된다.
  • 세 번째 커밋의 트리 내용을 기본 상태로 삼고 네 번째 이후 커밋을 히스토리에 쌓는다.
  • commit-tree 명령을 사용해서 새 커밋을 만든다.
  • 명령에 트리 개체를 전달하면 부모 없는 새 커밋을 생성하여 해시 값을 반환한다.
$ echo 'get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf

  • 이제 네 번째 커밋 이후의 히스토리를 쌓을 커밋이 준비됐다.
  • git rebase --onto 명령으로 네 번째 이후의 커밋을 새 커밋에 Rebase 한다.
  • --onto 옵션 뒤에 전달할 커밋은 쌓아올릴 대상이 되는 커밋을 입력한다.
  • 위에서 commit-tree 명령으로 반환받은 커밋을 사용하고 Rebase의 기준은 네 번째 커밋의 부모 커밋, 즉 세 번째 커밋인 9c68fdc 해시를 전달한다.
$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit

  • 위와 같이 Rebase 하고 나면 최신 커밋만 유지하는 새로운 히스토리가 만들어진다.
  • 새 히스토리의 가장 첫 번째 커밋에는 어떻게 이전 히스토리를 연결해서 확인할 수 있는지 설명하는 내용이 포함되게 된다.
  • 이렇게 생성한 새 히스토리를 새 리모트 저장소로 Push 한다.
  • 그리고 나서 Clone 해서 히스토리를 살펴보면 가장 최근 커밋 몇 개만 보이고 가장 첫 커밋에는 히스토리를 연결하는 내용이 있게 된다.

  • 이제 역할을 바꾸어 새 히스토리를 Clone 하고 전체 히스토리까지 확인하고자 하는 작업을 예로 들어보자.
  • 원래 히스토리로부터 분리한 새 히스토리 위에서 원래 히스토리를 확인하려면 우선 원래 히스토리를 포함하는 리모트 저장소를 추가하고 히스토리를 Fetch 한다.
$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master
  • 위와 같이 실행하고 나면 master 브랜치에는 간단한 히스토리의 최신 커밋만 있다.
  • 그리고 project-history/master 브랜치에는 원래 히스토리 전체가 있게 된다.
$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
  • 이 두 히스토리를 연결하기 위해 git replace 명령을 사용하여 새 히스토리의 커밋이 원래 히스토리에 속한 커밋을 가리키도록 할 수 있다.
  • 예제에서는 새 히스토리의 ‘fourth commit’과 project-history/master 브랜치의 ‘fourth commit’을 파라미터로 전달한다.
$ git replace 81a708d c6e1e95
  • 이제 master 브랜치에서 히스토리를 조회해보면 아래와 같은 히스토리가 된다.
$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
  • 연결한 네 번째 커밋 이후의 커밋을 재작성하지 않고도 replace 명령으로 간단하게 히스토리를 변경했다.
  • 변경한 히스토리에서도 bisectblame 같은 다른 Git 명령을 사용할 수 있다.

  • 연결된 히스토리를 보면 replace 명령으로 커밋을 변경했음에도 여전히 c6e1e95 해시가 아니라 81a708d 해시로 나오는 것을 확인할 수 있다.
  • 반면 cat-file 명령으로 보면 c6e1e95 해시의 내용이 출력된다.
$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit
  • Replace 이전 네 번째 커밋 81a708d 해시의 부모는 622e88e 해시이므로 위의 9c68fdce 로 나오는 내용은 변경한 대상인 c6e1e95 해시의 내용이다.

  • 이렇게 히스토리를 연결하는 것 같은 Replace 명령의 결과는 Refs로 관리한다.

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040
  • Replace 내용을 Refs로 관리한다는 말은 손쉽게 이 내용을 서버로 Push 하여 다른 팀원과 공유할 수 있다는 것을 뜻한다.
  • 이렇게 Replace하는 것이 유용하지 않을 수도 있다.
  • 어쨌든 모든 팀원이 두 히스토리를 다운로드해야 하는데 굳이 나눠야 하나?
  • 하지만, 어떨 때는 Replace하는 것이 유용할 수도 있다.