Git 원격 저장소: push·pull·fetch와 거부된 push 해결법

로컬 bare 저장소로 오프라인 재현. fetch와 pull의 차이, non-fast-forward로 거부된 push, upstream 추적, pull.rebase 선택법, clone 크기 실측까지.

시리즈: Git 강좌 3/6 (중급). 커밋과 브랜치는 익숙하지만(1·2강) push와 pull은 주문처럼 치고 있는 분을 위한 글입니다. origin/main이 무엇인지, push가 왜 거부되는지, fetch와 pull이 어떻게 다른지, pull 설정은 무엇을 골라야 하는지 알게 됩니다.

방금 git push가 거부된 분, git pull을 했더니 아무도 원하지 않은 병합 커밋이 생긴 분, "강제 푸시는 절대 금지"라는 말만 듣고 이유는 못 들은 분을 위한 글입니다. 모든 예제는 오프라인으로 돌아갑니다. "서버"는 로컬 폴더에 만든 bare 저장소이고, clone 두 개 alice와 bob이 같은 팀 동료 두 명 역할을 합니다. 출력되는 메시지는 인증 부분만 빼면 GitHub 같은 호스팅 서비스에서 보는 것과 같습니다.

실행 환경은 Git 2.54.0(Apple Git-157), Apple Silicon Mac(M1 Pro), 데모 신원, 임시 폴더뿐입니다. 출력 속 로컬 경로는 ~/demo/…로 줄였습니다.

마음껏 망가뜨려도 되는 원격 만들기

원격(remote)은 내 저장소가 이름으로 알고 있는 다른 저장소입니다. 보통 이름은 origin입니다. bare 저장소는 작업 트리 없이 .git 내용물만 있는 저장소입니다. 서버에서는 아무도 파일을 직접 고치지 않으니 bare를 씁니다.

git init --bare -b main remote.git
git clone remote.git alice
Initialized empty Git repository in ~/demo/rm3/remote.git/
Cloning into 'alice'...
warning: You appear to have cloned an empty repository.
done.

이 경고는 무해합니다. Alice가 커밋하고 push합니다.

cd alice
printf 'orders = []\n' > shop.py; git add shop.py; git commit -qm "Start shop"
git push
git branch -vv
To ~/demo/rm3/remote.git
 * [new branch]      main -> main
* main edd9011 [origin/main] Start shop

[origin/main]이 main의 upstream(추적 브랜치)입니다. push, pull, status가 기본으로 비교하는 원격 브랜치이고, git clone이 설정해 줬습니다.

왜 일반(bare가 아닌) 저장소에는 push하지 않을까요? 해 보니 서버 쪽이 거부했습니다.

remote: error: refusing to update checked out branch: refs/heads/main
remote: error: By default, updating the current branch in a non-bare repository
remote: is denied, because it will make the index and work tree inconsistent
...
 ! [remote rejected] main -> main (branch is currently checked out)

upstream 추적과 새 브랜치의 첫 push

새 브랜치에는 설정해 주기 전까지 upstream이 없습니다.

git switch -c feature/cart
# ... 커밋 ...
git push
fatal: The current branch feature/cart has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/cart

To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.

안내대로 git push -u origin feature/cart를 하거나, push.autoSetupRemote=true를 한 번 설정합니다(Git 2.37 이상). 설정 후에는 같은 git push가 이렇게 출력했습니다.

 * [new branch]      feature/cart -> feature/cart
branch 'feature/cart' set up to track 'origin/feature/cart'.

또 하나의 단골은 없는 브랜치 이름을 push하는 것입니다. 오래된 자료는 대부분 master라고 쓰지만 새 저장소는 main을 씁니다.

$ git push origin master
error: src refspec master does not match any
error: failed to push some refs to '~/demo/rm3/remote.git'

"src refspec … does not match any"는 그 이름의 로컬 브랜치가 없다는 뜻입니다. git branch로 확인하세요.

fetch와 pull: origin/main의 정체

origin/main은 원격 추적 브랜치입니다. 마지막으로 origin과 통신했을 때 main이 어디 있었는지 로컬에 적어 둔 읽기 전용 메모입니다. fetch(또는 먼저 fetch를 하는 pull)를 할 때만 움직입니다.

Alice가 커밋을 push했습니다. Bob은 아직 fetch하지 않았습니다.

cd ../bob
git status
On branch main
Your branch is up to date with 'origin/main'.

여기가 함정입니다. "up to date"는 서버가 아니라 Bob 로컬의 낡은 origin/main과 비교한 결과입니다. fetch하면 달라집니다.

git fetch
git status -sb
git log --oneline main..origin/main
From ~/demo/rm3/remote
   edd9011..8500a27  main       -> origin/main
## main...origin/main [behind 1]
8500a27 Add TAX

fetch는 커밋을 내려받고 origin/main을 옮겼을 뿐, Bob의 main과 파일은 그대로입니다. 먼저 살펴보고(git log main..origin/main, git diff main origin/main) git merge로 합치면 됩니다. 이번에는 fast-forward였습니다. git pull은 git fetch와 이 합치기 단계를 이어서 실행하는 명령일 뿐입니다.

그림: 서버에 있는 원격 main은 git fetch로 내 저장소의 원격 추적 브랜치 origin/main에 내려받아진다. origin/main에서 git merge 또는 git rebase로 내 로컬 main과 작업 파일에 반영한다. git pull은 이 두 단계를 한 번에 한다. git push는 내 main을 서버의 main으로 보내며, 서버에 내가 없는 커밋이 있으면 거부된다.
git fetch는 origin/main(원격 위치를 기억한 로컬 사본)만 갱신한다. git pull은 fetch에 내 브랜치로의 병합 또는 리베이스를 더한 것이다.
하고 싶은 일 명령 내 브랜치·파일을 건드리나
동료가 push한 것을 안전하게 보기 git fetch 아니오
합치기 전에 비교 git log main..origin/main, git diff main origin/main 아니오
동료 작업을 내 브랜치로 git pull (fetch + merge/rebase) 예
fast-forward일 때만 받기 git pull --ff-only 예, 아니면 실패
지금 서버 상태 확인 git ls-remote origin 아니오

거부된 push와 합치는 방법

이번에는 두 사람이 동시에 일합니다. Alice가 배송비를 추가해 먼저 push했습니다. Bob은 config.py를 추가해 커밋하고 push합니다.

$ git push
To ~/demo/rm3/remote.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '~/demo/rm3/remote.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Bob의 push를 받아들이면 서버의 main에서 Alice의 커밋이 빠지기 때문에 Git이 거부합니다. pull 설정이 없는 상태로 Bob이 git pull을 실행하면:

From ~/demo/rm3/remote
   8500a27..b2e3cbf  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
...
fatal: Need to specify how to reconcile divergent branches.

처음 두 줄을 보세요. fetch는 이미 끝났습니다. 이 상태에서 다시 push하면 이유가 (fetch first)에서 (non-fast-forward)로 바뀝니다. 이제 Git이 원격 커밋을 알고 있고, Bob의 브랜치가 뒤처졌다고 판단하기 때문입니다. git status -sb는 [ahead 1, behind 1]이었습니다.

Bob의 저장소를 두 벌 복사해서 두 가지 답을 모두 해 봤습니다.

병합(git pull --no-rebase):

*   c70294d Merge branch 'main' of ~/demo/rm3/remote
|\  
| * b2e3cbf Add shipping fee
* | a556d35 Add currency
|/  
* 8500a27 Add TAX

리베이스(git pull --rebase):

Successfully rebased and updated refs/heads/main.
* 50a55ed Add currency
* b2e3cbf Add shipping fee
* 8500a27 Add TAX
* edd9011 Start shop

리베이스에서는 Bob의 커밋이 Alice의 커밋 위에 다시 쌓이면서 ID가 바뀌었습니다(a556d35 → 50a55ed). 히스토리는 일직선이 되고 push도 통과했습니다(b2e3cbf..50a55ed main -> main). 병합 쪽도 동작은 하지만, pull할 때마다 아무 의미 없는 "Merge branch 'main' of …" 커밋이 쌓입니다.

판단도: fetch first 또는 non-fast-forward로 push가 거부됨. 로컬 커밋을 아직 공유하지 않았다면 git pull --rebase(일직선 히스토리, 커밋 ID가 새로 바뀜). 이미 다른 곳에 공유했다면 git pull --no-rebase(병합 커밋 추가). 그 뒤 다시 push한다. 공유 브랜치에 git push --force는 쓰지 않는다.
push가 거부되면 합치는 방법을 고른다. 나만 가진 커밋이라면 리베이스가 보통의 답이다.
설정 갈라졌을 때 git pull의 동작 어울리는 경우
pull.rebase true 내 로컬 커밋을 위에 다시 쌓음 대부분의 팀. 공개 전 작업을 깔끔하게
pull.rebase false 병합 커밋을 만듦 로컬 커밋을 이미 다른 곳에 공유한 경우
pull.ff only fast-forward가 아니면 거부 직접 커밋하지 않는 브랜치(배포용 checkout 등)
설정 없음(2.54.0) fatal: Need to specify how to reconcile divergent branches. 선택을 강제

리베이스가 다시 쓰는 것은 아직 push하지 않은 내 커밋뿐이라 안전합니다. 리베이스의 자세한 내용과, 남이 이미 가진 커밋을 리베이스하면 안 되는 이유는 4강에서 다룹니다.

흔한 실수: "일단 강제 푸시"

실제로 일어나는 일. Alice의 clone은 아직 origin/main이 b2e3cbf라고 알고 있습니다(Bob의 push를 fetch하지 않음). Alice가 마지막 커밋을 amend하고 push합니다. 일반 push가 거부되니 force에 손이 갑니다.

--force-with-lease는 Alice가 마지막으로 본 상태가 서버에 아직 그대로인지 먼저 확인합니다.

$ git push --force-with-lease
 ! [rejected]        main -> main (stale info)
error: failed to push some refs to '~/demo/rm3/remote.git'

그냥 --force에는 아무 확인이 없습니다.

$ git push --force
 + 50a55ed...56ed907 main -> main (forced update)

Bob의 커밋 50a55ed가 서버의 main에서 사라졌습니다. 다음에 Bob이 pull하면 + 50a55ed...56ed907 main -> origin/main (forced update)와 함께 갈라짐 오류가 나오고, Bob의 작업은 그의 로컬 clone에만 남습니다(실험 뒤 서버는 사본으로 복원했습니다).

해결: 공유 브랜치에는 강제 푸시하지 않습니다. 나만 쓰는 기능 브랜치를 리베이스한 뒤 다시 써야 할 때는 git push --force-with-lease를 씁니다. 바로 전에 git fetch를 했다면 들어온 내용을 먼저 확인하세요(git log HEAD..origin/<브랜치>). fetch가 lease가 비교하는 값을 갱신해서 보호가 사라지기 때문입니다.

실측: 전체·얕은(shallow)·블롭 없는(blobless) clone

원격에는 "얼마나 내려받느냐"는 문제도 있습니다. CSV 파일 하나에 커밋마다 20줄씩 추가하는 커밋 2,000개 저장소를 만들었습니다(난수 시드 42 고정, 최종 40,000줄·984,486바이트, git gc 후 서버 쪽 팩 10.55MiB). 하드 링크가 아닌 실제 팩 전송이 일어나도록 file://로 clone했고, 각 3회, Apple Silicon Mac(M1 Pro), Git 2.54.0에서 쟀습니다.

clone 종류 회차별(ms) .git 크기 히스토리 커밋 수
전체 1169.4 / 1124.1 / 1221.5 10,936 KB 2,000
--depth 1(shallow) 128.1 / 129.6 / 140.9 608 KB 1
--filter=blob:none(blobless) 180.9 / 190.7 / 184.8 1,108 KB 2,000
막대그래프: 전체 clone 1,169ms, --depth 1 얕은 clone 130ms, --filter=blob:none 블롭 없는 clone 185ms.
커밋 2,000개 저장소를 file://로 clone한 시간(각 3회 중앙값). .git 크기는 full 10,936KB, shallow 608KB, blobless 1,108KB. Git 2.54.0, Apple M1 Pro.

작은 clone에도 나중에 내는 비용이 있습니다.

  • blobless clone에서 git log -p -50은 첫 실행에 6,792.8ms가 걸렸습니다. 예전 버전 파일 50개를 그때그때 받아 오기 때문이고, .git은 25,644KB로 커졌습니다. 두 번째는 300.3ms였습니다.
  • shallow clone에서는 git log --oneline에 커밋이 하나만 나왔고 git rev-parse --is-shallow-repository는 true였습니다. git fetch --unshallow는 1,241.9ms가 걸렸고 .git은 12,104KB로, 사실상 전체 clone과 같은 비용이 됐습니다.
  • 새것이 없을 때 git fetch는 5회 중앙값 41.5ms였습니다.

정리하면: 커밋 하나만 빌드하는 CI는 --depth 1, 히스토리는 필요하지만 예전 파일까지는 필요 없는 개발자는 blobless, 매일 쓰는 작업용 clone은 전체 clone.

솔직한 한계와 버전 메모

  • 모두 Git 2.54.0과 로컬 bare 저장소로 실행했습니다. 호스팅 서비스에는 인증, 브랜치 보호, 자체 거부 메시지(보호 브랜치가 강제 푸시를 거부하는 등)가 더해지는데, 여기서는 실행하지 않았습니다.
  • push.autoSetupRemote는 Git 2.37에 추가됐습니다. 갈라진 상태의 pull이 오류가 된 것은 2.33/2.34 무렵이며(그 전에는 경고 후 병합), 문구는 2.54.0에서만 확인했습니다.
  • --force-with-lease는 Git 1.8.5부터 있습니다. 기대값을 명시하지 않으면 로컬의 origin/main과 비교하므로, 직전 git fetch가 보호를 약하게 만듭니다.
  • 부분 clone(--filter)은 서버 지원이 필요해서 로컬 bare 저장소에 uploadpack.allowFilter true를 설정했습니다. file:// 시간에는 네트워크 지연이 빠져 있으니 실제 차이는 보통 더 큽니다.

연습 문제

  1. git status는 "Your branch is up to date with 'origin/main'"이라고 하는데, 동료는 10분 전에 push했다고 합니다. 내 브랜치를 바꾸지 않고 동료의 커밋을 보려면?
  2. push가 (non-fast-forward)로 거부됐습니다. 로컬에는 한 번도 push하지 않은 커밋이 두 개 있습니다. 히스토리를 일직선으로 유지하면서 원격 작업을 합치는 명령은?

정답

  1. git fetch 다음 git log --oneline main..origin/main. status는 마지막으로 fetch한 origin/main하고만 비교합니다.
  2. git pull --rebase(또는 한 번만 git config pull.rebase true) 후 git push. 두 커밋의 ID는 바뀌지만, 다른 누구도 갖고 있지 않으니 괜찮습니다.

이전 강좌: Git 강좌 2: 브랜치·병합·충돌 · 다음 강좌: Git 강좌 4: rebase·cherry-pick과 히스토리 · 시리즈 처음: Git 강좌 1 · 신입 개발자 학습 로드맵으로 돌아가기.

자주 묻는 질문

git fetch와 git pull은 무엇이 다른가요?

git fetch는 새 커밋을 내려받아 origin/main 같은 원격 추적 브랜치만 갱신하고, 내 브랜치나 파일은 건드리지 않습니다. git pull은 fetch한 다음 origin/main을 내 브랜치로 병합하거나 리베이스합니다. 실험에서 원격에 새 커밋이 있어도 fetch 전 git status는 up to date라고 했고, fetch 뒤에야 behind 1이 됐습니다.

push가 fetch first나 non-fast-forward로 거부되는 이유는?

원격 브랜치에 내가 갖고 있지 않은 커밋이 있어서, 그대로 받아들이면 그 커밋이 사라지기 때문입니다. git pull --rebase(병합을 원하면 --no-rebase)로 먼저 합친 뒤 다시 push하세요. 공유 브랜치에 --force는 쓰지 않습니다. --force-with-lease는 원격이 움직였기 때문에 (stale info)로 덮어쓰기를 거부했습니다.

fatal: Need to specify how to reconcile divergent branches는 무슨 뜻인가요?

내 브랜치와 원격 브랜치 모두에 새 커밋이 있는데, 병합할지 리베이스할지 정해 주는 pull.rebase나 pull.ff 설정이 없다는 뜻입니다. 명령마다 git pull --rebase / --no-rebase / --ff-only를 주거나, git config pull.rebase true처럼 한 번 설정하세요. 이 오류가 나왔을 때 fetch는 이미 끝난 상태입니다.

  • #git
  • #중급
  • #원격 저장소
  • #push
  • #pull
  • #fetch

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑