Git 문제해결 가이드

flight rules가 뭐예요?

뭔가 잘못됐을 때 뭘 해야 할지에 대한 우주비행사(여기선 Git을 쓰는 개발자)를 위한 가이드

Flight Rules 는 어떤 문제가 발생한 이유와 그 단계의 매뉴얼에서 어렵사리 얻은 지식입니다. 기본적으로 각 시나리오의 매우 자세하고 구체적인 운영 절차랍니다. [...]

NASA는 수성(Mercury) 시대 때 지상팀에서 처음으로 "lessons learned"란 것을 모았는데 수천 개의 문제의 상황들, 부서진 해치 손잡이로 인한 엔진 고장부터 컴퓨터 문제 그리고 그 해답까지, 1960년대 초부터 우리의 실수들, 재앙들, 해결책 등이 목록화되어있습니다.

— Chris Hadfield, 인생을 위한 우주비행사의 가이드.

이 문서의 규칙

명확하게 하기 위해 이 문서의 모든 예제는 현재 브랜치를 표시하고 스테이지에 변경이 있는지를 나타내기 위해 커스텀 된 배시 프롬프트를 사용합니다. 브랜치는 괄호 안에 있고, 브랜치 다음의 *는 스테이지의 변경된 것을 나타냅니다.

리포지터리

로컬 리포지터리에서 시작하기

이미 존재하는 프로젝트 디렉터리를 Git 리포지터리로 최적화해 쓰려면 다음과 같이 입력합니다.

(my-folder) $ git init

리모트 리포지터리를 복제해오기

리모트 리포지터리를 클론하려면, 리포지터리 URL을 복사해와서 실행합니다.

$ git clone [url]

폴더 이름이 리모트 리포지터리 이름과 같이 저장됩니다.

복제할 리모트 서버의 연결을 확인하세요. (대부분 인터넷 연결을 확인하란 뜻입니다)

다른 리포지터리 이름으로 복제를 해오고 싶다면 다음 명령어를 실행합니다.

$ git clone [url] 새-폴더-이름

커밋 수정

최근 남긴 커밋 확인하기

git commit -a로 막 커밋을 남긴 후에 커밋 메시지를 확인하고 싶다면 최근의 커밋을 현재 HEAD에서 볼 수 있습니다.

(main)$ git show

또는

$ git log -n1 -p

만약 특정 커밋의 파일을 보고 싶다면, 이렇게 할 수도 있습니다. (commitID는 확인하고자 하는 그 커밋입니다)

$ git show <commitid>:filename

커밋 메시지를 잘못 썼을 때

만약 메시지를 잘못 썼고 아직 푸시를 안 했다면, 커밋 메시지 바꾸기를 따라 해보세요.

$ git commit --amend --only

이 방법은 편집 가능한 기본 텍스트 에디터가 열릴 텐데요, 다른 방법으론 한 줄에 쓸 수도 있습니다.

$ git commit --amend --only -m 'xxxxxxx'

만약 푸시를 이미 했다면, 커밋을 수정해서 강제 푸시를 할 수 있긴 한데 별로 추천하는 방법은 아닙니다.

커밋을 원하지 않는 이름과 이메일 설정으로 해버렸을 때

하나의 커밋이라면 이렇게 수정합니다.

$ git commit --amend --no-edit --author "이름 <이메일>"

대안으로 git config --global author.(이름|이메일)에서 설정을 다시 맞추고 다음 명령을 실행하는 방법도 있습니다.

$ git commit --amend --reset-author --no-edit

만약 전체 이력 변경이 필요하다면, git filter-branch의 설명 페이지를 보세요.

지난 커밋에서 파일 하나를 지우고 싶을 때

지난 커밋에서 파일 변경내역을 지우려면, 이렇게 해보세요.

$ git checkout HEAD^ myfile
$ git add myfile
$ git commit --amend --no-edit

그 파일이 새롭게 커밋으로 추가됐고 그 파일만 지우고 (git에서만) 싶은 경우엔,

$ git rm --cached myfile
$ git commit --amend --no-edit

이 방법은 열린 패치가 있고 불필요한 파일을 커밋 했거나 리모트에 강제 푸시로 패치를 업데이트 해야 할 때 특히 유용합니다. --no-edit 옵션은 기존 커밋 메시지를 유지하는 데 사용됩니다.

마지막 커밋 삭제하기

푸시된 커밋을 지우고 싶다면 이걸 따라 하면 되는데, 이력을 돌이킬 수 없게 되고 리포지터리에서 이미 풀을 받아 간 다른 사람의 이력도 엉망이 됩니다. 간단히 말하자면, 잘 모르겠으면 절대 하지 마세요.

$ git reset HEAD^ --hard
$ git push --force-with-lease [remote] [branch]

아직 푸시하지 않았다면, 리셋으로 마지막 커밋 전 상태로 돌아가세요. (변경 점은 스테이지에 남겨둡니다)

(my-branch*)$ git reset --soft HEAD@{1}

이 방법은 푸시를 안 했을 때만 동작합니다. 푸시를 했으면, 안전한 방법은 git revert SHAofBadCommit 한 가지밖에 없습니다. 이 방법은 모든 지난 커밋 변경 점으로 되돌아간 새 커밋을 만듭니다. 또는, 만약 푸시한 브랜치가 리베이스에 안전하다면 (만약 다른 사람이 풀 받지 않는다면), git push --force-with-lease 명령어를 쓸 수 있습니다. 더 알고 싶다면, 이 섹션을 참고해주세요.

임의적인 커밋 지우기

이전과 동일한 경고입니다. 가능한 이 방법은 쓰지 마세요.

$ git rebase --onto SHA1_OF_BAD_COMMIT^ SHA1_OF_BAD_COMMIT
$ git push --force-with-lease [remote] [branch]

아니면 대화형 리베이스를 쓰고 지우고 싶은 커밋 라인을 지워도 돼요.

수정된 커밋을 푸시했는데, 에러 메시지가 나타납니다

To https://github.com/yourusername/repo.git
! [rejected] mybranch -> mybranch (non-fast-forward)
error: failed to push some refs to 'https://github.com/tanay1337/webmaker.org.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

알아두기

리베이스(아래를 보세요)나 어멘드는 기존 커밋을 새 걸로 바꿉니다. 그래서 이미 먼저 수정된 커밋이 푸시됐다면 강제 푸시를 해야 합니다. 이 방법을 쓸 땐 조심하세요! 항상 작업 되는 브랜치가 맞나 확인하세요!

(my-branch)$ git push origin mybranch --force-with-lease

일반적으로 강제 푸시를 쓰지 마세요. 새 커밋을 만들어서 푸시하는 게 수정된 커밋을 강제로 푸시하는 것보다 훨씬 낫습니다. 그런 수정된 커밋은 그 브랜치나 다른 자식 브랜치를 쓰는 다른 개발자의 소스 이력과 충돌의 원인이 될 겁니다. --force-with-lease 는 여전히 실패할 텐데, 누군가가 같은 브랜치를 쓴다면 변경 점을 덮어쓰는 푸시를 할 수도 있어요.

절대로 아무도 같은 브랜치를 안 쓰거나, 절대로 브랜치에 업데이트를 해야 할 때 --force (-f) 옵션을 쓸 수 있지만, 일반적으론 피하는 게 좋아요.

하드 리셋을 해버렸는데 되돌리고 싶습니다

만약 하드 리셋을 했다고 해도 커밋을 돌릴 순 있습니다. Git은 며칠간은 로그를 가지고 있거든요.

알아두기

이건 커밋을 남겼거나 스테이시같이 백업을 했을 때만 유효합니다. git reset --hard은 커밋되지 않은 수정사항을 다 지울겁 니다, 그러니 조심해서 써야 합니다. (안전한 방법으론 git reset --keep이 있어요)

(main)$ git reflog

지난 커밋과 리셋을 위한 커밋을 볼 수 있습니다. 돌아가고 싶은 커밋의 SHA 코드를 골라서, 리셋하세요.

(main)$ git reset --hard SHA1234

계속할 수 있습니다.

머지를 실수로 커밋, 푸시해버렸습니다

만약 실수로 머지할 준비가 안 된 피쳐 브랜치를 메인 브랜치에 머지했어도 되돌릴 순 있습니다. 하지만 문제는 있습니다: 머지 커밋은 한 개 이상의 부모(보통은 두 개)를 가지게 됩니다.

사용하려면 다음 명령을 실행합니다.

(feature-branch)$ git revert -m 1 <commit>

여기서 -m 1 옵션은 부모 번호 1(머지가 만들어진 브랜치)을 되돌릴 상위 항목으로 선택하라는 의미입니다.

알아두기

부모 번호는 커밋 식별자가 아니고, 오히려 머지된 커밋이 Merge: 8e2ce2d 86ac2e7 이라는 라인을 가지고 있습니다. 부모 번호는 이 라인에서 원하는 부모의 1 기반 인덱스이고, 첫 번째 식별자는 1, 다음은 2 이렇게 이어집니다.

스테이지

지난 커밋에 스테이지 변경 점을 추가하고 싶습니다

(my-branch*)$ git commit --amend

전체가 아닌 새 파일만 스테이지에 올리고 싶습니다

보통은 부분적으로 파일을 스테이지하려면 이렇게 합니다.

$ git add --patch filename.x

-p는 축약된 옵션입니다. 이 방식은 대화형 모드를 열 텐데, s 옵션을 쓰면 커밋을 나눌 수 있습니다. 하지만 새 파일이라면 그런 옵션이 없습니다. 새 파일을 추가하려면 다음과 같이 합니다.

$ git add -N filename.x

그다음 임의적으로 라인들을 골라 추가해주려면 e 옵션이 필요합니다. git diff --cachedgit diff --staged는 로컬에 저장된 부분과 스테이지에 있는 라인들을 비교해서 보여줄 거에요.

하나의 파일 변경 점을 두 개의 다른 커밋에 남기고 싶습니다

git add는 전체 파일들을 커밋에 추가합니다. git add -p는 대화형으로 추가하고픈 변경 점들을 고를 수 있어요.

아직 스테이지에 안 올라간 변경 점을 스테이지에 추가하고, 스테이지에 있는 변경 점을 다시 빼고 싶습니다

이건 좀 꼼수인데, 스테이지 전인 파일들을 스테이시 해서 빼두고선 리셋 할 수 있습니다. 그다음 스테이시를 다시 불러와 추가합니다.

$ git stash -k
$ git reset --hard
$ git stash pop
$ git add -A

스테이지 전의 변경 점

스테이지 전의 변경 점을 새 브랜치로 옮기고 싶습니다

$ git checkout -b my-branch

스테이지전 변경 점을 만들어둔 다른 브랜치로 옮기고 싶습니다

$ git stash
$ git checkout my-branch
$ git stash pop

내 로컬에 있는 커밋 안된 변경 점을 스테이징과 관계없이 다 무시하고 싶습니다

만약 모든 스테이징 됐거나 안 된 변경 점을 버리고 싶다면 이렇게 합니다.

(my-branch)$ git reset --hard
# or
(main)$ git checkout -f

이 방법은 git add로 스테이징 된 모든 파일이 빠지게 됩니다.

$ git reset

이 방법은 커밋되지 않은 모든 로컬 변경 점이 되돌려집니다. (리포지터리 최상단 루트에서 실행해야 합니다.)

$ git checkout .

또 커밋되지 않은 변경 점들 중 몇 가지 파일이나 디렉터리만 되돌릴 수 있습니다.

$ git checkout [some_dir|file.txt]

거기에 또 다른 되돌리는 방법으로 (타이핑 칠 게 많지만 어떤 하위 디렉터리에서도 가능합니다) 다음 명령이 있습니다.

$ git reset --hard HEAD

이 방법은 모든 트래킹 되지 않은 파일들을 지워서 Git에서 트래킹 되는 파일들만 남습니다.

$ git clean -fd

-x 옵션 또한 무시된 파일들을 다 지웁니다.

스테이지 안된 특정 변경 점을 지우고 싶습니다

작업 중인 영역에서 전체가 아닌 특정 부분을 지우고 싶을 때 원치 않는 변경 점을 확인하고, 변경 점을 잘 보관하세요.

$ git checkout -p
# 삭제하고 싶은 사항에 y를 입력하세요

또다른 전략은 stash을 같이 쓰는겁니디. 챙겨야 하는 변경 점을 스테이시 하고, 작업 중인 영역을 리셋하고, 다시 올바른 변경 점으로 재적용합니다.

$ git stash -p
# 저장하고 싶은 사항들을 다 선택하세요
$ git reset --hard
$ git stash pop

대안으로, 원치 않는 변경 점을 스테이시 해서 그걸 날리는 방법도 있습니다.

$ git stash -p
# 저장하고 싶지 않은 사항들을 다 선택하세요
$ git stash drop

스테이지 안된 특정 파일을 지우고 싶습니다

작업 영역에서 특정 파일을 지우고 싶을 때는 이렇게 합니다.

$ git checkout myFile

대안으로, 작업영역 내 여러 파일을 지우고 싶을 때 모두 나열해서 적습니다.

$ git checkout myFirstFile mySecondFile

로컬에 있는 스테이지 안된 변경 점만 지우고 싶습니다

모든 스테이징 안된 커밋 전인 변경 점을 지우고 싶을 때는 다음 명령을 실행합니다.

$ git checkout .

트래킹 안 된 파일들을 전부 지우고 싶습니다

트래킹 안 된 파일들 다 지우고 싶을 땐 이렇게 합니다.

$ git clean -f

브랜치

모든 브랜치 리스트를 보고 싶습니다

로컬 브랜치 다 보기

$ git branch

리모트 브랜치 다 보기

$ git branch -r

로컬과 리모트 브랜치 모두 보기

$ git branch -a

커밋에서 브랜치 만들기

$ git checkout -b <branch> <SHA1_OF_COMMIT>

다른 브랜치에서 풀을 받아와 버렸습니다

이건 잘못된 풀을 받기 전 HEAD가 어딜 가리키고 있었는지 볼 수 있는 git reflog를 써볼 기회입니다.

(main)$ git reflog
ab7555f HEAD@{0}: pull origin wrong-branch: Fast-forward
c5bc55a HEAD@{1}: checkout: checkout message goes here

간단히 원하는 커밋으로 브랜치를 되돌릴 수 있습니다.

$ git reset --hard c5bc55a

끝이에요!

로컬의 커밋을 지워서 서버에 있는 내 브랜치와 맞추고 싶습니다

서버에 변경 점을 푸시 안 했는지부터 확인해요.

git status가 오리진보다 몇 개의 커밋들이 앞서 있는지 보여줄 겁니다.

(my-branch)$ git status
# On branch my-branch
# Your branch is ahead of 'origin/my-branch' by 2 commits.
# (use "git push" to publish your local commits)
#

오리진(리모트과 같은 상태의)로 맞추는 리셋을 하는 방법 중 하나는 다음과 같습니다.

(main)$ git reset --hard origin/my-branch

새 브랜치 대신에 마스터에 커밋을 해버렸습니다

마스터에 있으면서 새 브랜치를 만드세요.

(main)$ git branch my-branch

마스터 브랜치를 기존 커밋으로 리셋합니다.

(main)$ git reset --hard HEAD^

HEAD^HEAD^1의 축약인데요. HEAD^의 첫 번째 부모를 의미하고, 비슷한 HEAD^2는 두 번째 부모를 의미합니다. (머지는 두 부모를 가질 수 있죠)

알아두기

HEAD^2HEAD~2과 같은 게 아닙니다. (더 자세한 정보는 이 링크를 참고하세요)

대안으로, HEAD^를 쓰고 싶지 않다면, 마스터 브랜치로 옮길 커밋 해시를 알아두세요. (git log가 트릭을 부릴 거에요) 그리고 그 해시로 리셋을 해요. git push가 리모트랑 변경 점이 똑같은 걸 확인해줍니다.

예를 들자면, 그 마스터의 커밋의 해시가 a13b85e라면 다음과 같이 하면 됩니다.

(main)$ git reset --hard a13b85e
HEAD is now at a13b85e

새 브랜치로 체크아웃해서 계속 작업합니다.

(main)$ git checkout my-branch

다른 레퍼런스 같은 곳에서 모든 파일을 유지하고 싶습니다

수백 번의 변경 점을 가진 스파이크(아래 알아두기 참고) 작업을 한다고 가정해보죠. 모든 건 동작하고 있고,그 작업을 저장해두기 위해 다른 브랜치로 커밋을 합니다.

(solution)$ git add -A && git commit -m "Adding all changes from this spike into one big commit."

그 커밋을 브랜치(아마 feature일 수도 있고, develop 일 수도 있겠죠)에 넣고 싶을 때, 모든 파일을 지키는데 관심이 있을 겁니다. 큰 커밋을 작게 나누고 싶을거에요.

현재 가지고 있는 건 다음과 같습니다.

  • 스파이크를 위한 솔루션과 함께인 solution 브랜치. develop 브랜치의 1단계 앞선 상태.
  • 변경 점을 추가하고 싶은 develop 브랜치

브랜치로 내용들을 불러오는 것으로 해결할 수 있습니다.

(develop)$ git checkout solution -- file1.txt

develop브랜치에서 solution 브랜치의 저 파일의 내용을 얻을 수 있습니다.

# On branch develop
# Your branch is up-to-date with 'origin/develop'.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: file1.txt

그다음, 평소처럼 커밋하세요.

알아두기

스파이크 솔루션은 문제를 분석하거나 풀기 위해 만들어졌습니다. 이 솔루션들은 모두가 문제의 확실한 시각화를 얻고선 평가되고 제거됩니다.~ 위키피디아.

한 브랜치에 다른 브랜치에 남겼어야 하는 커밋을 여러 개 남겼습니다

마스터 브랜치에 있다고 가정하고 git log 해보면 커밋 두 개를 볼 수 있을 겁니다.

(main)$ git log

commit e3851e817c451cc36f2e6f3049db528415e3c114
Author: Alex Lee <alexlee@example.com>
Date: Tue Jul 22 15:39:27 2014 -0400

Bug #21 - Added CSRF protection

commit 5ea51731d150f7ddc4a365437931cd8be3bf3131
Author: Alex Lee <alexlee@example.com>
Date: Tue Jul 22 15:39:12 2014 -0400

Bug #14 - Fixed spacing on title

commit a13b85e984171c6e2a1729bb061994525f626d14
Author: Aki Rose <akirose@example.com>
Date: Tue Jul 21 01:12:48 2014 -0400

First commit

각 버그 커밋의 해시를 가져오세요. (21번은 e3851e8, 14번은 5ea5173)

우선, 마스터 브랜치의 정확한 커밋 (a13b85e)으로 리셋합니다.

(main)$ git reset --hard a13b85e
HEAD is now at a13b85e

그리고, 21번 버그 작업을 위한 새로운 브랜치를 만들 수 있습니다.

(main)$ git checkout -b 21
(21)$

그리고 21번 버그 커밋을 체리픽 해서 브랜치 최상단에 올립니다. 그 커밋을 적용할 건데, 오직 그 커밋만을 헤드에 뭐가 있든 최상단으로 적용할 거란 의미입니다.

(21)$ git cherry-pick e3851e8

이 지점에서 충돌이 있을 수도 있습니다. 어떻게 충돌을 해결할지 대화형 리베이스 섹션 안에 있는 충돌이 있습니다 부분을 참고하세요.

자 이제 14번 버그 작업을 위해 마스터로 가서 새 브랜치를 만듭니다.

(21)$ git checkout main
(main)$ git checkout -b 14
(14)$

그리고 마지막으로, 14번 버그 작업을 위한 커밋을 체리픽합니다.

(14)$ git cherry-pick 5ea5173

업스트림에선 지워진 로컬 브랜치를 지우고 싶습니다

GitHub에 풀리퀘스트로 머지를 하면, 포크 뜬 머지 브랜치를 지울 건지 선택할 수 있는 옵션을 줍니다. 해당 브랜치에 계속 작업할 예정이 없다면, 다량의 오래된 브랜치들로 뒤덮이지 않게 로컬 작업을 지워주는 게 더 깔끔합니다.

$ git fetch -p upstream

여기서, upstream은 패치로 가져오려는 리모트 리포지터리에요.

브랜치를 지워버렸습니다

주기적으로 리모트으로 푸시한다면, 대부분은 안전해야 해요. 그렇지만 가끔은 브랜치를 지울 수 있어요. 새 브랜치를 만들고 파일을 하나 만들었다고 해보죠.

(main)$ git checkout -b my-branch
(my-branch)$ git branch
(my-branch)$ touch foo.txt
(my-branch)$ ls
README.md foo.txt

추가하고 커밋합니다.

(my-branch)$ git add .
(my-branch)$ git commit -m 'foo.txt added'
(my-branch)$ foo.txt added
1 files changed, 1 insertions(+)
create mode 100644 foo.txt
(my-branch)$ git log

commit 4e3cd85a670ced7cc17a2b5d8d3d809ac88d5012
Author: siemiatj <siemiatj@example.com>
Date: Wed Jul 30 00:34:10 2014 +0200

foo.txt added

commit 69204cdf0acbab201619d95ad8295928e7f411d5
Author: Kate Hudson <katehudson@example.com>
Date: Tue Jul 29 13:14:46 2014 -0400

Fixes #6: Force pushing after amending commits

이제 다시 마스터로 돌아가 '실수로' 브랜치를 지워보죠.

(my-branch)$ git checkout main
Switched to branch 'main'
Your branch is up-to-date with 'origin/main'.
(main)$ git branch -D my-branch
Deleted branch my-branch (was 4e3cd85).
(main)$ echo oh noes, deleted my branch!
oh noes, deleted my branch!

여기에서 업그레이드된 로그 도구인 '리플로그'에 익숙해져야 합니다. 리플로그는 리포지터리의 모든 행동의 이력을 다 보관해요.

(main)$ git reflog
69204cd HEAD@{0}: checkout: moving from my-branch to main
4e3cd85 HEAD@{1}: commit: foo.txt added
69204cd HEAD@{2}: checkout: moving from main to my-branch

보시다시피 지워진 브랜치의 커밋 해시도 볼 수 있습니다. 지웠던 브랜치를 살릴 수 있는지 한 번 해보죠.

(main)$ git checkout -b my-branch-help
Switched to a new branch 'my-branch-help'
(my-branch-help)$ git reset --hard 4e3cd85
HEAD is now at 4e3cd85 foo.txt added
(my-branch-help)$ ls
README.md foo.txt

짜잔! 지워진 파일들을 되돌려 놨어요. git reflog는 리베이스가 끔찍하게 잘못 됐을 때 아주 유용합니다.

브랜치를 지우고 싶습니다

리모트 브랜치를 삭제하려면 다음과 같이 합니다.

(main)$ git push origin --delete my-branch

이렇게도 할 수 있습니다.

(main)$ git push origin :my-branch

로컬 브랜치를 삭제하려면 이렇게 합니다.

(main)$ git branch -d my-branch

현재 브랜치나 업스트림에 머지되지 않은 로컬 브랜치를 지우려면 다음과 같이 합니다.

(main)$ git branch -D my-branch

여러 개의 브랜치를 지우고 싶습니다

fix/로 시작하는 모든 브랜치들을 지우고 싶다면 다음과 같이 합니다.

(main)$ git branch | grep 'fix/' | xargs git branch -d

브랜치 이름을 바꾸고 싶습니다

현재 (로컬) 브랜치 이름을 바꾸는 명령어는 다음과 같습니다.

(main)$ git branch -m new-name

다른 (로컬) 브랜치 이름을 바꾸려면 이렇게 합니다.

(main)$ git branch -m old-name new-name

다른 사람이 작업 중인 리모트 브랜치로 체크아웃하고 싶습니다

우선, 리모트 리포지터리에서 모든 브랜치를 패치 받습니다.

(main)$ git fetch --all

리모트의 daves로 체크아웃하고 싶다고 하면.

(main)$ git checkout --track origin/daves
Branch daves set up to track remote branch daves from origin.
Switched to a new branch 'daves'

(--trackgit checkout -b [branch] [remotename]/[branch]의 축약이에요)

daves 브랜치의 로컬 카피를 줄 거에요. 그리고 푸시된 업데이트들도 리모트로 표시돼요.

현재 로컬 브랜치로 새로운 리모트 브랜치를 만들고 싶습니다

$ git push <remote> HEAD

또한 리모트 브랜치를 현재 브랜치를 위한 업스트림으로 설정하고 싶다면, 대신 아래 방법을 써보세요.

$ git push -u <remote> HEAD

push.default 설정의 upstream 모드와 simple모드 (2.0 버전의 Git의 기본)와 함께, 아래 커맨드는 이전에 -u 옵션으로 등록된 리모트 브랜치와 관련된 현재 브랜치를 푸시할 겁니다.

$ git push

git push의 다른 모드의 동작은 push.default 문서에 설명돼 있습니다.

리모트 브랜치를 로컬 브랜치를 위한 업스트림으로 설정하고 싶습니다

리모트 브랜치를 현재 쓰고 있는 로컬 브랜치를 위한 업스트림으로 설정할 수 있습니다.

$ git branch --set-upstream-to [remotename]/[branch]
# 아니면 짧게:
$ git branch -u [remotename]/[branch]

다른 로컬 브랜치를 위한 업스트림 리모트 브랜치를 설정하려면 다음과 같이 합니다.

$ git branch -u [remotename]/[branch] [local-branch]

HEAD를 기본 리모트 브랜치로 트래킹하도록 설정하고 싶습니다

리모트 브랜치를 확인해보는 것으로, HEAD가 트래킹 중인 리모트 브랜치를 볼 수 있습니다. 몇몇 경우에는, 원하던 브랜치가 아닐 겁니다.

$ git branch -r
origin/HEAD -> origin/gh-pages
origin/main

origin/HEADorigin/main를 트래킹하는 것으로 변경하려면, 이 커맨드로 실행할 수 있습니다.

$ git remote set-head origin --auto
origin/HEAD set to main

다른 브랜치에 변경 점을 잘못 남겼습니다

커밋 되지 않은 변경 점, 거기다 잘못된 브랜치에 하고 있었다면 변경 점을 스테이시하고 원하는 브랜치로 가서 스테이시 어플라이 하세요.

(wrong_branch)$ git stash
(wrong_branch)$ git checkout <correct_branch>
(correct_branch)$ git stash apply

리베이스와 머지

리베이스/머지 한 걸 되돌리고 싶습니다

현재 브랜치를 의도하든 의도하지 않든 브랜치로 머지 또는 리베이스 했거나, 리베이스/머지 도중에 완료하거나 끝내지 못했을 겁니다. Git은 위험한 과정 전에 원래의 HEAD 포인트를 ORIG_HEAD라 불리는 변수에 보관해요, 그러니 리베이스/머지 전 상태로 브랜치를 복구하기 간단합니다.

(my-branch)$ git reset --hard ORIG_HEAD

리베이스를 했는데, 강제 푸시하고 싶진 않습니다

아쉽게도 그런 변경 점을 리모트 브랜치에 반영하려면 강제 푸시밖에 방법이 없습니다. 이력을 변경해왔기 때문이죠. 리모트 브랜치는 강제 푸시 외엔 적용해 주지 않을거에요. 많은 분이 머지 워크플로우를 리베이스 워크플로우보다 선호하는 많이 이유 중 하나죠 - 큰 팀에선 개발자의 강제 푸시로 곤란해질 수 있습니다. 주의해서 쓰세요. 리베이스를 그나마 안전하게 쓰는 방법은 리모트 브랜치의 모든 변경 점과 똑같이 반영하는 게 아니라 대신에 이렇게 해보세요.

(main)$ git checkout my-branch
(my-branch)$ git rebase -i main
(my-branch)$ git checkout main
(main)$ git merge --ff-only my-branch

더 확인이 필요하다면, 이 스택오버플로우의 스레드를 참고하세요.

커밋끼리 합치고 싶습니다

main에 풀 리퀘스트가 될 브랜치에서 작업하고 있다고 가정해봅시다. 가장 간단한 경우는 원하는 게 모든 커밋을 하나의 커밋으로 합치고 변경 점의 시간을 신경 쓰지 않아도 되는 것일 때, 리셋하고 커밋을 다시 하면 됩니다. 마스터 브랜치가 최신이고 모든 변경 점이 커밋된 것만 확인한 뒤 다음과 같이 합니다.

(my-branch)$ git reset --soft main
(my-branch)$ git commit -am "New awesome feature"

좀 더 조정하고, 시간 기록까지 보관하고 싶다면, 대화형 리베이스가 필요할 거에요.

(my-branch)$ git rebase -i main

만약 다른 브랜치로 붙는 작업을 하는 게 아니라면, HEAD를 기준으로 리베이스 해야합니다. 예로 마지막 2개의 커밋을 스쿼시(기존 커밋에 반영해넣는)하고 싶다면 HEAD~2로 리베이스 해요. 3개라면 HEAD~3으로 하고요.

(main)$ git rebase -i HEAD~2

대화형 리베이스를 실행하면 텍스트 에디터로 이런 것들을 볼 수 있을 겁니다.

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
pick b729ad5 fixup
pick e3851e8 another fix

# Rebase 8074d12..b729ad5 onto 8074d12
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

모든 #으로 시작하는 주석줄은 리베이스에 영향을 주지 않습니다.

다음으로 pick 부분을 다른 명령어로 바꾸거나, 해당하는 라인을 지워서 커밋을 지울 수도 있습니다.

예를 들자면 오래된 (첫 번째) 커밋만 두고 두 번째 오래된 커밋과 나머지를 다 합치고 싶을 때, 첫 번째와 두 번째 커밋 제외한 나머지 커맨드들을 f로 바꿔야 할 거에요:

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
f b729ad5 fixup
f e3851e8 another fix

이 커밋들을 합치고 커밋 이름을 바꾸고 싶다면, 추가로 적어줘야 합니다. 두 번째 커밋 다음에 r를 추가하거나 간단히 f 대신 s를 추가해주면 됩니다.

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
s b729ad5 fixup
s e3851e8 another fix

그런 다음에 한 번 더 뜨는 텍스트 에디터로 커밋 이름을 바꿀 수 있습니다.

Newer, awesomer features

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# rebase in progress; onto 8074d12
# You are currently editing a commit while rebasing branch 'main' on '8074d12'.
#
# Changes to be committed:
# modified: README.md
#

전부 다 성공하면, 이런 메시지를 볼 겁니다.

(main)$ Successfully rebased and updated refs/heads/main.

안전한 머지 전략

--no-commit는 머지는 하지만 실패하고 자동 커밋이 안된 것처럼 보이는데, 커밋하기전에 머지 결과를 보고 추가로 조정할 수 있게 해줘요. no-ff는 피쳐 브랜치가 있었다는 증거를 남기고, 이력을 일관되게 가지게 해요.
> (main)$ git merge --no-ff --no-commit my-branch

브랜치를 커밋 하나로 머지해야 합니다

(main)$ git merge --squash my-branch

푸시 되지 않은 커밋만 합치고 싶습니다

가끔 여러 가지 작업 도중인 커밋을 푸시하기 전에 합치고 싶을 거에요. 다른 누군가가 벌써 참고해서 커밋을 만들고 있을 테니 이미 푸시된 커밋을 잘못 합치길 바라진 않을 겁니다.

(main)$ git rebase -i @{u}

이 명령은 아직 푸시하지 않은 커밋만으로 대화형 리베이스를 실행하기 때문에 목록 내에 있는 어떤 커밋이든 재정렬/수정/합치기 안전합니다.

머지를 중단 해야 할 때

때때로 머지는 어떤 파일에 문제를 일으킬 수도 있습니다, 이 경우 옵션 abort으로 현재 충돌 해결 프로세스를 중단하고 병합하기 전 상태로 다시 구성할 수 있습니다.

(my-branch)$ git merge --abort

이 명령은 1.7.4 버전부터 쓸 수 있습니다.

브랜치내 모든 커밋이 머지됐는지 확인하고 싶습니다

브랜치 내 모든 커밋이 다른 브랜치로 머지됐는지 확인하려면, 그 브랜치들 HEAD (또는 특정 커밋)를 비교해야 합니다.

(main)$ git log --graph --left-right --cherry-pick --oneline HEAD...feature/120-on-scroll

이 명령은 어디에는 있고 다른 덴 없는 커밋이 있나를 알려주고 브랜치들 사이에 공유되지 않은 커밋 목록을 보여줍니다. 다른 옵션은 이렇게 합니다.

(main)$ git log main ^feature/120-on-scroll --no-merges

대화형 리베이스로 발생가능한 이슈

리베이스 편집 화면에서 'noop'

이런 걸 본다면:

noop

동일한 커밋에 있거나 현재 브랜치보다 앞서 있는 브랜치에 대해 리베이스를 시도한다는 의미에요. 이렇게 해볼 수 있습니다.

  • 마스터 브랜치가 있어야 할 곳에 있나 확인
  • 대신해서 HEAD~2 또는 더 기존 항목을 리베이스

충돌이 있습니다

리베이스를 똑바로 끝내지 못했다면, 충돌을 해결해야 할 겁니다.

어떤 파일이 충돌 났는지 git status를 먼저 실행해봐요.

(my-branch)$ git status
On branch my-branch
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

both modified: README.md

이 예시에선, README.md 가 충돌 났네요. 파일을 열어서 아래와 같은 부분을 찾아보세요.

   <<<<<<< HEAD
some code
=========
some code
>>>>>>> new-commit

새로운 커밋으로 추가된 코드(예시에선, 중간선부터 new-commit까지의)와 HEAD 사이에서 차이점을 해결해야 할 겁니다.

어느 한쪽 브랜치의 코드를 남기고 싶다면, --ours 또는 --theirs를 쓰면 돼요:

(main*)$ git checkout --ours README.md
  • 머지할때, --ours를 쓰면 로컬 브랜치의 변경 점을 유지하고, --theirs 는 다른 브랜치의 변경 점을 유지해요.
  • 리베이스할 땐, --theirs가 로컬 브랜치의 변경 점을 유지하고 --ours는 다른 브랜치의 변경 점을 유지해요. 이런 차이에 관한 설명은 Git 정식 문서 중 이 문서를 보세요.

만약 머지가 더 복잡하면, 비주얼 디프 에디터를 쓸 수도 있습니다.

(main*)$ git mergetool -t opendiff

코드의 충돌을 해결하고 테스트가 해결되고 나면, 바뀐 파일 내용을 git add 해주고, git rebase --continue로 리베이스를 이어서 합니다.

(my-branch)$ git add README.md
(my-branch)$ git rebase --continue

만약 모든 충돌을 개선한 내용이 커밋 전과 동일한 트리 구조를 가진다면, 대신에 git rebase --skip을 해야 합니다.

리베이스 중 멈추고 싶은 어떤 시점이거나 원래 상태의 브랜치로 돌아가고 싶다면, 이렇게 할 수 있습니다.

(my-branch)$ git rebase --abort

스테이시

모든 변경 점 스테이시 하기

작업 중인 디렉터리에서의 변경한 내용 전부를 스테이시 하려면 다음 명령을 실행합니다.

$ git stash

트래킹 되지 않은 파일까지도 포함하려면, -u 옵션을 씁니다.

$ git stash -u

특정 파일들만 스테이시 하기

작업 중인 디렉터리에서 한 파일만 스테이시 하기

$ git stash push working-directory-path/filename.ext

작업 중인 디렉터리에서 여러 파일 스테이시 하기

$ git stash push working-directory-path/filename1.ext working-directory-path/filename2.ext

메시지와 함께 스테이시 하기

$ git stash save <message>

특정 스테이시 목록에서 가져와 적용하기

메시지 작성된 스테이시 리스트 먼저 확인하세요.

$ git stash list

그런 다음 리스트 내 특정 스테이시를 적용하세요.

$ git stash apply "stash@{n}"

여기에서, 'n' 은 스택 안에서 스테이시의 위치를 나타냅니다.. 젤 위에 있는 스테이시가 0 일 거에요.

찾아보기

어떤 커밋에서 문자열을 찾고 싶습니다

특정한 문자열이 포함된 어떤 커밋을 찾으려면, 이런 구조로 쓸 수 있습니다.

$ git log -S "string to find"

일반적인 파라미터들은 다음과 같습니다.

  • --source 각 커밋에 도달한 명령어에 지정된 참조 이름을 보여주는 걸 의미합니다.

  • --all 는 모든 브랜치에서 시작하는 걸 의미합니다.

  • --reverse 반대의 순서로 출력해요, 변경 점의 첫 번째 커밋이 보일 거란 거죠.

작성자나 커미터를 찾고 싶습니다

작성자나 커미터의 모든 커밋을 찾으려면 이렇게 쓸 수 있습니다.

$ git log --author=<name or email>
$ git log --committer=<name or email>

작성자와 커미터가 같지 않다는 것만 염두에 두세요. --author는 코드를 실제로 코드를 작성한 사람이고 반면에 --committer는 실제 작성자를 대신해 커밋을 한 사람입니다.

특정 파일이 포함된 커밋을 목록화하고 싶습니다

특정 파일이 든 모든 커밋을 찾으려면 이렇게 합니다.

$ git log -- <path to file>

보통은 정확한 경로를 쓸 테지만 와일드카드로 경로나 파일명을 쓸 수도 있습니다.

$ git log -- **/*.js

와일드카드를 쓸 때, 커밋된 파일의 목록을 볼 수 있는 --name-status로 확인하는 게 유용합니다.

$ git log --name-status -- **/*.js

커밋을 참조하는 태그를 찾고 싶습니다.

특정 커밋이 포함된 모든 태그를 찾으려면 다음과 같이 합니다.

$ git tag --contains <commitid>

서브모듈

모든 서브모듈을 클론하기

$ git clone --recursive git://github.com/foo/bar.git

벌써 클론했다면 다음과 같이 합니다.

$ git submodule update --init --recursive

서브모듈 지우기

서브모듈을 만드는 건 아주 간단하지만 지우는 건 그렇진 않아요. 필요한 명령어는 다음과 같습니다.

$ git submodule deinit submodulename
$ git rm submodulename
$ git rm --cached submodulename
$ rm -rf .git/modules/submodulename

기타 항목들

지운 파일 복구하기

우선 그 파일이 마지막으로 있었던 커밋을 찾습니다.

$ git rev-list -n 1 HEAD -- filename

그런 다음 그 파일을 체크아웃합니다.

git checkout deletingcommitid^ -- filename

태그 지우기

$ git tag -d <tag_name>
$ git push <remote> :refs/tags/<tag_name>

지워진 태그 복구하기

이미 지운 태그를 복구하고 싶다면, 이런 단계를 따라해볼 수 있습니다. 우선, 연결할 수 없는 태그를 찾습니다.

$ git fsck --unreachable | grep tag

태그의 해시를 메모해두세요. 그런 다음 git update-ref을 써서 지워진 태그를 복구합니다.

$ git update-ref refs/tags/<tag_name> <hash>

이제 태그가 복구돼있을 겁니다.

지워진 패치

만약 GitHub에서 누군가가 풀리퀘스트를 보냈는데 이미 원래의 포크가 지워졌다면, url을 쓸 수 없게 돼 리포지터리를 클론할 수 없거나 .diff, .patch와 같은 git am를 쓸 수 없을 거예요. 하지만 GitHub의 특별한 참조를 이용해서 풀 리퀘스트 자체를 확인할 수 있습니다. PR#1의 내용을 pr_1이란 새 브랜치로 패치 받으려면 다음과 같이 합니다.

$ git fetch origin refs/pull/1/head:pr_1
From github.com:foo/bar
* [new ref] refs/pull/1/head -> pr_1

Zip파일로 리포지터리 추출하기

$ git archive --format zip --output /full/path/to/zipfile.zip main

파일 추적하기

파일 내용엔 변경이 없이 파일 이름을 앞글자만 대문자로 바꾸고 싶습니다

(main)$ git mv --force myfile MyFile

Git 풀 받을때 로컬 파일을 덮어씌우고 싶습니다

(main)$ git fetch --all
(main)$ git reset --hard origin/main

파일을 로컬에는 보관하고 Git에서 지우고 싶습니다

(main)$ git rm --cached log.txt

특정한 버전대로 파일을 복구하고 싶습니다

복구하고 싶은 해시가 c5f567 이라고 가정하면 다음과 같이 하면 됩니다.

(main)$ git checkout c5f567 -- file1/to/restore file2/to/restore

c5f567 한 단계전으로 복구하고 싶다면, c5f567~1로 적어주세요

(main)$ git checkout c5f567~1 -- file1/to/restore file2/to/restore

커밋과 브랜치 간의 특정 파일 변경 이력이 필요합니다

마지막 커밋과 c5f567으로 부터의 차이를 비교하고 싶다고 가정하면 다음과 같이 하면 됩니다.

$ git diff HEAD:path_to_file/file c5f567:path_to_file/file

브랜치도 같은 방법으로:

$ git diff main:path_to_file/file staging:path_to_file/file

설정

Git 명령어 몇 개를 앨리어스로 등록하고 싶습니다

맥OS나 리눅스에는, Git 설정 파일이 ~/.gitconfig 에 있어요. 단축용으로 (몇 개는 평소 쓰는 용도로) 앨리어스 몇 개를 아래와 같이 계속 추가해오고 있습니다.

[alias]
a = add
amend = commit --amend
c = commit
ca = commit --amend
ci = commit -a
co = checkout
d = diff
dc = diff --changed
ds = diff --staged
f = fetch
loll = log --graph --decorate --pretty=oneline --abbrev-commit
m = merge
one = log --pretty=oneline
outstanding = rebase -i @{u}
s = status
unpushed = log @{u}
wc = whatchanged
wip = rebase -i @{u}
zap = fetch -p

리포지터리에 빈 디렉터리를 추가하고 싶습니다

못해요! Git은 지원하지 않거든요, 근데 꼼수가 있어요. 디렉터리에에 .gitignore 파일을 아래 내용으로 만드세요

 # Ignore everything in this directory
*
# Except this file
!.gitignore

다른 일반적인 컨벤션은 그 폴더 안에 .gitkeep이라는 이름의 빈 파일을 만드는 겁니다.

$ mkdir mydir
$ touch mydir/.gitkeep

.keep이란 이름으로도 쓸 수 있는데요, 두 번째 라인이 touch mydir/.keep가 되어야겠죠.

리포지터리 유저명과 비밀번호를 캐시 해두고 싶습니다

인증이 필요한 리포지터리를 가지고 있을 텐데요. 이런 경우 유저명과 비밀번호를 캐시 할 수 있을테니 매번 푸시/풀할 때마다 입력할 필욘 없습니다. 크리덴셜 헬퍼가 해줄 거에요.

$ git config --global credential.helper cache
# Set git to use the credential memory cache
$ git config --global credential.helper 'cache --timeout=3600'
# Set the cache to timeout after 1 hour (setting is in seconds)

Git이 권한과 파일모드 변경을 무시하게 만들고 싶습니다

$ git config core.fileMode false

이것을 로그인된 유저의 기본 행위로 설정으로 해두려면, 이렇게 쓰세요.

$ git config --global core.fileMode false

글로벌 유저로 설정해두고 싶습니다

모든 로컬 리포지터리에 사용되는 사용자 정보를 설정하려면, 그리고 버전 이력을 리뷰할때 알아보기 쉬운 이름으로 설정하려면 다음을 실행하세요.

$ git config --global user.name “[firstname lastname]”

각 이력 생산자에게 연관해서 이메일 설정을 해주려면 다음과 같이 하면 됩니다.

git config --global user.email “[valid-email]”

Git에 명령어 색상을 넣고 싶습니다

손쉬운 리뷰를 위한 Git 커맨드라인 자동 색상을 설정하려면 다음 명령을 실행합니다.

$ git config --global color.ui auto

뭘 잘못했는지 모르겠습니다

음, 큰일이네요. 뭔가를 reset 했거나, 다른 브랜치로 머지했거나, 지금은 찾질 못하는 커밋으로 강제 푸시를 해버렸군요. 알다시피, 어떤 시점에선, 잘하고 있었고 거기로 돌아가고 싶겠죠. 이게 바로 git reflog의 존재 이유입니다. reflog 는 브랜치 끝의 어떤 변경 점이든 브랜치나 태그에 의해 참조되지 않더라도 다 보관합니다. 기본적으로, HEAD가 변경되는 모든 경우, 리플로그에 새로운 입력이 추가됩니다. 아쉽게도 이 기능은 로컬 리포지터리에서만 동작하고, 오직 움직임만을 트래킹해요 (예를 들자면 어디에도 기록되지 않았던 파일의 변경은 트래킹하지 않습니다)

(main)$ git reflog
0a2e358 HEAD@{0}: reset: moving to HEAD~2
0254ea7 HEAD@{1}: checkout: moving from 2.2 to main
c10f740 HEAD@{2}: checkout: moving from main to 2.2

이 리플로그는 마스터에서 2.2 브랜치로 체크아웃하고 되돌린 것을 보여주네요. 저기에선, 오래된 커밋으로 리셋하기 어렵습니다. 최신 활동이 HEAD@{0} 상단 라벨로 보이네요.

만약 실수로 뒤로 이동했다면, 리플로그는 실수로 지워진 2개의 커밋 전 상태인 (0254ea7)를 가리키는 커밋 마스터를 포함할 겁니다.

$ git reset --hard 0254ea7

git reset을 쓰는 것으로 마스터를 이전 커밋으로 되돌릴 수 있습니다. 이력이 실수로 변경됐을 때의 안정망을 제공할 거에요.

(여기에서 복제해와 수정했습니다).

다른 리소스

도서

  • Pro Git - Scott Chacon 과 Ben Straub의 훌륭한 Git 책
  • Git Internals - Scott Chacon의 또 다른 훌륭한 Git 책

튜토리얼

스크립트와 도구

  • firstaidgit.io 가장 많이 묻는 Git 질문의, 검색 가능한 모음
  • git-extra-commands - 유용한 기타 Git 스크립트 모음
  • git-extras - Git 유틸리티 -- 리포지터리 요약, repl, 변경 이력 밀집도, 작성자 커밋 비율 등
  • git-fire - git-fire는 모든 현재 파일을 추가, 커밋, 새 브랜치로 푸시(머지를 예방하기 위한) 등 비상사태를 도와주는 플러그인
  • git-tips - 자그마한 Git 팁들
  • git-town - 포괄적이고, 높은 수준의 Git 워크플로우 지원! http://www.git-town.com

GUI 클라이언트

  • GitKraken - 완전 고급의 Git 클라이언트 Windows, Mac & Linux
  • git-cola - 또 다른 Git 클라이언트 Windows, OS X
  • GitUp - 아주 독단적으로 방식으로 Git의 복잡함을 다루는 새로운 GUI
  • gitx-dev - 또다른 그래픽적인 Git 클라이언트 OS X
  • Sourcetree - 아름답고 무료인 Git GUI 안에서 단순함과 강력함이 만나다 Windows and Mac
  • Tower - 그래픽 Git 클라이언트 OS X (유료)
  • tig - Git을 위한 터민러 텍스트 모드 인터페이스
  • Magit - Emacs 패키지를 위해 구현된 Git 인터페이스
  • GitExtensions - 쉘 확장, 비주얼 스투디오 2010-2015 플러그인 그리고 독자적인 Git 리포지터리 도구
  • Fork - 빠르고 친숙한 Git 클라이언트 Mac (베타)
  • gitk - 간단한 리포지터리 상태를 볼 수 있는 리눅스 Git 클라이언트

출처

깃을 위한 flight rules

라이선스

Attribution-ShareAlike 4.0 International