Published on

Git rebase 후 강제푸시 충돌? reflog로 복구하기

Authors

서론

git rebase는 커밋 히스토리를 깔끔하게 만들지만, 그 대가로 커밋 SHA가 바뀌는(=히스토리가 재작성되는) 작업입니다. 그래서 rebase 이후 원격 브랜치에 반영하려면 보통 강제 푸시가 필요하고, 이때 팀원이 같은 브랜치에 커밋을 올렸거나, 내가 다른 기기에서 작업한 커밋이 섞여 있으면 “충돌”처럼 보이는 상황(혹은 실제로 원격 히스토리를 덮어써 버리는 사고)이 자주 발생합니다.

다행히 Git은 대부분의 경우 reflog(레퍼런스 로그) 덕분에 “되돌릴 수 있는 흔적”을 로컬에 남깁니다. 이 글에서는 다음을 목표로 합니다.

  • rebase + 강제푸시에서 문제가 생기는 대표 패턴 이해
  • reflogrebase 이전 상태 또는 강제푸시 이전 상태를 찾아 복구
  • 안전한 재푸시(--force-with-lease)와 팀 협업 관점의 재발 방지

참고로 “원인 파악 → 안전한 복구 → 재발 방지” 흐름은 인프라 장애 대응과도 유사합니다. 디버깅 루틴을 정립해두면, 예를 들어 Terraform EKS 상태 꼬임으로 apply 무한 반복 끊기 같은 케이스에서도 ‘로그/상태를 근거로 되돌리는’ 감각이 그대로 도움이 됩니다.

왜 rebase 후 강제푸시가 위험해지는가

1) rebase는 커밋을 “수정”이 아니라 “재생성”한다

rebase는 기존 커밋을 그대로 두고 순서만 바꾸는 게 아니라, 보통 새 커밋을 만들어 기존 커밋을 대체합니다. 그래서 같은 내용이라도 SHA가 바뀌고, 원격 브랜치가 가리키던 커밋과 로컬 브랜치가 가리키는 커밋이 완전히 다른 그래프가 됩니다.

2) 강제푸시는 원격 브랜치 포인터를 “내가 가진 히스토리로” 강제로 이동시킨다

  • git push --force: 원격 브랜치가 무엇을 가리키든 상관없이 덮어씀
  • git push --force-with-lease: 원격이 내가 마지막으로 본 상태에서 변하지 않았을 때만 덮어씀(상대적으로 안전)

팀원이 먼저 푸시한 커밋이 있는데 내가 --force로 밀어버리면, 원격에서 그 커밋이 사라진 것처럼 보입니다(실제로는 일정 기간 서버에 남아있을 수 있으나, 일반적인 브랜치 참조에서는 끊깁니다).

3) “충돌”은 두 종류다

  • 리베이스 과정에서의 충돌: git rebase 중 파일 충돌
  • 푸시 단계에서의 충돌/거부: 원격이 앞서 있거나(Non-fast-forward), 혹은 --force-with-lease가 “원격이 바뀌었다”고 판단하여 거부

이 글은 주로 “rebase 후 강제푸시 단계에서 꼬인 히스토리”를 reflog로 복구하는 시나리오에 집중합니다.

사고 전, 가장 먼저 할 일: 현재 상태를 보존하기

복구를 시작하기 전에, 현재 로컬 브랜치 상태를 별도 브랜치로 보관해두면 마음이 편해집니다.

# 현재 작업 브랜치가 feature/login 이라고 가정

git status

git branch backup/feature-login-before-recover

원격도 함께 확인합니다.

git fetch --all --prune

git log --oneline --decorate --graph --all --max-count=30

이 단계에서 “원격이 어떤 커밋을 가리키는지”, “내 로컬이 어떤 커밋을 가리키는지”를 눈으로 확인하는 게 중요합니다.

reflog로 복구 가능한 이유

Git은 HEAD가 이동한 기록을 로컬에 남깁니다. rebase는 HEAD를 여러 번 이동시키므로 reflog에 흔적이 풍부하게 남습니다.

reflog 확인:

git reflog --date=iso

출력 예시는 대략 이런 형태입니다.

1a2b3c4 HEAD@{0}: rebase (finish): returning to refs/heads/feature/login
9f8e7d6 HEAD@{1}: rebase (pick): Add validation
8a7b6c5 HEAD@{2}: rebase (start): checkout main
3d2c1b0 HEAD@{3}: commit: WIP: login form
...

여기서 핵심은:

  • rebase (start) 직전의 HEAD@{n} 또는
  • rebase 시작 전에 있던 브랜치 포인터

를 찾아서 그 커밋으로 브랜치를 되돌릴 수 있다는 점입니다.

시나리오 A: rebase를 잘못했고, 아직 강제푸시는 안 했다

이 경우가 가장 쉽습니다.

  1. reflog에서 rebase 시작 직전 커밋을 찾습니다.
git reflog --date=iso
  1. 해당 커밋으로 브랜치를 되돌립니다.
# 예: rebase 시작 직전이 HEAD@{3} 라고 가정

git reset --hard HEAD@{3}
  1. 원격과 비교해서 정상인지 확인합니다.
git log --oneline --decorate --graph origin/feature/login feature/login --max-count=20

이제 다시 rebase를 하든, merge로 바꾸든 선택하면 됩니다.

시나리오 B: rebase 후 강제푸시까지 했고, 원격 히스토리가 덮였다

여기서부터가 “사고”에 가깝습니다. 그래도 복구는 가능합니다.

1) 원격에서 사라진 커밋을 찾는 방법

원격 브랜치가 덮이기 전의 커밋은 내 로컬 reflog에 남아있을 가능성이 큽니다(강제푸시를 한 로컬에서라면 특히).

git reflog show feature/login --date=iso

브랜치별 reflog를 보면, 해당 브랜치가 가리키던 이전 커밋들이 나옵니다.

또는 origin/feature/login의 reflog는 로컬에 없을 수 있지만, fetch를 수행한 시점의 remote-tracking ref 이동도 일부 남습니다.

git reflog show origin/feature/login --date=iso

(환경/설정에 따라 기록이 적을 수 있습니다.)

2) “복구 브랜치”를 먼저 만든다

찾아낸 커밋(예: OLD_SHA)을 바로 reset하기보다, 일단 브랜치로 보관합니다.

# OLD_SHA에 덮이기 전 원격이 가리키던 커밋이 있다고 가정

git branch recover/feature-login OLD_SHA

git log --oneline --decorate --graph recover/feature-login --max-count=10

이제 recover 브랜치를 기준으로 복구 전략을 선택합니다.

3) 원격을 “되돌리는” 두 가지 전략

전략 1) 원격 브랜치를 아예 이전 커밋으로 되돌리기(강제푸시)

팀 합의가 있고, 원격을 정확히 과거 상태로 복구해야 한다면:

# 현재 브랜치를 recover/feature-login으로 전환

git switch recover/feature-login

# 원격 브랜치를 과거 커밋으로 되돌림
# --force-with-lease를 권장 (원격이 또 변했으면 막아줌)

git push --force-with-lease origin recover/feature-login:feature/login

주의:

  • 이 작업은 또 한 번 히스토리를 덮습니다.
  • 복구 타이밍 사이에 다른 사람이 푸시했다면 --force-with-lease가 거부할 수 있고, 그때는 다시 협의가 필요합니다.

전략 2) “사라진 커밋”을 새 커밋으로 다시 얹기(cherry-pick)

원격을 되감는 게 부담스럽다면, 사라진 커밋들을 현재 브랜치 위로 다시 적용하는 방식이 안전할 때가 많습니다.

# 현재 원격 상태를 기준으로 브랜치 맞추기

git switch feature/login

git fetch origin

git reset --hard origin/feature/login

# recover 브랜치에만 있는 커밋을 골라서 적용
# (범위는 상황에 맞게 조정)

git cherry-pick OLD_SHA1..OLD_SHA2

# 정상 푸시

git push origin feature/login

이 방식은 “원격 히스토리를 되감지” 않기 때문에 협업 충격이 상대적으로 작습니다. 다만 커밋 SHA는 달라지고(새로 생성), 충돌이 날 수도 있습니다.

--force vs --force-with-lease: 실무 기준

가능하면 무조건 --force-with-lease를 기본값처럼 사용하세요.

# 나의 로컬이 알고 있는 origin/feature/login이
# 실제 원격과 동일할 때만 강제 업데이트

git push --force-with-lease origin feature/login

추가로 더 엄격하게 “특정 SHA일 때만” 허용하고 싶다면, lease를 명시할 수도 있습니다.

# 원격 브랜치가 특정 SHA일 때만 강제푸시 허용

git push --force-with-lease=refs/heads/feature/login:EXPECTED_SHA origin feature/login

이 패턴은 CI/CD에서 배포 브랜치 보호를 우회해야 하는 특수 상황에서 유용합니다.

reflog로도 못 찾을 때 점검할 것

  • 다른 로컬 클론(다른 PC/디렉터리)에 커밋이 남아있을 수 있음
  • 팀원의 로컬에도 해당 커밋이 남아있을 수 있음
  • GitHub/GitLab은 일정 기간 동안 “dangling commit”이 UI나 API로 접근 가능한 경우가 있음(조직/설정에 따라 다름)
  • 로컬에서 GC가 돌았거나 reflog 만료 설정이 짧으면 기록이 사라질 수 있음

로컬 reflog 보존 기간은 설정으로 확인할 수 있습니다.

git config --get gc.reflogExpire

git config --get gc.reflogExpireUnreachable

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

1) 공유 브랜치에는 rebase 금지, 개인 브랜치에서만 허용

  • main, develop, 릴리즈 브랜치: rebase 금지(merge만)
  • 개인 feature 브랜치: rebase 허용(단, PR 올리기 전/리뷰 중 규칙 합의)

2) 브랜치 보호 규칙 + 강제푸시 제한

GitHub 기준으로는 브랜치 보호(Protected Branch)로 강제푸시를 막거나, 특정 권한만 허용할 수 있습니다.

3) 강제푸시 전 “원격 최신 확인”을 습관화

git fetch origin

git log --oneline --decorate --graph origin/feature/login..feature/login

git log --oneline --decorate --graph feature/login..origin/feature/login
  • 첫 번째 로그: 내가 원격에 없는 커밋
  • 두 번째 로그: 원격에 있는데 내가 없는 커밋(이게 있으면 강제푸시 위험)

4) 작업 자동화(CI)에서도 ‘안전장치’를 둔다

강제푸시가 포함된 자동화는 특히 위험합니다. GitHub Actions 캐시처럼 “될 줄 알았는데 안 되는” 문제가 반복되면 원인을 좁히는 체크리스트가 중요하듯, 강제푸시 자동화도 사전 점검이 필요합니다. 관련해서는 GitHub Actions 캐시가 안 먹을 때 키·경로·권한처럼 원인 분해 방식으로 접근하면 도움이 됩니다.

실전 복구 예제: 한 번에 따라 하기

상황: feature/login에서 rebase 후 --force로 푸시했고, 팀원 커밋이 사라졌다고 가정.

  1. 현재 상태 백업
git switch feature/login

git branch backup/feature-login-now
  1. reflog에서 덮기 전 커밋 찾기
git reflog show feature/login --date=iso
  1. 덮기 전 커밋으로 복구 브랜치 생성
git branch recover/feature-login OLD_SHA
  1. 원격 되돌리기(팀 합의 후)
git switch recover/feature-login

git push --force-with-lease origin recover/feature-login:feature/login
  1. 팀원에게 안내(필수)
  • “원격이 특정 시점으로 되돌아갔다”
  • “각자 로컬 브랜치에서 git fetch 후, 필요하면 git reset --hard origin/feature/login 또는 rebase/merge로 정리”

결론

rebase 자체가 나쁜 도구는 아닙니다. 문제는 공유된 히스토리에 rebase를 적용하고, 강제푸시로 원격을 덮는 순간 협업 비용이 급증한다는 점입니다. 그래도 사고가 났을 때는 당황하지 말고,

  • (1) 현재 상태를 백업 브랜치로 고정
  • (2) git reflog로 “정상 시점”의 커밋을 찾고
  • (3) 복구 브랜치를 만든 뒤
  • (4) 원격 되돌리기(강제푸시) 또는 cherry-pick으로 재적용

순서로 접근하면 대부분 복구할 수 있습니다.

장애 대응에서 로그가 핵심이듯, Git 복구에서는 reflog가 핵심입니다. 필요한 순간에 바로 꺼내 쓸 수 있도록, 평소에 --force-with-lease와 함께 reflog 조회를 습관화해두면 좋습니다.