git push --force vs --force-with-lease: 남의 커밋을 지우지 않는 강제 푸시
git commit --amend나 git rebase로 이미 원격에 올린 커밋을 고치면 로컬과 원격의 기록이 갈라진다. 이때 일반 git push는 거절되고 강제 푸시가 필요하다.
git push --forcegit push --force-with-lease
둘 다 "원격 브랜치를 내 로컬 브랜치로 덮어쓴다"는 결과는 같다. 차이는 덮어쓰기 전에 무엇을 확인하느냐다. 실제 실행 결과로 비교하고, --force-with-lease에 남아 있는 함정과 보완책인 --force-if-includes까지 정리했다.
요약
| 옵션 | 덮어쓰기 전 확인 | 그사이 동료가 푸시한 커밋 |
|---|---|---|
--force | 없음 | 조용히 사라짐 |
--force-with-lease | 원격이 내가 마지막으로 본 상태(원격 추적 브랜치)와 같은지 | 거절. 단, 그사이 fetch했다면 사라질 수 있음 |
--force-with-lease --force-if-includes | 위 조건 + 원격 추적 브랜치의 커밋이 내 로컬 기록에 반영됐는지 | 거절(fetch만 하고 반영 안 한 경우 포함) |
강제 푸시가 필요하면 --force-with-lease --force-if-includes를 쓴다.
실험 환경
로컬에 bare 저장소(원격 역할) 하나와 클론 두 개(나, 동료)를 만들었다.
git init --bare -b main remote.git
git clone remote.git me
git clone remote.git teammate
- 내가 커밋
A를main에 푸시한다. - 동료가
A위에B를 만들어 푸시한다. 나는 아직 모른다. - 나는 로컬에서
A를 amend해A (amended)로 바꾼다. - 강제 푸시를 시도한다.
원격 main: A ── B (teammate)
내 로컬 main: A (amended)
강제 푸시가 그대로 성공하면 동료의 B는 원격에서 사라진다. (Git 2.54 기준, 저장소 경로는 remote.git으로 줄여 표기)
--force: 확인 없이 덮어쓰기
git push --force origin main
To remote.git
+ 0e5bd20...da1a271 main -> main (forced update)
git log --oneline origin/main
# da1a271 A (amended)
경고도 에러도 없이 성공하고, 동료의 B는 흔적 없이 사라졌다. --force는 원격이 지금 어떤 상태든 내 브랜치로 바꾼다. 동료는 다음 pull에서야 커밋이 없어진 것을 알게 된다.
--force-with-lease: 내가 본 상태일 때만
git push --force-with-lease origin main
To remote.git
! [rejected] main -> main (stale info)
error: failed to push some refs to 'remote.git'
stale info는 "네가 알고 있는 원격 정보가 오래됐다"는 뜻이다.
동작 원리
푸시 전에 두 값을 비교한다.
- 기대값: 로컬의 원격 추적 브랜치
origin/main이 가리키는 커밋. 마지막으로fetch·pull·push할 때 본 원격 상태다. - 실제값: 지금 원격 서버의
main.
같으면 "내가 본 이후 아무도 푸시하지 않았다"는 뜻이므로 덮어쓰고, 다르면 거절한다. 이름의 lease(임대)도 "내가 본 상태를 빌려 쓰고 있으니 그대로일 때만 덮어쓴다"는 의미다. 위 실험에서 내 origin/main은 A, 원격 main은 B라서 거절되었다.
기대값 직접 지정
# 원격 main이 정확히 0e5bd20일 때만 덮어쓰기
git push --force-with-lease=main:0e5bd20 origin main
스크립트나 CI처럼 기준 커밋이 분명할 때 유용하다. 원격 추적 브랜치를 쓰지 않으므로 아래 fetch 함정의 영향을 받지 않는다. 대신 --force-if-includes와 함께 써도 추가 확인은 일어나지 않는다.
함정: fetch가 lease를 갱신한다
--force-with-lease의 기대값은 원격 추적 브랜치인데, 이것은 git fetch만 해도 최신 원격 상태로 바뀐다.
git fetch
git push --force-with-lease origin main
To remote.git
+ 0e5bd20...da1a271 main -> main (forced update)
이번에는 성공했고 동료의 B가 다시 사라졌다. fetch로 내 origin/main이 B가 되어 기대값과 실제값이 같아졌기 때문이다. 하지만 나는 B를 받아오기만 했을 뿐 내 브랜치에 반영하지 않았다.
이런 상황은 생각보다 흔하다.
- VS Code, IntelliJ, GitKraken 같은 도구는 백그라운드에서 주기적으로
git fetch를 실행하기도 한다. - 다른 브랜치를 보려고 습관적으로
git fetch를 칠 수도 있다.
그러면 lease가 조용히 갱신되어 --force-with-lease가 --force와 똑같이 동작한다.
--force-if-includes: 받아온 커밋을 반영했는지까지
Git 2.30에서 추가된 --force-if-includes를 --force-with-lease와 함께 쓰면 조건이 하나 더 붙는다.
원격 추적 브랜치가 가리키는 커밋이 내 로컬 브랜치의 reflog 항목 중 하나에서 도달 가능한지 확인한다.
즉 "원격에서 받아온 커밋을 실제로 내 브랜치에 반영(merge·rebase)한 적이 있는가"를 본다. fetch만 하고 반영하지 않았다면 거절된다.
git fetch
git push --force-with-lease --force-if-includes origin main
To remote.git
! [rejected] main -> main (remote ref updated since checkout)
error: failed to push some refs to 'remote.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
git pull --rebase 등으로 동료의 B를 내 브랜치에 반영한 뒤 다시 푸시하면 된다.
결과 비교
| 명령 | fetch 안 함 | fetch 후 |
|---|---|---|
--force | 성공, B 유실 | 성공, B 유실 |
--force-with-lease | 거절 (stale info) | 성공, B 유실 |
--force-with-lease --force-if-includes | 거절 (stale info) | 거절 (remote ref updated since checkout) |
실무 권장 설정
1. 두 옵션을 함께 쓰기
git push --force-with-lease --force-if-includes origin <branch>
--force-if-includes를 기본값으로 켤 수 있다. 이 옵션은 기대값을 지정하지 않은 --force-with-lease와 함께일 때만 동작하므로, 설정을 켜도 --force는 여전히 아무것도 확인하지 않는다.
git config --global push.useForceIfIncludes true
git config --global alias.pushf "push --force-with-lease --force-if-includes"
# 사용: git pushf origin feat/my-branch
2. --force는 쓰지 않는 습관
--force가 정말 필요한 경우는 드물다(브랜치를 통째로 교체하기로 팀과 합의한 경우 정도). refspec 앞에 +를 붙이는 git push origin +main도 --force와 같아서 원격 상태를 확인하지 않는다.
3. 공용 브랜치는 서버에서 막기
클라이언트 옵션은 결국 개인 습관에 달려 있다. main, develop 같은 공용 브랜치는 GitHub Branch protection rules(또는 Rulesets)로 강제 푸시 자체를 금지하는 것이 가장 확실하다. 강제 푸시는 개인 feature 브랜치에서 amend·rebase 결과를 올릴 때로 한정한다.
정리
--force는 원격 상태를 확인하지 않는다. 그사이 누가 푸시한 커밋은 조용히 사라진다.--force-with-lease는 원격이 내가 마지막으로 본 상태일 때만 덮어쓴다. 하지만 기준이 원격 추적 브랜치라fetch만 해도 보호가 풀린다.--force-if-includes를 함께 쓰면 원격 커밋을 실제로 반영했는지까지 확인해 이 빈틈을 막는다.