Git 실전 · 되돌린 기능이 다시 안 들어온다 · 이론
되돌린 기능은 다시 merge 해도 들어오지 않는다
한 줄 요약
revert 는 변경을 뒤집는 새 커밋을 만듭니다. 그래서 병합 커밋을 되돌리려면 어느 쪽을
기준으로 뒤집을지(-m)를 말해야 하고, 되돌린 기능 브랜치를 나중에 다시 merge 하면 git 은
"이미 합쳐져 있다" 고 답합니다. 이력은 그대로 있고 내용만 사라진 상태이기 때문입니다.
왜 이게 필요했나
릴리스 다음 날 장애가 납니다. 원인은 어제 합친 기능 브랜치이고, 이미 여러 사람이
그 이력을 받아 갔습니다. reset 은 쓸 수 없습니다 — 남에게 간 커밋을 지우는 것이라
모두의 이력이 어긋납니다. 그래서 revert 를 씁니다.
그런데 그 병합 커밋을 되돌리려고 하면 git 이 멈춥니다.
$ git revert 685e75ferror: commit 685e75f is a merge but no -m option was given.fatal: revert failed병합 커밋은 부모가 둘입니다. "되돌린다" 는 것은 어떤 상태로 돌아간다는 뜻인데, 부모가
둘이면 어느 부모의 상태로 돌아갈지가 정해지지 않습니다. -m 1 은 첫 번째 부모
(대개 합쳐 넣은 쪽, 즉 main)를 기준으로 삼아 두 번째 부모가 가져온 변경을 뒤집으라는
뜻입니다. -m 2 는 반대입니다.
여기까지는 대부분 알고 넘어갑니다. 진짜 함정은 그다음입니다.
어떻게 동작하나
병합을 revert -m 1 로 되돌린 뒤, 기능을 고쳐서 다시 넣으려고 git merge feature 를
하면 이렇게 나옵니다.
Already up to date.버그처럼 보이지만 정확한 답입니다. merge 는 이력의 도달 가능성으로 판단합니다.
그 병합 커밋이 여전히 이력에 있고 feature 의 모든 커밋이 거기서 도달 가능하므로,
merge 입장에서는 합칠 것이 남아 있지 않습니다. 내용이 없는 이유는 그 뒤에 온 revert
커밋이 지웠기 때문이지, 합쳐지지 않아서가 아닙니다.
그래서 되돌린 기능을 되살리는 방법은 revert 를 revert 하는 것입니다.
git revert <되돌림 커밋>git 2.x 는 이 커밋의 기본 제목을 Reapply "..." 로 붙여 줍니다. 이력에 되돌림과
되살림이 모두 남으므로, 나중에 "이 기능이 왜 두 번 들어왔나" 를 이력만 읽고 알 수
있습니다. Linux 커널 문서에서 이 패턴을 권하는 이유도 같습니다 — 이력이 실제로 일어난
일을 말해야 합니다.
cherry-pick 에 출처를 남기기
핫픽스를 릴리스 브랜치에서 만들고 main 에도 넣는 일은 흔합니다. 그냥 cherry-pick
하면 새 커밋에는 원본이 어디였는지가 남지 않습니다. -x 를 붙이면 메시지 끝에 한 줄이
붙습니다.
(cherry picked from commit fb42e70c24e9bab398dcef64c3b2bd316de8ca4e)나중에 "이 수정이 릴리스에도 들어갔나" 를 조사할 때 이 한 줄이 있고 없고의 차이가
큽니다. 다만 비공개 저장소에서 공개 저장소로 옮길 때는 없는 해시를 가리키게 되니
그때는 쓰지 않습니다.
같은 변경이 이미 들어갔는지 — patch-id
"이 커밋이 저 브랜치에 이미 반영됐나" 는 해시로는 알 수 없습니다. cherry-pick 은
새 커밋을 만들기 때문입니다. git 은 대신 patch-id 를 씁니다 — diff 에서 공백과
줄 번호를 지우고 해시한 값이라, 같은 변경이면 커밋 해시가 달라도 같은 값이 나옵니다.
$ git show <원본> | git patch-id --stable37d09d56... fb42e70c...$ git show <가져온 것> | git patch-id --stable37d09d56... 075fc52e...git cherry -v <upstream> <head> 가 이 계산을 대신해 줍니다. 앞의 - 는 그 변경이
upstream 에 이미 있다는 뜻이고, + 는 아직 없다는 뜻입니다.
여기에 중요한 한계가 있습니다. 충돌을 해결해 가져온 커밋은 patch-id 가 달라집니다.
문맥이 바뀌었으니 diff 도 다르기 때문입니다. 그래서 git cherry 는 그 커밋을 +
(아직 없음)로 표시합니다. 자동 판정을 그대로 믿고 다시 가져오면 같은 수정이 두 번
들어갑니다 — 이 도구가 답하는 질문은 "같은 diff 가 있는가" 이지 "같은 의도가 반영됐는가"
가 아닙니다.
현장에서 만나는 모습
- 릴리스 브랜치의 핫픽스를
main에 가져올 때-x를 규칙으로 정해 둡니다. - 문제가 된 기능을 되돌렸다가 고쳐서 다시 넣을 때, 되살림 커밋의 제목만 보고
- 장기 지원 브랜치에 백포트할 때
git cherry -v로 남은 것을 먼저 세고, 충돌로 - 충돌 중에
git cherry-pick --abort로 깨끗이 물러설 수 있다는 것을 알면, 반쯤
"왜 두 번 들어왔지" 라고 묻는 일이 없어집니다.
해결한 항목은 목록에 손으로 표시해 둡니다.
해결한 상태로 커밋해 버리는 사고가 줄어듭니다.
다음 실습에서 할 것
병합 커밋을 만들고 -m 없이 되돌려 거절당하는 것부터 봅니다. -m 1 로 되돌린 뒤git merge 가 "Already up to date" 를 내는 것을 확인하고, revert 의 revert 로
되살립니다. 그다음 핫픽스를 -x 로 가져오고, 충돌하는 커밋 하나는 --abort 로 물러섰다
다시 --continue 로 마칩니다. 마지막에 git cherry 와 git patch-id 로 무엇이 이미
들어갔는지 세어 보면, 충돌로 해결한 커밋이 왜 + 로 남는지가 눈에 보입니다.
공식 문서는 [git-revert](https://git-scm.com/docs/git-revert),
[git-cherry-pick](https://git-scm.com/docs/git-cherry-pick),
[git-cherry](https://git-scm.com/docs/git-cherry),
[git-patch-id](https://git-scm.com/docs/git-patch-id) 입니다.