- Published on
Git rebase 후 강제푸시 막고 안전하게 되돌리기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 브랜치 히스토리를 깔끔하게 만들기 위해 git rebase를 쓰는 팀이 많습니다. 문제는 rebase가 커밋을 새로 쓰는 작업이라서, 원격에 이미 공유된 브랜치에서 rebase를 수행하면 로컬 히스토리와 원격 히스토리가 달라지고, 그 상태에서 git push --force 같은 강제 푸시가 필요해집니다.
강제 푸시 자체가 항상 나쁜 것은 아니지만, 공유 브랜치에서의 무분별한 강제 푸시는 다른 사람의 작업을 원격에서 "지워버리는" 결과로 이어질 수 있습니다. 이 글에서는
- 강제 푸시를 원천적으로 어렵게 만들고
- rebase 이후에도 안전하게 푸시하는 방법을 표준화하며
- 이미 강제 푸시가 발생했을 때 되돌리는 절차
를 실무 관점에서 정리합니다.
참고: CI에서 실수 방지를 강화하려면 브랜치 보호 규칙과 함께 파이프라인 정책도 같이 점검하는 편이 좋습니다. 관련해서는 GitLab CI 캐시 안 먹힐 때 key·policy·권한 점검 글의 "권한/정책" 파트를 같이 보면 도움이 됩니다.
왜 rebase가 강제 푸시를 부르는가
rebase는 커밋을 "이동"시키는 것처럼 보이지만, 실제로는 새 커밋들을 재작성합니다. 즉, 커밋 해시가 바뀝니다.
예를 들어 원격 origin/feature에 커밋 A-B-C가 있고, 로컬에서 rebase로 A-B-C'가 되었다면(해시는 달라짐), 원격은 여전히 A-B-C를 가리키고 있으니 단순 git push는 거절됩니다. 이때 사람들이 흔히 하는 선택이 git push --force입니다.
하지만 --force는 원격 브랜치의 포인터를 강제로 옮기기 때문에, 누군가 원격의 C를 기반으로 작업한 커밋들이 있다면 그 사람의 히스토리가 "고아"가 될 수 있습니다.
1) 강제 푸시를 막는 가장 확실한 방법: 브랜치 보호
가장 강력한 방어는 서버에서 막는 것입니다. 로컬 훅은 우회 가능하지만, GitHub/GitLab의 브랜치 보호는 우회가 훨씬 어렵습니다.
GitHub: Branch protection rules
main/master에 대해Require a pull request before mergingRequire status checks to passRestrict who can pushDo not allow force pushes활성화
운영 팁:
- 팀이
main에 직접 push하지 못하도록 하고 PR만 허용하면, rebase로 인한 강제 푸시가 발생할 여지가 크게 줄어듭니다. - 릴리즈 브랜치(
release/*)도 동일하게 보호하는 경우가 많습니다.
GitLab: Protected branches
Settings→Repository→Protected branchesAllowed to push를 Maintainer로 제한Allowed to merge만 Developer에 허용하는 패턴이 흔합니다.
추가로 GitLab은 프로젝트 설정에서 강제 푸시를 제한하는 옵션을 함께 제공하는 경우가 있으니, 보호 브랜치 설정과 같이 확인하세요.
2) 그래도 필요한 경우: --force-with-lease로 안전장치 걸기
팀 정책상 feature 브랜치에서는 rebase를 허용하더라도, 무조건 --force 대신 --force-with-lease를 쓰게 만드는 것이 좋습니다.
git push --force-with-lease origin feature/my-branch
--force-with-lease는 "내가 마지막으로 봤던 원격 상태"와 현재 원격 상태가 같을 때만 강제 푸시를 허용합니다. 즉,
- 내가 rebase 하는 동안 다른 사람이 원격 브랜치에 커밋을 추가했다면
- 내 강제 푸시가 그 커밋을 덮어쓰게 되므로
이 옵션이 푸시를 거절해 사고를 막습니다.
권장 별칭(alias)까지 강제하면 더 좋습니다.
git config --global alias.pf "push --force-with-lease"
# 사용: git pf origin feature/my-branch
3) 로컬에서 실수 줄이기: pre-push 훅으로 --force 차단
서버에서 막는 게 최선이지만, 로컬에서도 1차 방어막을 둘 수 있습니다.
아래는 --force 푸시를 감지하면 실패시키는 간단한 pre-push 훅 예시입니다.
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
# 강제 푸시를 감지해 차단하는 단순 훅
# 주의: 훅은 로컬에서만 동작하며 우회 가능
case "$GIT_PUSH_OPTION_COUNT" in
"") ;; # 구버전 git에서는 옵션 전달이 없을 수 있음
esac
# 사용자가 명시적으로 FORCE_PUSH_ALLOWED=1을 준 경우만 허용
if [ "$FORCE_PUSH_ALLOWED" = "1" ]; then
exit 0
fi
# push 명령행에서 --force를 직접 감지하기는 어렵기 때문에
# 정책적으로 '강제 푸시는 반드시 git pf(alias)로만' 같은 룰을 두고
# 원격 브랜치 보호와 함께 운영하는 편이 안정적입니다.
echo "[blocked] Force push is not allowed by local policy."
echo "Use PR merge, or if absolutely needed, coordinate and use --force-with-lease."
exit 1
EOF
chmod +x .git/hooks/pre-push
실무에서는 훅만으로 완벽히 막기 어렵기 때문에, 브랜치 보호 + --force-with-lease 표준화가 핵심입니다.
4) rebase를 안전하게 하는 습관: 원격 추적과 백업 브랜치
rebase를 해야 한다면, 다음 습관이 사고를 크게 줄입니다.
(1) rebase 전 원격 최신화
git fetch origin
(2) 현재 상태를 백업 브랜치로 남기기
git switch feature/my-branch
git branch backup/feature-my-branch-before-rebase
이 백업 브랜치는 나중에 되돌릴 때 매우 강력한 보험이 됩니다.
(3) rebase 후 푸시는 --force-with-lease
git rebase origin/main
git push --force-with-lease origin feature/my-branch
5) 이미 강제 푸시를 해버렸다: 안전하게 되돌리는 3가지 시나리오
강제 푸시 사고는 "원격에 있던 커밋이 안 보인다"는 형태로 나타납니다. 하지만 Git은 보통 즉시 삭제하지 않고, 참조만 끊긴 상태일 가능성이 큽니다.
시나리오 A: 내 로컬에 이전 커밋이 남아 있다면 reflog로 복구
가장 빠른 복구는 reflog입니다. 브랜치 포인터가 어디를 가리켰는지 기록이 남습니다.
git reflog --date=iso
출력에서 강제 푸시 이전의 커밋 해시를 찾습니다(예: abc1234). 그 다음 두 가지 방식 중 하나를 택합니다.
방법 1) 브랜치를 해당 커밋으로 되돌리고 다시 푸시
git switch feature/my-branch
git reset --hard abc1234
git push --force-with-lease origin feature/my-branch
방법 2) "되돌리기용" 새 브랜치를 만들어 PR로 복구
공유 브랜치에서 다시 강제 푸시하는 것이 부담이라면, 복구 브랜치를 만들어 PR로 되살리는 방법이 더 안전합니다.
git switch -c restore/feature-my-branch abc1234
git push origin restore/feature-my-branch
그리고 restore/*를 feature/*로 머지하는 PR을 만들어 팀 합의 하에 복구합니다.
시나리오 B: 내 로컬엔 없고, 다른 팀원 로컬에만 있다면
다른 팀원이 아직 이전 히스토리를 가지고 있을 수 있습니다.
- 팀원에게
git reflog로 이전 커밋 해시를 찾아달라고 요청 - 해당 커밋에서 복구 브랜치 생성 후 원격에 푸시
# 팀원 로컬에서
git switch -c restore/feature-from-teammate <old-commit-hash>
git push origin restore/feature-from-teammate
이후 PR로 복구하면, 추가 강제 푸시 없이도 안전하게 되살릴 수 있습니다.
시나리오 C: 원격에도 흔적이 남아 있다면(호스팅 서비스 기능 활용)
GitHub/GitLab은 UI에서 브랜치/커밋 탐색, 삭제된 브랜치 복원, PR의 커밋 목록 등으로 "사라진 것처럼 보이는 커밋"을 찾을 수 있는 경우가 많습니다.
- 최근 PR의 커밋 탭
- 브랜치 목록의 "최근 업데이트" 정렬
- 서버 측 로그/감사 로그(엔터프라이즈 플랜에서 제공되는 경우)
커밋 해시만 확보하면, 로컬에서 다음처럼 복구 브랜치를 만들 수 있습니다.
git fetch origin
git switch -c restore/from-remote <commit-hash>
git push origin restore/from-remote
6) "되돌리기"에서 reset과 revert를 구분하자
git reset --hard는 히스토리를 되감는 방식이라, 원격 반영에는 보통 강제 푸시가 필요합니다.git revert는 새 커밋으로 되돌림을 기록합니다. 공유 브랜치에서 더 안전할 때가 많습니다.
다만 rebase/강제 푸시 사고는 "잘못된 변경을 되돌리는" 문제라기보다 "사라진 커밋을 복원"하는 문제인 경우가 많아, 이때는 revert보다 reflog 기반 복구가 적합합니다.
7) 팀 운영 체크리스트: 사고를 구조적으로 줄이는 규칙
main/release/*는 무조건 보호 브랜치로 설정하고 강제 푸시 금지- feature 브랜치에서 rebase를 허용하더라도 푸시는
--force-with-lease만 허용 - rebase 전
git fetch origin을 습관화 - 큰 rebase 전에는 백업 브랜치 생성
- 사고 발생 시에는
- 즉시 추가 푸시 중단
reflog로 커밋 해시 확보- 복구 브랜치로 PR 복구를 우선 고려
운영 환경에서 "실수"는 항상 발생합니다. 중요한 것은 개인의 주의력에만 기대지 않고, 권한/정책/명령 습관을 통해 실수를 실패 가능한 형태로 만들고, 복구 경로를 팀 표준으로 갖추는 것입니다.
추가로, 배포 파이프라인에서 브랜치 정책이 느슨하면 운영 장애로 이어질 수 있습니다. 운영 이슈 대응 관점은 EKS ALB Ingress 504인데 Pod는 정상일 때처럼 "겉보기 정상"에 속지 않는 체크리스트 접근과도 닮아 있습니다. Git 히스토리도 마찬가지로, 표면적인 브랜치 상태만 보지 말고 reflog/원격 보호 규칙까지 포함해 관찰 가능성을 확보하는 게 핵심입니다.
부록: 자주 쓰는 명령 모음
# 원격 최신화
git fetch origin
# rebase 전 백업 브랜치
git branch backup/my-branch-before-rebase
# 안전 강제 푸시
git push --force-with-lease origin my-branch
# 브랜치 포인터 이동 기록 확인
git reflog
# 특정 커밋에서 복구 브랜치 생성
git switch -c restore/my-branch <commit-hash>