A Visual Git Reference

다른 언어로 보기

이 문서는 Git에서 주로 사용하는 명령들에 대해 여러 그래프와 함께 요약하여 설명하고 있습니다. 이 문서를 통해 Git이 어떻게 돌아가는지 한번 살짝 살펴보고 나면 Git을 이해하는데 있어 밝은 등대가 되어줄 것입니다. 이 문서가 어떻게 만들어졌는지 관심있는 분은 GitHub 저장소로 찾아와서 살펴보시기 바랍니다.

목차

  1. 기초 사용법
  2. 관례
  3. 명령어 살펴보기
    1. Diff
    2. Commit(커밋)
    3. Checkout
    4. Detached HEAD와 Commit
    5. Reset
    6. Merge
    7. Cherry Pick
    8. Rebase
  4. 기술적인 내용
  5. 실습: 명령어들의 결과 보기

기초 사용법

위의 네 가지 명령을 사용하여 작업 디렉토리, Stage 영역(Index 라고도 부르는), 히스토리(저장된 커밋들) 사이에 파일을 복사합니다.

git reset -pgit checkout -p 또는 git add -p 명령과 같이 -p 옵션을 사용하여 파일을 지정하지 않고 어떤 파일에 대해 명령을 적용할 지 대화형 명령을 사용할 수 있습니다.

Stage 영역을 거치지 않고 직접 History로부터 파일을 Checkout 하거나, Stage 영역을 거치지 않고 직접 Commit을 할 수도 있습니다.

관례

이 문서는 다음과 같은 그래프를 사용하여 Git 사용법을 설명 합니다.

커밋은 5글자의 ID로 표현하며, 부모 커밋을 화살표로 가리킵니다. 브랜치는 오렌지색이며 어떤 특정 커밋을 가리키고 있습니다. HEAD 라는 이름으로 현재 브랜치 가리킬 수 있습니다. 위의 그림에는 5개의 커밋이 있으며 ed489 커밋이 가장 최근의 커밋입니다. main 브랜치(현재 선택한 브랜치)는 가장 최근의 커밋을 가리키고 있으며 stable 브랜치는 main 브랜치의 뿌리 부분(Ancestor) 입니다.

명령어 살펴보기

Diff

커밋간의 변경된 사항을 살펴보는 방법은 여러가지가 있습니다. 아래 예제는 여러 방법 중 대표적인 것들입니다. 파일 이름을 옵션으로 지정하면 특정 파일에 대한 변경사항만 확인할 수 도 있습니다.

Commit(커밋)

커밋을 하면 Git은 Stage 영역의 파일들과 부모 커밋 정보 그리고 현재 커밋 정보를 사용하여 새로운 커밋 개체(Commit Object)를 만듭니다. 그리고 현재 브랜치가 이 새로 만들어진 커밋을 가리키도록 만듭니다. 아래 그림에 보면 현재 브랜치는 main 이고 명령을 실행하기 전에는 ed489 커밋을 가리키고 있습니다. 새로 커밋을 하게 되면 커밋의 부모가 ed489f0cec 커밋이 만들어지고 main 브랜치는 f0cec 커밋을 가리키게 됩니다.

이런 새로운 커밋이 추가되는 과정은 현재 브랜치가 다른 브랜치의 뿌리 부분(Ancestor)이라고 해도 가능한 일입니다. 아래 그림을 보면 main 브랜치의 뿌리가 되는 stable 브랜치에서 커밋을 할 경우 1800b 커밋이 만들어집니다. 이렇게 되면 stable 브랜치는 main 브랜치의 직접적인 뿌리 부분이 되지는 않습니다. 이 두 개의 히스토리 내용을 합치기 위해서는 Merge(통합, 병합) 또는 Rebase 명령이 필요합니다.

가끔 커밋을 할 때 실수를 할 수 있습니다. 하지만 이미 커밋을 했다고 해도 git commit --amend 명령으로 쉽게 실수를 고칠 수 있습니다. 이 명령을 사용하면 현재 커밋과 부모가 같은 새 커밋을 만듭니다. (실수했던 커밋을 다른 커밋이나 브랜치가 사용하지 않았다면 자동으로 없어질 것입니다.)

마지막 경우는 detached HEAD에서 커밋을 하는 것인데 아래에서 다시 다루기로 한다.

Checkout

Checkout 명령은 히스토리나 Stage 영역으로부터 현재 작업 디렉토리로 파일을 복사하는 명령입니다. 또는 브랜치를 변경할 때 사용하기도 합니다.

Checkout 명령에 파일 이름이 주어지면 (또는 -p 옵션) Git은 해당 파일을 주어진 커밋에서 Stage 영역과 작업 디렉토리로 복사합니다. 예를 들어 git checkout HEAD~ foo.c 명령을 실행하면 HEAD~ 커밋(현재 커밋보다 한 단계 앞의 커밋)으로부터 foo.c 파일을 작업 디렉토리에 복사하고 Stage 영역에도 추가합니다. (커밋 이름을 지정하지 않으면 Stage 영역에서 복사해옵니다.) 현재 브랜치가 가리키는 커밋은 바뀌지 않았다는 점을 주목해봅니다.

파일 이름을 지정하지 않고 브랜치 이름만 지정하면 Git은 HEAD를 지정한 브랜치를 가리키도록 변경합니다. 이것은 결과적으로 브랜치를 변경한 것과 같습니다. 따라서 자동으로 Stage 영역과 작업 디렉토리의 내용은 해당 브랜치의 내용으로 변경됩니다. a47c3 커밋에 포함된 파일들을 현재 디렉토리로 복사할 것이며 이전 ed489 커밋에는 포함되었지만 a47c3 커밋에 포함되어있지 않은 파일들은 삭제될 것입니다. 두 커밋에 모두에 포함되지 않은 파일은 무시될 것입니다.

파일 이름을 지정하지 않고, (로컬) 브랜치 이름도 지정하지 않은 경우 - 즉, 태그, 리모트 브랜치, SHA-1 아이디, 혹은 main~3와 같은 유형을 인자로 지정했다면, detached HEAD라고 부르는 익명 브랜치(Anonymous Branch)를 사용하게 되는데 프로젝트의 히스토리를 옮겨다닐 때 유용하게 사용할 수 있습니다. 'Git 프로젝트'의 1.6.6.1 버전을 컴파일 해보고 싶다면 git checkout v1.6.6.1 명령으로 소스를 Checkout하여 컴파일 하고, 그 결과 바이너리들을 설치해볼 수 있습니다(v1.6.6.1은 브랜치는 아니고 태그입니다). 그리고 나서 다시 git checkout main 명령으로 다른 브랜치로 변경할 수도 있습니다. detached HEAD에서 커밋을 하면 경우가 좀 달라지는데 아래에서 다시 살펴볼 것입니다.

Detached HEAD에서 커밋하기

detached HEAD에서의 커밋도 별반 다르지 않습니다. 다만 아무 브랜치도 업데이트되지 않는다는 것만 다를 뿐입니다. (익명의 브랜치라고 생각해볼 수 있습니다.)

detached HEAD에서 다른 브랜치로 변경하게 되면(예를 들어 main 같은) detached HEAD의 커밋을 가리키는 어떤 이름도 갖지 못하게 되어 접근할 길을 잃고 맙니다. 아래 그림을 보면 브랜치를 변경 후 어떤 이름도 2eecb를 가리키고 있지 않습니다.

하지만 이 커밋을 가리키도록 새 브랜치를 만들 수 있는데 git checkout -b name 명령을 사용할 수 있습니다.

Reset

Reset 명령은 현재 브랜치가 가리키고 있는 커밋을 이동시킬 때 사용하며 Stage 영역과 작업 디렉토리의 내용을 갱신합니다. 또한 실제 작업 디렉토리 내용을 변경하지 않은 채로 이전 커밋에서 파일을 Stage 영역으로 복사할 때에도 사용합니다.

파일 이름 없이 커밋만 지정하는 경우 브랜치가 해당 커밋을 가리키도록 변경합니다. Stage 영역 또한 해당 커밋에 맞게 갱신됩니다. --hard 옵션이 주어지면 작업 디렉토리 또한 갱신됩니다. --soft 옵션이 주어지면 작업디렉토리 및 Stage 영역 둘 다 갱신하지 않습니다.

커밋 이름이 지정되지 않으면 HEAD를 대신 사용합니다. 이 경우에는 브랜치가 가리키는 위치는 변경되지 않고 Stage 영역의 내용(또는 --hard 옵션이 주어지면 작업 디렉토리 까지)이 가장 마지막 커밋의 내용으로 갱신됩니다.

파일 이름을 지정하면 (또는 -p 옵션을 사용하면) Checkout 명령과 비슷한 역할을 합니다. 다만 Stage 영역만 갱신된다는 점이 다릅니다. (어떤 시점의 커밋으로부터 파일을 갱신할 지 HEAD 대신 커밋 이름을 지정하여 선택할 수 있습니다.)

Merge

Merge 명령은 다른 커밋들을 하나로 합쳐서 새로운 커밋을 만듭니다. Merge 명령을 실행하기 전에 Stage 영역에 작업중인 파일이 없는지 꼭 확인해둡니다. Merge하는 경우 중 가장 간단한 경우는 Merge할 대상이 현재 커밋의 직접적인 뿌리가 되는 경우 인데, 이 때는 합칠 내용이 없습니다. 다음은 현재 커밋이 Merge할 대상의 직접적인 뿌리가 되는 경우인데, 이 때는 fast-forward Merge가 실행되는데 간단히 가리키는 지점이 대상 커밋이 되고 대상 커밋의 내용을 Checkout 합니다.

이젠 진짜 Merge를 살펴볼 차례입니다. 다른 Merge 전략을 선택할 수도 있지만 기본적으로 Git은 재귀적인(Recursive) Merge 전략을 사용합니다. 이 전략은 현재 커밋(ed489), 대상이 되는 커밋(33104), 그리고 공통의 뿌리가 되는 커밋(b325c)을 가지고 3-way Merge를 수행합니다. Merge한 결과는 작업 디렉토리와 Stage 영역에 저장되며 부모가 여럿(33104, ed489)인 새 커밋을 만듭니다.

Cherry Pick(열매 고르기)

Cherry-pick 명령은 커밋을 하나 꺼내서 현재 작업중인 브랜치 마지막 부분에 '복사'를 하면서 해당 커밋이 변경하는 부분을 적용하고 메시지나 저자 정보 등의 커밋 정보를 함께 저장합니다.

Rebase

여러 브랜치를 하나로 모으고자 할 때 Rebase 명령을 Merge 명령 대신 사용할 수 있습니다. Merge 명령은 두 부모를 가지는 하나의 새 커밋을 만들기 때문에 히스토리가 직선적이지 않습니다. Rebase 명령을 사용하면 커밋들을 하나씩 순차적으로 적용해나가면서 히스토리를 직선으로 만들 수 있습니다. Cherry-pick 명령을 자동으로 한번에 수행하는 것이라고 보시면 됩니다.

위의 Rebase 명령은 topic 브랜치에만 포함되어 있는 모든 커밋들(169a62c33a)을 main 브랜치에 추가합니다. 커밋들을 추가하고 나서 topic 브랜치가 마지막 커밋을 가리키도록 이동시킵니다. Rebase하고 나서 더 이상 가리킬(Reference) 수 없는 커밋들은 쓰레기통으로 사라집니다.

--onto 옵션을 사용하면 Rebase에 사용할 커밋을 얼마나 오래 전 까지의 커밋을 사용할 지 제한할 수 있습니다. 아래 명령은 169a6 커밋 이후의 모든 커밋들(여기에서는 2c33a 커밋)을 main 브랜치에 적용시킵니다.

추가로 git rebase --interactive 명령이 있는데 간단히 커밋을 적용하는 것 이외에도 더 복잡한 기능, 커밋에 대해서 Namely Dropping, Reordering, Modifying, Squashing을 할 수 있습니다. 이해하기 쉽게 그릴 수 있는 그림이 없어 부득이 메뉴얼 문서 git-rebase(1) 링크를 드립니다.

기술적인 내용

파일의 실제 내용은 사실 Index(.git/index)나 커밋 개체(Commit Object)에 저장되는 것이 아니라, 개체 데이터베이스(Object Database, .git/objects)에 SHA-1 해시로 구분하여 blob형태로 저장이 됩니다. Index는 파일이름의 목록과 파일 blob을 가리키는 Hash를 저장하고 있습니다. 커밋에는 추가로 tree라는 형식의 데이터가 있는데 마찬가지로 Hash로 구분하고 디렉토리 구조를 담고 있습니다. 각 디렉토리는 포함된 파일 목록에 대한 tree 데이터를 담고 있습니다. 각 커밋은 가장 상위 디렉토리에 대한 tree 정보를 갖고 있어 커밋에 포함된 디렉토리 및 파일 정보를 접근할 수 있습니다.

detached HEAD에서 커밋을 만들게 되면 뭔가 만든 커밋을 가리킬 것이 필요한데 HEAD에 대한 reflog를 사용할 수 있습니다. 하지만 이 정보는 시간이 지나면 버려지기 때문에 결국 아무것도 가리키는 것이 없는 커밋은 git commit --amend 명령이나 git rebase 명령으로 버려지는 커밋 처럼 버려지게 됩니다.

실습: 명령어들의 결과 확인

Visualizing Git Concepts with D3를 통해 git 명령어의 결과를 시각적으로 시뮬레이션하는 것처럼, 다음의 실습을 통해 저장소를 변경해 봄으로써 즉시 명령어의 결과를 확인해 볼 수 있습니다. 유용하게 사용되기를 바랍니다.

먼저 임의의 저장소를 생성합니다:

$ git init foo
$ cd foo
$ echo 1 > myfile
$ git add myfile
$ git commit -m "version 1"

이제 편리하게 상태를 확인하기 위해 다음의 함수를 정의합니다:

show_status() {
  echo "HEAD:     $(git cat-file -p HEAD:myfile)"
  echo "Stage:    $(git cat-file -p :myfile)"
  echo "Worktree: $(cat myfile)"
}

initial_setup() {
  echo 3 > myfile
  git add myfile
  echo 4 > myfile
  show_status
}

처음에는 모든 것들의 version 1 상태입니다.

$ show_status
HEAD:     1
Stage:    1
Worktree: 1

add와 commit을 하면 상태가 변경됨을 확인 할 수 있습니다.

$ echo 2 > myfile
$ show_status
HEAD:     1
Stage:    1
Worktree: 2
$ git add myfile
$ show_status
HEAD:     1
Stage:    2
Worktree: 2
$ git commit -m "version 2"
[main 4156116] version 2
 1 file changed, 1 insertion(+), 1 deletion(-)
$ show_status
HEAD:     2
Stage:    2
Worktree: 2

이제 실습을 위해 초기 상태를 만듭니다. 3가지가 모두 다른 상태입니다.

$ initial_setup
HEAD:     2
Stage:    3
Worktree: 4

이제 각 명령어가 어떤 역할을 하는지 확인합니다. 위에 설명된 다이어그램과 일치하는 것을 볼 수 있습니다.

git reset -- myfile은 HEAD에서 stage로 복사합니다:

$ initial_setup
HEAD:     2
Stage:    3
Worktree: 4
$ git reset -- myfile
Unstaged changes after reset:
M   myfile
$ show_status
HEAD:     2
Stage:    2
Worktree: 4

git checkout -- myfile은 stage에서 worktree로 복사합니다:

$ initial_setup
HEAD:     2
Stage:    3
Worktree: 4
$ git checkout -- myfile
$ show_status
HEAD:     2
Stage:    3
Worktree: 3

git checkout HEAD -- myfile은 HEAD에서 stage와 worketree 모두로 복사합니다:

$ initial_setup
HEAD:     2
Stage:    3
Worktree: 4
$ git checkout HEAD -- myfile
$ show_status
HEAD:     2
Stage:    2
Worktree: 2

git commit myfile은 worktree에서 stage와 HEAD 모두로 복사합니다:

$ initial_setup
HEAD:     2
Stage:    3
Worktree: 4
$ git commit myfile -m "version 4"
[main 679ff51] version 4
 1 file changed, 1 insertion(+), 1 deletion(-)
$ show_status
HEAD:     4
Stage:    4
Worktree: 4

Copyright © 2010, Mark Lodato. 한국어 번역 © 2011, Sean Lee.

이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 3.0 미국 라이선스에 따라 이용할 수 있습니다.