Git: push 거부 — 원격에 없는 커밋이 있어 거절됨 (non-fast-forward)
안녕하세요, BlueByte입니다. git push를 실행했는데 커밋이 올라가는 대신 Git이 ! [rejected] main -> main (fetch first)와, 원격에 당신에게 없는 커밋이 있다는 문단으로 멈춥니다. 깨진 것도, 잃어버린 커밋도 없습니다 — Git이 보이는데 당신에겐 없는 히스토리를 덮어쓰기를 거부하는 것뿐입니다. 증상은 push가 Updates were rejected로 끝나며 거절 줄에 (fetch first)나 (non-fast-forward)가 붙는 것입니다. 오늘은 이 두 변형의 뜻, push가 거부된 이유, 두 히스토리를 안전하게 합치는 법, 재발 예방까지 하나씩 짚어보겠습니다.
"(fetch first)"와 "(non-fast-forward)"가 알려주는 것
전체 거절 메시지는 이렇습니다:
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:acme/app.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.Git은 당신의 브랜치가 원격 브랜치의 곧은 연장선일 때 — fast-forward일 때 — 만 push를 받아들입니다. 원격이 앞서 나가면 당신 커밋은 더 이상 원격 tip의 직계 자손이 아니므로, Git은 원격 커밋을 버리는 대신 거부합니다. 두 힌트 단어가 어느 경우인지 알려줍니다: (fetch first)는 당신 로컬 브랜치가 원격의 새 커밋을 아직 못 봤다는 뜻이고, (non-fast-forward)는 두 히스토리가 실제로 갈라져 양쪽 모두 상대에게 없는 커밋을 가졌다는 뜻입니다.
거부된 push를 만드는 두 상황
non-fast-forward 거절은 다음 중 하나에서 옵니다:
- 마지막 동기화 이후 누군가 push함. 동료나 CI 작업이 같은 브랜치에 커밋을 더했습니다. 당신 히스토리는 틀린 게 아니라 뒤처진 것 — 따라잡으면 됩니다.
- 로컬 히스토리를 다시 씀.
git rebase,git commit --amend,git reset이 이미 push한 커밋을 바꿔, 당신 브랜치와 원격이 과거에 대해 서로 어긋납니다.
첫 번째가 흔하며 깔끔한 해결책이 있습니다. 두 번째는 조심해야 합니다 — 안전한 해결과 위험한 해결이 거의 똑같이 생겼기 때문입니다.
손대기 전에 히스토리가 얼마나 갈라졌는지 보기
아직 아무것도 강제하지 마세요 — 먼저 원격 상태를 가져와 간극을 재세요:
git fetch
git statusYour branch and 'origin/main' have diverged,
and have 1 and 2 different commits each, respectively.이 줄이 진단 전부입니다: 당신에겐 원격에 없는 커밋 1개, 원격엔 당신에게 없는 커밋 2개가 있습니다. 나란히 보려면:
git log --oneline --graph --left-right HEAD...origin/main< 표시는 당신 커밋, >는 원격 커밋입니다. 모두 >이고 <가 없으면 단순히 뒤처진 것 — 그냥 git pull이 fast-forward합니다. 둘 다 나오면 갈라진 것이라 merge나 rebase를 만들게 됩니다.
흔한 해결: 원격 작업을 먼저 들여오고 push하기
누군가 앞서 push한 일상적 경우엔 그들의 커밋을 가져와 당신 것을 그 위에 얹고 push하세요:
git pull --rebase
git pushSuccessfully rebased and updated refs/heads/main.
...
To github.com:acme/app.git
3f2a1b0..9c8d7e6 main -> maingit pull --rebase는 원격 커밋을 가져와 당신 커밋을 그 뒤에 다시 얹으므로 히스토리가 선형으로 남고 push가 이제 fast-forward합니다. 그냥 git pull(merge)도 되지만 merge 커밋이 남습니다. rebase가 충돌을 보고하면 파일을 해결하고 git add한 뒤 git rebase --continue를 실행하세요.
히스토리를 일부러 다시 썼다면: --force-with-lease, 절대 --force는 금물
리뷰 전 지저분한 브랜치를 squash하는 식으로 이미 push한 자기 커밋을 의도적으로 다시 써서 갈라진 경우라면, 그것을 도로 pull하면 정리가 되돌아갑니다. 이때는 원격을 덮어쓰되 안전장치를 두세요:
git push --force-with-lease--force-with-lease는 원격이 당신이 마지막으로 본 위치를 여전히 가리킬 때만 덮어씁니다 — 그사이 동료가 push했다면 그 커밋을 지우는 대신 push가 실패합니다. 그냥 git push --force는 그 검사를 건너뛰어 남이 push한 작업을 조용히 지울 수 있으니, 매번 --force-with-lease를 쓰고, main 같은 공유 브랜치는 합의 없이 절대 강제하지 마세요.
실제 사례: 작업 중 동료가 push했을 때
수정을 마치고 커밋한 뒤 git push가 (fetch first)와 Updates were rejected because the remote contains work that you do not have로 거부됩니다. git fetch와 git status를 실행하니 브랜치가 1개와 1개로 갈라졌다고 나옵니다. 아무것도 다시 쓰지 않았으니 흔한 경우입니다: git pull --rebase를 실행하면 Git이 당신의 커밋 1개를 동료 커밋 위에 다시 얹고 git push가 성공합니다 — 거절 줄이 사라지고 두 커밋이 순서대로 원격에 올라갑니다.
push가 실제로 도착했는지 확인
로컬과 원격이 일치하는지 확인하세요:
git statusYour branch is up to date with 'origin/main'.
nothing to commit, working tree cleanup to date with 'origin/main'이면 push가 깔끔하게 fast-forward한 것입니다. git log --oneline -3은 원격 커밋과 나란히 당신 커밋을 tip에 보여야 하고, 다음 push에서 (fetch first)가 없어야 합니다.
거부된 push에 놀라지 않으려면
작업을 시작하기 전과 push하기 전에 fetch해서 원격보다 뒤처지는 일을 줄이세요. git config --global pull.rebase true를 설정하면 git pull이 기본으로 rebase해 merge 커밋을 흩뿌리지 않고 선형 히스토리를 지킵니다. 공유 브랜치에는 직접 push보다 pull request를 쓰고, 히스토리를 다시 써야 한다면 동료 작업을 덮지 않도록 항상 --force-with-lease를 쓰세요. 하루 일과 끝에 한 번 크게 push하기보다 작게 자주 push할수록 덜 갈라집니다.
protected-branch나 "src refspec" 거절과의 차이
non-fast-forward 거절은 히스토리 모양의 문제입니다 — 당신 브랜치가 원격의 자손이 아니고, fetch로 풀립니다. 비슷해 보이는 다른 두 거절은 그렇지 않습니다: protected branch hook declined나 GH006은 히스토리와 무관하게 서버 정책이 push를 막는 것 — pull request가 필요하고 아무리 pull해도 소용없습니다. error: src refspec main does not match any는 브랜치가 로컬에 아예 없다는 뜻 — 대개 오타이거나 아직 아무것도 커밋 안 한 빈 저장소입니다. 메시지가 "remote contains work"가 아니라 hook이나 refspec을 가리키면 non-fast-forward가 아니고 git pull로 고쳐지지 않습니다.
관련 질문
(fetch first)와 (non-fast-forward)의 차이는 무엇인가요?
둘 다 같은 부류의 거절입니다. (fetch first)는 당신 로컬 브랜치가 원격의 최신 커밋을 아직 못 본 것이고, (non-fast-forward)는 히스토리가 이미 갈라진 것입니다. git fetch와 git status로 어느 쪽인지 구분하세요.
그냥 git push --force로 넘어가도 되나요?
되긴 하지만 --force는 안전 검사를 건너뛰어 동료가 push한 커밋을 조용히 지울 수 있습니다. git push --force-with-lease를 쓰세요 — 원격이 당신이 마지막으로 본 위치일 때만 덮어쓰고, 아니면 실패합니다.
git pull --rebase에서 충돌이 납니다.
충돌 파일을 열어 마커를 해결하고 git add한 뒤 git rebase --continue를 실행하세요. 물러나 다시 생각하려면 git rebase --abort로 시작 지점으로 돌아갑니다.
통합에 merge와 rebase 중 무엇을 써야 하나요?
둘 다 유효합니다. rebase(git pull --rebase)는 선형 히스토리를 유지하고, merge(그냥 git pull)는 merge 커밋으로 정확한 형상을 보존합니다. 선형을 선호하면 git config --global pull.rebase true로 기본값을 정하세요.
거절이 'remote contains work'가 아니라 'protected branch'라는데 같은 건가요?
아닙니다. protected-branch 거절은 서버 정책입니다 — 아무리 최신이어도 push가 거부되고 pull해도 소용없습니다. 그 브랜치는 pull request를 여세요.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.