히스토리 안전하게 재작성하기
모든 재작성(commit --amend, reset, rebase, filter-repo)은 새로운 커밋을 생성하고 브랜치 포인터를 이전 커밋에서 이동시킵니다. 아무도 이전 커밋을 가지고 있지 않다면 문제없지만, 누군가 가지고 있다면 파괴적입니다. 그들의 클론에는 여전히 이전 히스토리가 있고, 다음에 push나 pull할 때 중복이나 충돌로 돌아옵니다. 따라서 첫 번째 질문은 '어떻게'가 아니라 '누가 이 커밋을 가지고 있나?'입니다.
1. 결정: 재작성할지 되돌릴지(revert)
git fetch --prune
git status --short --branch # ahead/behind, dirty 상태
git branch -r --contains <commit> # 어떤 원격 브랜치가 이미 이 커밋을 가지고 있는지
git log --oneline @{u}..HEAD # 로컬에만 존재하는 커밋
| 커밋이 위치한 곳 | 이렇게 하세요 |
|---|---|
| 로컬 전용, 한 번도 push되지 않음 | 자유롭게 재작성. |
| 이 사용자만 작업하는 브랜치(해당 PR 브랜치)에 push됨 | 재작성 후 lease로 push (5단계). |
공유 또는 보호된 브랜치: main, master, develop, release/* 또는 다른 사람의 커밋이 위에 있는 모든 브랜치 | 재작성하지 마세요. git revert를 사용하세요. |
| 이미 릴리스되거나 배포됨 | git revert. 재작성으로 배포를 취소할 수 없습니다. |
사용자가 'main에서 그 커밋을 제거해 줘'라고 말할 때, 그들은 보통 SHA가 아니라 그 효과를 제거하기를 원합니다. git revert <sha>가 그렇게 해주고, 히스토리를 정직하게 유지하며, 일반 push만으로 충분합니다. 병합 커밋의 경우 git revert -m 1 <merge-sha>를 사용하세요. revert를 선택한 이유를 설명하고, 사용자가 공유 브랜치에서 지우기를 고집한다면 누구에게 영향을 미칠지 언급하고 명시적인 동의를 받으세요.
나중에 커밋이 동일한 줄을 편집한 경우 revert는 충돌이 발생합니다. 이는 정확히 나쁜 커밋 위에 커밋이 쌓인 경우입니다. 다른 충돌과 동일하게 해결하고(git-conflict-resolution 참조): 나쁜 변경을 제거하고, 이후 작업을 유지한 다음 git revert --continue를 실행하세요.
revert의 함정 하나: 병합을 되돌린 후 동일한 브랜치를 다시 병합해도 아무것도 가져오지 않습니다. Git은 해당 커밋이 이미 들어왔다고 생각하기 때문입니다. 작업을 다시 적용하려면 먼저 revert 자체를 되돌리세요(revert the revert).
2. 아무것도 건드리기 전에 복구 지점 생성
git rev-parse HEAD # 기록
git branch backup/<branch>-pre-rewrite # reflog가 아닌 실제 ref
reflog도 브랜치가 있던 위치를 기록하지만, 이 클론에 국한되며 만료되고(도달 가능한 항목은 90일, 도달 불가능한 항목은 기본 30일), reflog expire 후 git gc로 종료됩니다. 명명된 백업 브랜치는 비용이 없습니다. 사용자가 결과를 확인할 때까지 그대로 두세요.
3. 가장 덜 파괴적인 명령 선택
| 목표 | 명령 | 참고 |
|---|---|---|
| 마지막 커밋의 메시지 수정 또는 잊은 파일 추가 | git commit --amend | -m "..." 또는 --no-edit으로 에디터 열기 방지. |
| 커밋 취소하지만 변경사항은 staged 상태로 유지 | git reset --soft HEAD~1 | |
| 커밋 취소 및 unstage, 파일은 유지 | git reset --mixed HEAD~1 (기본값) | |
| 브랜치를 뒤로 이동, 로컬 편집 손실 거부 | git reset --keep <commit> | 커밋되지 않은 변경사항을 덮어쓰려고 하면 중단. --hard보다 선호. |
| 커밋되지 않은 작업도 버림 | git reset --hard | 커밋되지 않은 작업은 reflog에 없으며 복구할 수 없습니다. 거기에 원하는 것이 없다는 확인 후에만; 확실하지 않으면 먼저 git stash -u. |
| 하나의 파일 변경사항 폐기 | git restore <file> | |
| fix-up을 이전 커밋으로 squash 또는 병합 | git commit --fixup=<sha> 후 autosquash | 아래 참조. |
| 커밋 드롭 또는 순서 변경 | 제공된 todo로 rebase -i | 아래 참조. |
실패한 pre-commit 훅은 커밋이 만들어지지 않았음을 의미합니다. '재시도'하기 위해 git commit --amend를 실행하면 이전 커밋을 수정하여 새 변경사항을 잘못된 커밋에 넣게 됩니다. 훅 실패 후 문제를 수정하고, 다시 스테이징한 후 정상적인 새 커밋을 만드세요.
인터랙티브 에디터 없이 인터랙티브 리베이스
git rebase -i는 에디터를 열며 비대화형 셸에서 멈춥니다. 이에 의존하지 마세요. 작동하는 두 가지 방법:
# fix-up 커밋을 대상에 접기 - git이 생성한 todo 수용:
git commit --fixup=<target-sha>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
# 드롭 또는 순서 변경: 직접 todo 작성.
GIT_SEQUENCE_EDITOR="sed -i '/^pick <short-sha> /d'" git rebase -i <base>
<base>는 변경하려는 첫 번째 커밋 이전의 커밋입니다. 리베이스가 충돌에서 멈추면 해결하고, git add한 다음 GIT_EDITOR=true git rebase --continue를 실행하세요. 잘못되면 git rebase --abort로 시작 지점으로 돌아갑니다.
4. 재작성이 의도한 대로 변경되었는지 확인
메시지 편집, 순서 변경 또는 squash는 최종 트리가 동일해야 합니다:
git diff backup/<branch>-pre-rewrite HEAD --stat # 비어 있음 = 동일한 내용
git range-diff backup/<branch>-pre-rewrite...HEAD # 커밋별 비교
컨텐츠를 드롭하려 했다면 diff는 정확히 그 내용만 보여주고 다른 것은 없어야 합니다. 예상치 못한 것이 있으면 백업에서 복원하고 다시 수행하세요.
5. 게시 - 사용자의 승인을 받은 후에만
재작성을 push하는 것은 외부에 드러나고 되돌리기 어려운 단계입니다. 사용자가 이미 push를 요청하지 않았다면 먼저 확인하세요.
git push --force-with-lease=<branch>:<expected-remote-sha> --force-if-includes origin <branch>
- 절대 맨
--force를 사용하지 마세요. 원격에 있는 내용(아직 보지 못한 팀원의 push 포함)을 덮어씁니다. - 예상 SHA를 명시하세요. 일반
--force-with-lease는 원격 추적 ref와 비교하는데, IDE의 백그라운드git fetch가 해당 ref를 조용히 업데이트할 수 있으므로 lease가 아무것도 보호하지 못합니다. 1단계에서 본 SHA를 사용하세요(git rev-parse origin/<branch>). --force-if-includes(git 2.30+)는 또한 원격이 로컬 브랜치가 통합한 적 없는 것으로 이동한 경우 거부합니다.- 절대
main/master/develop/release/*를 force-push하지 마세요. 호스팅 규칙이 막고 있다면 그 규칙이 옳은 것입니다. 우회하지 마세요.
푸시 후, 이전 브랜치를 체크아웃한 사람은 pull 대신 새 브랜치로 리셋해야 하며 열린 PR의 리뷰 댓글이 앵커를 잃을 수 있다는 점을 사용자에게 알리세요.
안전장치
- 서명된 커밋은 재작성 시 서명을 잃습니다.
-S로 다시 서명하거나 사용자에게 알리세요. - 다른 사람이 작성한 커밋은 요청하지 않고 재작성하지 마세요. 심지어 기능 브랜치에서도 마찬가지입니다.
- 재작성된 커밋 위에 쌓인 스택 브랜치는 이제 오래되었습니다.
git-stacked-pr-rebase를 사용하여 각각 수동으로 리베이스하지 말고 다시 스택을 쌓으세요. - 커밋된 비밀은 재작성으로 해결되지 않습니다. 먼저 자격 증명을 교체하세요. 손상되었다고 가정하세요.
- 명확하게 보고하세요: 이전 팁이 무엇이었는지, 새 팁은 무엇인지, 백업 ref는 어디에 있는지, push되지 않은 것이 있는지. revert가 올바른 결정이었다면 그렇게 했다고 말하세요.