LabHub
学习 学习路径 课程

Git 实战

被回退的功能,再 merge 一次也回不来

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

한 줄 요약

revert변경을 뒤집는 새 커밋을 만듭니다. 그래서 병합 커밋을 되돌리려면 어느 쪽을 기준으로 뒤집을지(-m)를 말해야 하고, 되돌린 기능 브랜치를 나중에 다시 merge 하면 git 은 "이미 합쳐져 있다" 고 답합니다. 이력은 그대로 있고 내용만 사라진 상태이기 때문입니다.

概念图: 변경을 뒤집는 새 커밋 · 어느 부모의 상태로 · 이력의 도달 가능성 · revert 를 revert 하는 것

왜 이게 필요했나

릴리스 다음 날 장애가 납니다. 원인은 어제 합친 기능 브랜치이고, 이미 여러 사람이 그 이력을 받아 갔습니다. reset 은 쓸 수 없습니다 — 남에게 간 커밋을 지우는 것이라 모두의 이력이 어긋납니다. 그래서 revert 를 씁니다.

그런데 그 병합 커밋을 되돌리려고 하면 git 이 멈춥니다.

$ git revert 685e75f
error: 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 --stable
37d09d56... fb42e70c...
$ git show <가져온 것> | git patch-id --stable
37d09d56... 075fc52e...

git cherry -v <upstream> <head> 가 이 계산을 대신해 줍니다. 앞의 - 는 그 변경이 upstream 에 이미 있다는 뜻이고, + 는 아직 없다는 뜻입니다.

여기에 중요한 한계가 있습니다. 충돌을 해결해 가져온 커밋은 patch-id 가 달라집니다. 문맥이 바뀌었으니 diff 도 다르기 때문입니다. 그래서 git cherry 는 그 커밋을 + (아직 없음)로 표시합니다. 자동 판정을 그대로 믿고 다시 가져오면 같은 수정이 두 번 들어갑니다 — 이 도구가 답하는 질문은 "같은 diff 가 있는가" 이지 "같은 의도가 반영됐는가" 가 아닙니다.

현장에서 만나는 모습

다음 실습에서 할 것

병합 커밋을 만들고 -m 없이 되돌려 거절당하는 것부터 봅니다. -m 1 로 되돌린 뒤 git merge 가 "Already up to date" 를 내는 것을 확인하고, revert 의 revert 로 되살립니다. 그다음 핫픽스를 -x 로 가져오고, 충돌하는 커밋 하나는 --abort 로 물러섰다 다시 --continue 로 마칩니다. 마지막에 git cherrygit patch-id 로 무엇이 이미 들어갔는지 세어 보면, 충돌로 해결한 커밋이 왜 + 로 남는지가 눈에 보입니다.

공식 문서는 git-revert, git-cherry-pick, git-cherry, git-patch-id 입니다.