git push 사용법/팁

git push 사용법/팁

git push는 원격 저장소(remote repository)에 코드 변경분을 업로드하기 위해서 사용하는 Git 명령어 입니다.

git commit vs. git push

git commit 명령어는 로컬 저장소(local repository)에 코드 변경 이력을 남기기 위해서 사용됩니다. 여기서 로컬 저장소란 git clone 명령어를 통해서 내 컴퓨터에 복제해둔 원격 저장소의 복사본을 의미합니다. 따라서, git commit를 통해 로컬 저장소에 아무리 많은 코드 변경 이력을 남기더라도 원격 저장소에서는 알 길이 없습니다. 반드시 명시적으로 git push를 날려줘야, 그 동안 로컬 저장소에서 남겨놓은 코드 변경 이력들이 원격 저장소로 전송이 됩니다.

기본 사용법

git push 명령어는 기본적으로 원격 저장소명과 브랜치명을 인자로 받습니다.

$ git push <저장소명> <브랜치명>

예를 들어, my-feature라는 브랜치에 남겨놓은 코드 변경 이력을 origin라는 원격 저장소에 올리기 위한 git push 명령어는 다음과 같습니다.

$ git push origin my-feature

원격 저장소명은 git clone을 통해 저장소를 복제를 했다면 일반적으로 origin이며 git remote 명령어를 통해서 정확한 저장소명을 알아낼 수도 있습니다.

$ git remote
origin

인자 생략 하기 1

로컬에서 새롭게 만든 브랜치를 git push 명령어를 사용하여 원격으로 올리려고 하면 아래와 같은 오류를 접하게 됩니다.

$ git push
fatal: The current branch my-feature has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin my-feature

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

git push 명령어 뒤에 저장소명과 브랜치명을 명시해주지 않아서 발생하는 오류인데요. git push 명령어를 실행할 때 마다 매번 똑같은 저장소명과 브랜치명을 입력하는 게 귀찮게 느껴질 수 있습니다. 😫

이럴 경우, -u 옵션 또는 --set-upstream 사용하면 최초에 한 번만 저장소명과 브랜치명을 입력하고 그 이후에는 모든 인자를 생략할 수 있습니다.

예를 들어, 다음과 같이 저장소명과 브랜치명을 넘기면서 -u 옵션과 함께 git push 명령어를 날리면,

$ git push -u origin my-feature

그 이후에 커밋한 코드 변경분을 원격 저장소에 올릴 때는 인자없이 git push 명령어만 날리면 됩니다.

$ git commit -m "Change 1"
$ git commit -m "Change 2"
$ git commit -m "Change 3"
$ git push

인자 생략 하기 2

여러 브랜치를 넘나 들면서 작업을 하는 경우에는 최초에 한 번 인자를 넘기는 것도 귀찮게 느껴질 수 있습니다. 😅 대부분의 경우에는 로컬 저장소와 원격 저장소에서 동일한 브랜치 이름을 사용하기 때문에 항상 현재 브랜치를 기준으로 git push 명령어가 작동한다면 매우 편리할 것 같습니다.

이를 위해서는 약간의 설정이 필요한데요. 다음과 같이 git config 명령어를 사용해서 push.default 설정을 current로 설정해줍니다.

$ git config --global push.default current

자, 이제부터는 어느 브랜치에서 작업을 하든 git push만 날리면 원격 저장소에 동일한 브랜치로 코드 변경분이 업로드됩니다. 🤗

$ git push

그런데 push.default에는 current 말고도 여러 옵션이 있습니다. git push를 저장소명과 브랜치명 없이 실행했을 때 어떤 브랜치를 push할지 결정하는 설정인데요.

  • nothing — 항상 저장소명과 브랜치명을 명시해야 합니다. 가장 안전하지만 매번 타이핑해야 해서 불편합니다.
  • current — 현재 브랜치와 같은 이름의 원격 브랜치로 push합니다. tracking 설정이 없어도 바로 push할 수 있어서 편합니다.
  • upstream — 현재 브랜치가 tracking하는 원격 브랜치로 push합니다. 로컬과 원격 브랜치 이름이 달라도 됩니다.
  • simpleupstream과 비슷하지만, 로컬과 원격 브랜치 이름이 같을 때만 push합니다. Git 2.0부터 기본값입니다.
  • matching — 로컬과 원격에 같은 이름의 브랜치가 모두 있으면 전부 push합니다. Git 1.x의 기본값이었는데, 예를 들어 main, dev, feature 브랜치가 모두 원격에 있으면 세 개를 한꺼번에 push해버려서 위험합니다. 이 때문에 Git 2.0에서 기본값이 simple로 바뀌었습니다.

simple이 Git 기본값인 이유는 안전성과 편의성의 균형을 잡기 위해서입니다. tracking 설정도 필요하고 이름도 같아야 push가 되니까 실수를 방지할 수 있죠. 반면 current는 tracking 설정 없이도 바로 push할 수 있어서 편하지만, 원격 저장소가 여러 개일 때 의도하지 않은 곳에 push할 가능성이 있으니 이 점은 주의해야 합니다.

push.default와 remote.pushDefault

위에서 다룬 push.default는 “어떤 브랜치를 push할 것인가”를 결정하는 설정입니다. 이와 별도로 remote.pushDefault라는 설정도 있는데, 이쪽은 “어떤 원격 저장소에 push할 것인가”를 결정합니다.

git push  [어디로?]       [무엇을?]
             ↑                ↑
     remote.pushDefault   push.default
    (원격 저장소 선택)    (브랜치 선택)

원격 저장소가 origin 하나뿐인 프로젝트에서는 remote.pushDefault를 따로 설정할 필요가 없지만, fork 기반 워크플로우처럼 원격 저장소가 여러 개인 경우에는 알아두면 유용합니다.

remote.pushDefault에 대한 자세한 설명과 활용법은 git remote로 원격 저장소 관리하기를 참고하세요.

강제 푸시가 필요한 경우

로컬 저장소에서는 자유롭게 코드 변경 이력을 수정해도 내 컴퓨터 밖에 모르는 일이기 때문에 뭐라고 할 동료가 없습니다. 하지만 일단 원격 저장소에 코드 변경분을 올린 이후에는 더 이상 해당 코드 변경분은 순전히 나의 코드가 아니기 때문에 함부로 변경 이력을 수정하면 안 됩니다. 왜냐하면 동료가 해당 코드 변경분을 내려 받았는데, 내가 그 코드 변경 이력을 수정해서 다시 올리면, 그 동료에게 코드 충돌이 발생할 것이기 때문입니다. 😱

그렇기 때문에 git push로 원격 저장소에 올린 코드 변경분은 절대 덮어쓰지 않는 것이 원칙입니다만… 실무에서는 이미 push한 커밋을 수정해야 할 상황이 생기기도 합니다.

대표적인 경우를 몇 가지 살펴볼게요.

먼저 git commit --amend로 마지막 커밋 메시지를 수정하거나 빠뜨린 파일을 추가한 경우입니다. 이미 push한 커밋을 --amend로 고치면 커밋 해시가 바뀌기 때문에 일반적인 git push로는 올릴 수 없게 됩니다.

$ git commit --amend -m "수정된 커밋 메시지"
$ git push
To github.com:user/repo.git
 ! [rejected]        my-feature -> my-feature (non-fast-forward)
error: failed to push some refs to 'github.com:user/repo.git'

또한 git reset으로 커밋을 취소한 경우에도 마찬가지인데요. 로컬에서 커밋을 되돌리면 원격 저장소보다 커밋 이력이 뒤처지게 되어 push가 거부됩니다.

$ git reset --soft HEAD~2
$ git commit -m "두 커밋을 하나로 합침"
$ git push
 ! [rejected]        my-feature -> my-feature (non-fast-forward)

git rebase로 브랜치의 base를 변경하거나 커밋 이력을 정리한 경우도 있어요. main 브랜치의 최신 변경 사항을 반영하려고 rebase를 하면 커밋 해시가 전부 달라지게 되죠.

$ git rebase main
$ git push
 ! [rejected]        my-feature -> my-feature (non-fast-forward)

이런 상황에서는 모두 원격 저장소와 로컬 저장소의 이력이 어긋나면서 일반적인 push로는 올릴 수 없고, 강제 푸시(force push)가 필요합니다.

—force 옵션

--force 옵션(줄여서 -f)을 사용하면 원격 저장소의 이력을 로컬 저장소의 이력으로 강제로 덮어씁니다.

$ git push --force origin my-feature

-f라는 짧은 형태를 사용해도 동일합니다.

$ git push -f origin my-feature

강제 푸시가 성공하면 다음과 같은 출력을 볼 수 있는데요. + 기호와 (forced update)라는 메시지가 강제로 덮어쓰기가 일어났음을 알려줍니다.

$ git push -f origin my-feature
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 275 bytes | 275.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:user/repo.git
 + abc1234...def5678 my-feature -> my-feature (forced update)

--force 옵션은 강력하지만 위험할 수 있습니다. 원격 저장소에 있던 커밋을 완전히 다른 이력으로 바꿔치기하는 것이라서, 만약 그 사이에 동료가 같은 브랜치에 push한 커밋이 있었다면 그 커밋이 유실될 수 있습니다. 😱

예를 들어 이런 시나리오를 생각해봅시다.

  1. 내가 my-feature 브랜치에서 작업 후 push
  2. 동료가 같은 브랜치에 새 커밋을 push
  3. 내가 로컬에서 rebase 후 git push --force 실행
  4. 동료의 커밋이 사라짐 💥

이런 문제를 방지하기 위해 나온 것이 --force-with-lease 옵션입니다.

—force-with-lease 옵션

--force-with-lease는 “조건부 강제 푸시”라고 생각하면 됩니다. 원격 저장소의 상태가 내가 마지막으로 fetch한 시점과 동일할 때만 강제 푸시를 허용하고, 그 사이에 누군가 새로운 커밋을 올렸다면 push를 거부합니다.

$ git push --force-with-lease origin my-feature

내가 마지막으로 git fetch를 했을 때와 원격 브랜치 상태가 그대로라면 정상적으로 강제 푸시가 이루어집니다.

$ git push --force-with-lease origin my-feature
To github.com:user/repo.git
 + abc1234...def5678 my-feature -> my-feature (forced update)

반면에 그 사이에 동료가 커밋을 push한 상황이라면 다음과 같이 push가 거부되면서 안전하게 보호해줍니다.

$ git push --force-with-lease origin my-feature
To github.com:user/repo.git
 ! [rejected]        my-feature -> my-feature (stale info)
error: failed to push some refs to 'github.com:user/repo.git'

이 경우에는 git fetch를 먼저 실행해서 원격 저장소의 최신 상태를 가져온 다음, 동료의 변경 사항을 확인하고 반영한 후에 다시 push하면 됩니다.

팀에서 협업할 때는 --force 대신 --force-with-lease를 쓰는 게 좋습니다. 매번 긴 옵션을 입력하기 번거롭다면 git config를 이용해서 별칭(alias)을 등록해두면 편합니다.

$ git config --global alias.pushf "push --force-with-lease"

이렇게 설정해두면 git pushf만으로 안전한 강제 푸시를 할 수 있습니다.

$ git pushf origin my-feature

태그 푸시

git push 명령어는 기본적으로 브랜치만 원격 저장소에 올립니다. 로컬에서 만든 태그(tag)를 원격 저장소에 올리려면 태그 이름을 명시해야 합니다.

$ git push origin v1.0.0

여러 개의 태그를 한 번에 모두 올리고 싶다면 --tags 옵션을 사용합니다.

$ git push --tags

이미 올린 태그를 삭제하고 싶다면 --delete 옵션을 사용하면 됩니다.

$ git push --delete origin v1.0.0

원격 브랜치 삭제

더 이상 필요 없는 원격 브랜치를 삭제할 때도 git push 명령어를 사용합니다. --delete 옵션 뒤에 삭제할 브랜치 이름을 지정하면 됩니다.

$ git push --delete origin my-feature
To github.com:user/repo.git
 - [deleted]         my-feature

혹시 같은 이름의 태그와 브랜치가 동시에 존재한다면 refs/heads/ 접두사를 붙여서 브랜치를 명확히 지정할 수 있습니다.

$ git push --delete origin refs/heads/my-feature

마치며

이상으로 git push 명령어를 사용하는 기본적인 방법과 좀 더 편리하게 사용할 수 있는 팁, 그리고 강제 푸시를 다루는 방법까지 폭넓게 알아보았습니다.

핵심 내용을 정리하면,

  • git push <저장소명> <브랜치명>으로 원격에 코드를 올릴 수 있고, -u 옵션으로 인자를 생략할 수 있습니다.
  • push.defaultcurrent로 설정하면 어떤 브랜치에서든 git push만으로 충분합니다.
  • --force(-f)는 원격 이력을 강제로 덮어쓰지만, 동료의 커밋을 유실시킬 위험이 있습니다.
  • --force-with-lease는 다른 사람의 변경 사항이 있으면 push를 거부하여 안전하게 강제 푸시할 수 있습니다.
  • --tags로 태그를 올리고, --delete로 원격 브랜치나 태그를 삭제할 수 있습니다.

강제 푸시가 필요할 때는 --force 대신 --force-with-lease를 사용하는 습관을 들이면 협업 시 실수를 줄일 수 있습니다.

더 자세한 내용은 Git 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

Discord