Published on

Git rebase 후 force push 충돌 복구 체크리스트

Authors

서로 다른 사람이 같은 브랜치(또는 PR 브랜치)를 만지는 팀에서 git rebase--force(혹은 --force-with-lease)로 푸시하면, 원격 브랜치의 기준 커밋이 바뀌면서 동료의 작업이 “사라진 것처럼 보이거나”, PR이 엉뚱한 커밋을 포함하거나, 머지 충돌이 연쇄적으로 발생하는 일이 흔합니다.

이 글은 “이미 force push가 나가버린 뒤”를 전제로, 손실을 최소화하고 빠르게 정상 상태로 되돌리는 복구 체크리스트를 제공합니다. 핵심은 두 가지입니다.

  1. 절대 덮어쓰지 말고 증거(커밋 포인터)를 먼저 확보한다.
  2. 원격/로컬/동료 브랜치의 “정상 기준점”을 찾고, 그 위에 필요한 커밋만 재적용(cherry-pick/rebase) 한다.

> 참고: CI가 꼬여서 캐시/빌드가 이상해 보일 때는 원인이 Git 히스토리 변경(캐시 키 변화)일 수도 있습니다. 관련해서는 GitHub Actions 캐시 안 먹힘 원인 7가지도 함께 보면 좋습니다.

0) 상황 분류: 지금 어떤 문제가 발생했나

복구 전략은 “어떤 형태로 꼬였는지”에 따라 달라집니다.

  • A. 내 로컬에서 rebase 후 force push → 원격이 내 히스토리로 덮임
    • 동료가 원격을 기준으로 작업 중이었다면, 동료 커밋이 원격에서 안 보이게 됨(실제로는 dangling 상태일 수 있음)
  • B. 동료가 force push → 내 로컬이 원격과 완전히 diverge
    • git pull이 fast-forward 불가, 충돌/중복 커밋 발생
  • C. PR 브랜치에 rebase+force push → 리뷰 코멘트가 옛 커밋에 달림
    • GitHub UI상 “Outdated”가 늘고, 커밋 비교가 난해해짐
  • D. merge commit이 있는 브랜치를 rebase → 히스토리 재작성으로 충돌 폭발
    • rebase --rebase-merges 필요 가능

아래 체크리스트는 A~D를 포괄하되, 가장 치명적인 데이터 손실(A) 기준으로 안전하게 진행합니다.

1) 1분 안전조치: 덮어쓰기 중단 + 백업 브랜치 만들기

1-1. 추가 force push 금지

팀에 즉시 공유합니다.

  • “지금부터 해당 브랜치에 push 금지”
  • 누가 마지막으로 force push 했는지, 언제 했는지 확인

1-2. 로컬 상태 백업 브랜치 생성

현재 로컬 HEAD가 무엇이든, 일단 포인터를 남깁니다.

# 작업 중인 브랜치에서
current=$(git rev-parse --abbrev-ref HEAD)

git branch backup/${current}-$(date +%Y%m%d-%H%M%S)

1-3. 원격 브랜치 상태도 별도 포인터로 보관

원격 추적 브랜치 포인터를 로컬 브랜치로 저장합니다.

git fetch --all --prune

git branch backup/origin-${current}-$(date +%Y%m%d-%H%M%S) origin/${current}

이 두 개만 해도 “최악의 경우”에서 돌아갈 수 있는 발판이 생깁니다.

2) 증거 수집: reflog로 “사라진 커밋” 찾기

force push로 원격에서 커밋이 안 보이더라도, 로컬/동료 로컬에는 남아 있을 가능성이 큽니다.

2-1. 내 로컬 reflog 확인

git reflog --date=iso

rebase 직전/직후, reset, pull 등의 이벤트가 시간순으로 나옵니다. 여기서 다음을 찾습니다.

  • rebase 전 HEAD: rebase (start) 이전의 커밋
  • force push 이전의 로컬 브랜치가 가리키던 커밋

원하는 커밋 해시를 찾았다면, 즉시 브랜치로 고정합니다.

# 예: abc1234가 잃어버린 기준 커밋이라면
git branch rescue/lost-abc1234 abc1234

2-2. 원격에 남아있는 흔적 확인 (GitHub)

GitHub는 가끔 “force push 이전 커밋”이 UI에서 접근 가능한 경우가 있습니다.

  • PR의 “force-pushed” 이벤트
  • PR의 이전 커밋 목록
  • 브랜치 보호 규칙/감사 로그(조직 설정)

하지만 UI만 믿지 말고, 최종 복구는 커밋 해시 기반으로 진행하는 게 안전합니다.

3) 원인 확인: 왜 충돌/중복이 생겼나 (짧은 진단)

3-1. diverge 상태 확인

git fetch origin

git log --oneline --decorate --graph --left-right --cherry-pick \
  HEAD...origin/${current}
  • 왼쪽(<)은 로컬에만 있는 커밋
  • 오른쪽(>)은 원격에만 있는 커밋
  • --cherry-pick은 패치가 동일한 커밋을 “중복처럼” 표시하는 데 도움

3-2. “중복 커밋”인지 “진짜 다른 변경”인지

rebase 후에는 커밋 해시가 바뀌므로, 동일 변경이 다른 해시로 존재할 수 있습니다.

# 특정 커밋이 같은 패치인지 확인
git show <commit>

git patch-id < <(git show <commit>)

(실전에서는 git log --cherry/--cherry-pick이 더 편합니다.)

4) 복구 전략 선택: 3가지 패턴 중 하나로 끝낸다

여기서부터는 목표를 명확히 합니다.

  • 목표 1: 원격 브랜치를 “정상 기준점”으로 되돌린다
  • 목표 2: 사라진 커밋을 다시 얹는다
  • 목표 3: PR/CI를 가능한 덜 흔들며 정리한다

전략 A) “원격을 이전 상태로 되돌리기” (가장 직관적)

force push가 잘못 나갔고, 이전 원격 HEAD가 명확할 때.

  1. 이전 원격 HEAD(또는 정상 커밋) 해시를 찾습니다(예: good1234).
  2. 로컬에서 해당 커밋으로 브랜치를 맞춥니다.
  3. --force-with-lease 원격을 되돌립니다.
# good1234가 되돌릴 기준 커밋

git checkout ${current}
git reset --hard good1234

git push --force-with-lease origin ${current}
  • --force 대신 --force-with-lease를 쓰면, 내가 마지막으로 본 원격 상태에서 누군가 또 푸시했을 때 덮어쓰기를 막아줍니다.

전략 B) “원격은 유지, 내/동료 커밋을 다시 얹기” (협업 친화)

이미 원격에 새 커밋이 쌓였고, 되돌리면 더 큰 혼란이 예상될 때.

  1. 원격 최신을 기준으로 새 브랜치를 판다.
  2. 잃어버린 커밋들을 cherry-pick으로 선별 복구한다.
git fetch origin

git checkout -b restore/${current} origin/${current}

# 잃어버린 커밋들을 순서대로 적용
# (여러 개면 커밋 범위나 리스트를 사용)
git cherry-pick <c1> <c2> <c3>

# 충돌 시 해결 후
# git add ...
# git cherry-pick --continue

복구 브랜치를 PR로 올려서 리뷰/CI를 통과한 뒤, 최종적으로 원래 브랜치에 반영하는 방식이 안전합니다.

전략 C) “rebase를 다시 제대로 수행” (히스토리 정리 목적)

히스토리를 깔끔히 만들려다 꼬인 경우, 올바른 rebase 옵션으로 재정렬합니다.

  • merge commit이 있으면 --rebase-merges 고려
  • 충돌이 반복되면 --rerere-autoupdate로 재충돌 비용 감소
git fetch origin

git checkout ${current}

# 원격 기준으로 재정렬
# merge 커밋을 유지하고 싶다면 --rebase-merges
# 반복 충돌 완화: --rerere-autoupdate
git rebase --rebase-merges --rerere-autoupdate origin/main

# 검증 후
# (가능하면 팀 합의 후) push

git push --force-with-lease origin ${current}

5) “사라진 커밋”이 원격 어디에도 없을 때: dangling 커밋 복구

동료가 아직 로컬에 커밋을 갖고 있다면, 가장 확실한 복구 경로입니다.

5-1. 동료에게 요청할 것

  • 해당 브랜치에서 git reflog --date=iso 출력
  • “사라진 커밋”의 해시 또는 브랜치 포인터
  • 가능하면 임시 브랜치로 푸시
# 동료 로컬에서

git branch rescue/from-alice <lost_commit>
git push origin rescue/from-alice

그 다음 내 쪽에서 해당 브랜치를 기준으로 cherry-pick/rebase 하면 됩니다.

5-2. 내 로컬에서 dangling 객체 스캔 (최후 수단)

# 도달 불가능한 커밋/객체를 찾아줌
git fsck --lost-found

# .git/lost-found/commit 아래에 커밋이 생길 수 있음

찾은 커밋 해시를 브랜치로 고정한 뒤 복구를 진행합니다.

6) 충돌을 “빨리” 끝내는 실전 팁

6-1. conflict 범위를 줄이는 재배치

cherry-pick/rebase는 커밋 단위라 충돌이 누적됩니다. 충돌이 큰 커밋은 쪼개는 게 낫습니다.

# 인터랙티브 rebase로 커밋 쪼개기

git rebase -i <base>
# pick -> edit 로 바꾸고

git reset HEAD^
# 파일 단위로 add 후 커밋을 여러 개로

git add -p
git commit -m "split: part 1"

git add -p
git commit -m "split: part 2"

git rebase --continue

6-2. rerere로 반복 충돌 자동화

같은 충돌을 여러 번 해결하는 상황이라면 rerere를 켜면 체감이 큽니다.

git config --global rerere.enabled true
git config --global rerere.autoupdate true

6-3. “내가 맞다/상대가 맞다” 빠른 선택

충돌 파일에서 한쪽을 통째로 선택해야 하는 경우.

# 현재 브랜치(ours)로 통일
git checkout --ours path/to/file

# 상대 브랜치(theirs)로 통일
git checkout --theirs path/to/file

git add path/to/file

7) 푸시 단계 체크리스트: 다시는 덮어쓰지 않기

7-1. force push는 무조건 with-lease

git push --force-with-lease origin ${current}
  • 단, 여러 사람이 같은 브랜치에 푸시하는 환경이라면, 애초에 force push를 금지하는 브랜치 보호 규칙이 더 낫습니다.

7-2. 원격이 내가 아는 것과 같은지 확인

git fetch origin

git rev-parse HEAD

git rev-parse origin/${current}

7-3. PR/CI 관점의 검증

히스토리 재작성은 캐시 키/빌드 결과를 흔들 수 있습니다. CI가 이상하게 보이면 캐시 미스도 함께 의심하세요.

8) 팀 운영 체크리스트(재발 방지)

8-1. 브랜치 보호 규칙

  • main, release/*는 force push 금지
  • PR 브랜치도 팀 규칙으로 “리뷰 시작 이후에는 rebase 금지(또는 제한)”

8-2. rebase 정책 명문화

  • 개인 브랜치: 자유롭게 rebase 가능
  • 공유 브랜치: 원칙적으로 merge 또는 squash merge
  • 예외적으로 rebase가 필요하면:
    • 사전 공지
    • 작업 중단 시간 합의
    • --force-with-lease만 허용

8-3. 복구용 커맨드 템플릿을 팀 위키에 고정

사고는 반복됩니다. 아래 템플릿만 있어도 복구 속도가 달라집니다.

# 1) 백업
b=$(git rev-parse --abbrev-ref HEAD)
git branch backup/$b-$(date +%Y%m%d-%H%M%S)
git fetch origin
git branch backup/origin-$b-$(date +%Y%m%d-%H%M%S) origin/$b

# 2) 증거
git reflog --date=iso | head -n 30

# 3) 비교
git log --oneline --graph --left-right --cherry-pick HEAD...origin/$b

9) 최종 점검: “복구 완료” 정의

복구가 끝났다고 말하려면, 최소한 아래를 만족해야 합니다.

  • 원격 브랜치가 의도한 커밋 집합을 포함한다(누락/중복 없음)
  • PR diff가 “의도한 변경”과 일치한다
  • CI가 정상이며(필요 시 캐시 무효화 포함), 배포 파이프라인이 기대대로 동작한다
  • 팀원이 각자 로컬에서 원격을 다시 추적할 수 있다

동료가 로컬을 원격에 맞추는 방법(안전한 쪽)을 함께 안내하면 마무리가 깔끔합니다.

# 로컬 변경이 없다는 전제에서, 원격을 기준으로 브랜치 재정렬

git fetch origin

git checkout ${current}

git reset --hard origin/${current}

rebase+force push 사고의 본질은 “커밋 해시가 바뀌는 재작성”과 “원격 포인터 덮어쓰기”가 결합된 것입니다. 그래서 복구도 결국 포인터를 백업하고(reflog/backup branch), 정상 기준점을 찾고, 필요한 커밋만 재적용하는 절차로 수렴합니다. 위 체크리스트를 팀 표준으로 만들어두면, 사고가 나도 10분 내로 통제 가능한 수준으로 떨어뜨릴 수 있습니다.