14  이력 탐색

앞선 학습에서 살펴봤듯이, 식별자로 커밋을 조회할 수 있다. HEAD 식별자를 사용해서 작업 디렉터리의 가장 최근 커밋을 조회할 수 있다.

mars.txt 파일에 한 번에 한 줄씩 추가했다. 따라서 눈으로 봐도 진행 사항을 쉽게 추적할 수 있다. HEAD를 사용해서 추적 작업을 수행해 보자. 시작 전에 mars.txt 파일에 변경을 가해보자.

$ nano mars.txt
$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity
An ill-considered change

이제, 변경된 사항을 살펴보자.

$ git diff HEAD mars.txt

diff --git a/mars.txt b/mars.txt
index b36abfd..0848c8d 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1,3 +1,4 @@
 Cold and dry, but everything is my favorite color
 The two moons may be a problem for Wolfman
 But the Mummy will appreciate the lack of humidity
+An ill-considered change.

HEAD만 빼면, 앞서 살펴본 것과 동일하다. 이러한 접근법의 정말 좋은 점은 이전 커밋을 조회할 수 있다는 점이다.
~1(“~”은 “틸드(tilde)”, 발음기호 [til-duh])을 추가해서 HEAD 이전 첫 번째 커밋을 조회할 수 있다.

$ git diff HEAD~1 mars.txt

git diff 명령어를 사용해서 이전 커밋과 차이난 점을 보고자 한다면,
HEAD~1, HEAD~2 표기법을 사용해서 조회를 쉽게 할 수 있다:

$ git diff HEAD~2 mars.txt

diff --git a/mars.txt b/mars.txt
index df0654a..b36abfd 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1 +1,4 @@
 Cold and dry, but everything is my favorite color
+The two moons may be a problem for Wolfman
+But the Mummy will appreciate the lack of humidity
+An ill-considered change

git show를 사용해서도 커밋 메시지뿐만 아니라 이전 커밋과 변경사항을 보여준다.
git diff는 작업 디렉터리와 커밋 사이 차이나는 부분을 보여준다.

$ git show HEAD~2 mars.txt

commit 34961b159c27df3b475cfe4415d94a6d1fcd064d
Author: Vlad Dracula <vlad@tran.sylvan.ia>
Date:   Thu Aug 22 10:07:21 2013 -0400

    Start notes on Mars as a base

diff --git a/mars.txt b/mars.txt
new file mode 100644
index 0000000..df0654a
--- /dev/null
+++ b/mars.txt
@@ -0,0 +1 @@
+Cold and dry, but everything is my favorite color

이런 방식으로, 연쇄 커밋 사슬을 구성할 수 있다.
가장 최근 사슬의 끝값은 HEAD로 조회된다.
~ 표기법을 사용하여 이전 커밋을 조회할 수 있다.
그래서 HEAD~1(“head 마이너스 1”으로 읽는다.)은 “바로 앞선 커밋”을 의미하고,
HEAD~123은 지금 있는 위치에서 123번째 이전 수정으로 간다는 의미가 된다.

커밋된 것을 git log 명령어로 화면에 출력되는 숫자와 문자로 구성된 긴 문자열을 사용하여 조회할 수도 있다.
변경사항에 대해서 중복되지 않는 ID로, “중복되지 않는(unique)”의 의미는 정말 유일하다는 의미다.
특정 컴퓨터에 있는 임의 파일 집합에 대한 모든 변경사항은 중복되지 않는 40-문자 식별자가 붙어있다.
첫 번째 커밋은 ID로 f22b25e3233b4645dabd0d81e651fe074bd8e73b가 주어졌다.
그래서 다음과 같이 시도해 보자:

$ git diff f22b25e3233b4645dabd0d81e651fe074bd8e73b mars.txt

diff --git a/mars.txt b/mars.txt
index df0654a..93a3e13 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1 +1,4 @@
 Cold and dry, but everything is my favorite color
+The two moons may be a problem for Wolfman
+But the Mummy will appreciate the lack of humidity
+An ill-considered change

올바른 정답이지만, 40-문자로 된 난수 문자열을 타이핑하는 것은 매우 귀찮은 일이다.
그래서 Git은 앞의 몇 개 문자만으로도 사용할 수 있게 했다:

$ git diff f22b25e mars.txt

diff --git a/mars.txt b/mars.txt
index df0654a..93a3e13 100644
--- a/mars.txt
+++ b/mars.txt
@@ -1 +1,4 @@
 Cold and dry, but everything is my favorite color
+The two moons may be a problem for Wolfman
+But the Mummy will appreciate the lack of humidity
+An ill-considered change

좋았어요!
파일에 변경사항을 저장할 수 있고 변경된 것을 확인할 수 있다.
어떻게 옛 버전 파일을 되살릴 수 있을까?
우연히 파일을 덮어썼다고 가정하자:

$ nano mars.txt  
$ cat mars.txt

We will need to manufacture our own oxygen

이제 git status를 통해서 파일이 변경되었다고 하지만,
변경사항은 아직 준비영역(Staging area)에 옮겨지지 않은 것으로 확인된다:

$ git status

On branch master
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)

    modified:   mars.txt

no changes added to commit (use "git add" and/or "git commit -a")

git checkout 명령어를 사용해서 과거에 있던 상태로 파일을 되돌릴 수 있다:

$ git checkout HEAD mars.txt
$ cat mars.txt

Cold and dry, but everything is my favorite color
The two moons may be a problem for Wolfman
But the Mummy will appreciate the lack of humidity

이름에서 유추할 수 있듯이, git checkout 명령어는 파일의 옛 버전을 확인하고 가져온다. 즉, 되살린다.
이 경우 HEAD에 기록된 가장 최근에 저장된 파일 버전을 되살린다.
더 오래된 버전을 되살리고자 한다면, 대신에 커밋 식별자를 사용한다:

$ git checkout f22b25e mars.txt
$ cat mars.txt

Cold and dry, but everything is my favorite color
$ git status

# On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
# 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)
#
#   modified:   mars.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

변경사항은 준비영역에 머물러 있는 것에 주목한다. 다시, git checkout 명령어를 사용해서 이전 버전으로 되돌아간다:

$ git checkout HEAD mars.txt
헤드(HEAD)를 잃지 말자

f22b25e 커밋 상태로 mars.txt 파일을 되돌리는데 앞서 다음 명령어를 사용했다.

$ git checkout f22b25e mars.txt

하지만 주의하자! checkout 명령어는 다른 중요한 기능을 가지고 있어서, 만약 타이핑에 오류가 있다면 Git이 의도를 오해할 수 있다.
예를 들어, 앞선 명령에서 mars.txt를 빼먹게 되면…

$ git checkout f22b25e

Note: checking out 'f22b25e'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
 git checkout -b <new-branch-name>
HEAD is now at f22b25e Start notes on Mars as a base

“detached HEAD”는 “보기는 하지만 건드리지는 마시오”와 같다. 따라서 현재 상태에서 어떤 변경도 만들지 말아야 한다. 저장소의 지난 상태를 살펴본 후에 git checkout master 명령어로 HEAD를 다시 붙인다.

실행 취소를 하는 변경사항을 만들기 전에 저장소 상태를 확인하는 커밋 번호를 사용해야 한다는 것을 기억하는 것이 중요하다.
흔한 실수는 커밋 번호를 사용하지 않는 것이다. 아래 예제에서는 커밋 번호가 f22b25e인 가장 최신 커밋(HEAD~1) 이전의 상태로 다시 되돌리고자 한다:

그림 14.1: Git 복원(Checkout)

그래서, 모두 한데 모아보자.

그림 14.2: Git 동작방식 도식화
흔한 사례 단순화

git status 출력 결과를 주의 깊게 읽게 되면, 힌트가 포함된 것을 볼 수 있다.

(use "git checkout -- <file>..." to discard changes in working directory)

출력 결과가 언급하는 바는, 버전 식별자 없이 git checkout 명령어를 실행하게 되면
HEAD에 저장된 상태로 파일을 원복시킨다는 것이다. 더블 대시 --가 필요한 경우는 명령어 자체로부터 복구해야 되는 파일명을 구별할 때다. 없는 경우, Git은 커밋 식별자에 파일명을 사용한다.

파일이 하나씩 옛 상태로 되돌린다는 사실이 사람들이 작업을 조직하는 방식에 변화를 주는 경향이 있다.
모든 것이 하나의 큰 문서로 되어 있다면,
나중에 결론 부분에 변경사항을 실행 취소하지 않고, 소개 부분에 변경을 다시 되돌리기가 쉽지 않다(하지만 불가능하지는 않다).
다른 한편으로 만약 소개 부분과 결론 부분이 다른 파일에 저장되어 있다면,
시간 앞뒤로 이동하기가 훨씬 쉽다.

파일의 이전 버전 복구하기

정훈이가 몇 주 동안 작업한 파이썬 스크립트에 변경을 했고, 오늘 아침 정훈이가 작업한 변경 사항이 스크립트를 “망가뜨려서” 더 이상 실행이 되지 않는다. 백업도 없이, 버그를 고치는 데 1시간 이상 소모했다…

다행스럽게도, Git을 사용한 프로젝트 버전을 추적하고 있었다! 다음 아래 명령어 중 어떤 것이 data_cruncher.py로 불리는 파이썬 스크립트의 가장 최근 버전을
복구하게 할까?

  1. $ git checkout HEAD
  2. $ git checkout HEAD data_cruncher.py
  3. $ git checkout HEAD~1 data_cruncher.py
  4. $ git checkout <unique ID of last commit> data_cruncher.py
  5. 2번과 4번 모두

정답은 (5)-2번과 4번 모두.

checkout 명령어는 저장소에서 파일을 복원하여 작업 디렉토리에 있는 파일을 덮어쓴다. 답안 2번과 4번은 모두 저장소에 있는 data_cruncher.py 파일의 최신 버전을 복원한다. 답안 2번은 최신 버전을 나타내기 위해 HEAD를 사용하고, 답안 4번은 HEAD가 의미하는 바로 그 마지막 커밋의 고유한 ID를 사용한다는 차이만 있다.

답안 3번은 HEAD 이전 커밋에서 data_cruncher.py의 버전을 가져오는데, 이는 원하는 바가 아니다.

답안 1번은 위험할 수 있다! 파일명이 없으면 git checkout은 현재 디렉토리(및 그 아래의 모든 디렉토리)에 있는 모든 파일을 지정된 커밋 상태로 복원한다. 이 명령어는 data_cruncher.py를 최신 커밋 버전으로 복원하지만, 변경된 다른 모든 파일도 해당 버전으로 복원하여 해당 파일들에 대해 수행했을 수 있는 모든 변경 사항을 지워버린다! 위에서 논의했듯이, HEAD가 분리된 상태로 남게 되는데, 그런 상태에 놓여지는 것은 위험하다.

커밋 되돌리기

정훈이는 동료와 함께 파이썬 코드를 협업해서 작성하고 있다. 그룹 저장소에 마지막으로 커밋한 것이 잘못된 것을 알게 되어서, 실행 취소하여 원복하고자 한다.

정훈이는 실행 취소를 올바르게 해서 그룹 저장소를 사용하는
모든 구성원이 제대로 된 변경사항을 가지고 작업을 계속하길 원한다. git revert [잘못된 커밋 ID] 명령어는 정훈이가 이전에 잘못 커밋했던 작업에 대해 실행 취소하는 커밋을 새로 생성시킨다.

따라서, git revertgit checkout [커밋 ID]와 다른데 이유는 checkout이 그룹 저장소에 커밋되지 않는 로컬 변경사항에
대해서 적용된다는 점에서 차이가 난다. 정훈이가 git revert를 사용할 올바른 절차와 설명이 아래에 나와 있다. 빠진 명령어가 무엇일까?

  1. `________ # 커밋 ID를 찾을 수 있도록 Git 프로젝트 이력을 살펴본다.
  2. ID를 복사한다. (ID의 첫 문자 몇 개만 사용한다. 예를 들어, 0b1d055).
  3. git revert [커밋 ID]
  4. 새로운 커밋 메시지를 타이핑한다.
  5. 저장하고 종료한다.

명령어 git log는 커밋 ID와 함께 프로젝트 이력을 나열한다.

명령어 git show HEAD는 최신 커밋에서 이루어진 변경 사항을 보여주고, 커밋 ID를 나열한다. 그러나 정훈이는 그것이 정확한 커밋인지, 그리고 다른 누군가 저장소에 변경 사항을 커밋하지 않았는지 다시 한 번 확인해야 한다.

작업흐름과 이력 이해하기

다음 마지막 명령의 출력 결과는 무엇일까?

$ cd planets
$ echo "Venus is beautiful and full of love" > venus.txt
$ git add venus.txt
$ echo "Venus is too hot to be suitable as a base" >> venus.txt
$ git commit -m "Comment on Venus as an unsuitable base"
$ git checkout HEAD venus.txt
$ cat venus.txt #this will print the contents of venus.txt to the screen
  1.    Venus is too hot to be suitable as a base
  2.    Venus is beautiful and full of love
  3.    Venus is beautiful and full of love
       Venus is too hot to be suitable as a base
  4.    Error because you have changed venus.txt without committing the changes

정답은 2번이다.

git add venus.txt 명령어는 venus.txt의 현재 버전을 준비 영역에 올려놓는다. 두 번째 echo 명령어로 인한 파일의 변경사항은 작업 복사본에만 적용되고, 준비 영역에 있는 버전에는 적용되지 않는다.

따라서 git commit -m "Comment on Venus as an unsuitable base"가 실행될 때, 저장소에 커밋되는 venus.txt의 버전은 준비 영역에 있는 것으로, 한 줄만 가지고 있다.

이때 작업 복사본은 여전히 두 번째 줄을 가지고 있고(git status는 파일이 수정되었음을 보여줄 것이다). 그러나 git checkout HEAD venus.txt는 작업 복사본을 venus.txt의 가장 최근에 커밋된 버전으로 대체한다.

그래서 cat venus.txt는 다음과 같이 출력될 것이다.

Venus is beautiful and full of love.
git diff 이해 확인하기

git diff HEAD~3 mars.txt 명령어를 고려해 보자. 이 명령어를 실행하게 되면 실행 결과로 예상하는 바를 말해보자. 명령어를 실행하게 되면 어떤 일이 발생하는가? 그리고 이유는 무엇인가?

또 다른 명령어 git diff [ID] mars.txt를 시도해 보자.
여기서, [ID]를 가장 최근 커밋 식별자로 치환한다. 무슨 일이 생길까? 그리고 실제로 생긴 일은 무엇인가?

준비 단계 변경사항(Staged Changes) 제거하기

git checkout 명령어를 통해서 준비영역으로 올라오지 않은 변경사항이 있을 때, 이전 커밋을 복구할 수 있었다. 하지만, git checkout은 준비영역에 올라왔지만, 커밋되지 않는 변경사항에 대해서도 동작한다. mars.txt 파일에 변경사항을 만들고, 변경사항을 추가하고 나서,
git checkout 명령어를 사용하게 되면 변경사항이 사라졌는지 살펴보자.

변경사항을 추가한 후에는 git checkout을 직접 사용할 수 없다. git status 출력 결과를 살펴보자.

On branch main
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   mars.txt

동일한 출력 결과가 나오지 않는다면 파일 변경을 잊었거나, 추가하고 커밋까지 한 상태일 수 있다.

이 상태에서 git checkout -- mars.txt 명령어를 사용하면 오류는 발생하지 않지만, 파일도 복원되지 않는다. Git은 파일을 unstage하기 위해 먼저 git reset을 사용해야 한다고 친절하게 알려준다.

$ git reset HEAD mars.txt

Unstaged changes after reset:
M   mars.txt

이제 git status를 실행하면 다음과 같은 결과가 화면에 출력된다.

$ git status

On branch main
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)

      modified:   mars.txt

no changes added to commit (use "git add" and/or "git commit -a")

이는 이제 git checkout을 사용하여 파일을 이전 커밋 상태로 복원할 수 있다는 것을 의미하게 된다.


$ git checkout -- mars.txt
$ git status

On branch main
nothing to commit, working tree clean
변경 이력 탐색과 요약

변경 이력 탐색은 Git에 있어 중요한 부분 중의 하나로,
특히 커밋이 수개월 전에 이뤄졌다면, 올바른 커밋 ID를 찾는 것이 종종 크나큰 도전과제가 된다. planets 프로젝트가 50개 파일 이상으로 구성되었다고 상상해 보자.

mars.txt 파일에 특정 텍스트가 변경된 커밋을 찾고자 한다. git log를 타이핑하게 되면 매우 긴 목록이 출력된다. 어떻게 하면 검색 범위를 좁힐 수 있을까? git diff 명령어가 특정 파일만 탐색할 수 있다는 점을 상기하자.

예를 들어, git diff mars.txt. 이 문제에 유사한 아이디어를 적용해 보자.

$ git log mars.txt

불행하게도 커밋 메시지 일부는 매우 애매모호하다. 예를 들어, update files. 어떻게 하면 파일을 잘 검색할 수 있을까? git diff, git log 명령어 모두 매우 유용하다. 두 명령어 모두 변경 이력의 다른 부분을 요약해 준다. 둘을 조합하는 것은 가능할까? 다음 명령어를 실행해 보자:

$ git log --patch mars.txt

엄청 긴 출력 목록이 나타난다. 각 커밋마다 커밋 메시지와 차이가 쭉 출력된다. 질문: 다음 명령어는 무슨 작업을 수행할까요?

$ git log --patch HEAD~3 *.txt