-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-history.qmd
516 lines (389 loc) · 18.5 KB
/
git-history.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
---
editor:
markdown:
wrap: sentence
---
# 이력 탐색 {#git-history}
\index{버전 관리!이력}
앞선 학습에서 살펴봤듯이, 식별자로 커밋을 조회할 수 있다.
`HEAD` 식별자를 사용해서 작업 디렉터리의 *가장 최근 커밋*을 조회할 수 있다.
`mars.txt` 파일에 한 번에 한 줄씩 추가했다.
따라서 눈으로 봐도 진행 사항을 쉽게 추적할 수 있다.
`HEAD`를 사용해서 추적 작업을 수행해 보자.
시작 전에 `mars.txt` 파일에 변경을 가해보자.
``` bash
$ 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
```
이제, 변경된 사항을 살펴보자.
``` bash
$ 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**-d*uh*\])을 추가해서 `HEAD` 이전 첫 번째 커밋을 조회할 수 있다. \index{git!HEAD} \index{git!diff}
``` bash
$ git diff HEAD~1 mars.txt
```
`git diff` 명령어를 사용해서 이전 커밋과 차이난 점을 보고자 한다면,\
`HEAD~1`, `HEAD~2` 표기법을 사용해서 조회를 쉽게 할 수 있다:
``` bash
$ 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`는 작업 디렉터리와 커밋 사이 *차이나는 부분*을 보여준다.
\index{git!show}
``` bash
$ 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가 주어졌다.\
그래서 다음과 같이 시도해 보자:
``` bash
$ 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은 앞의 몇 개 문자만으로도 사용할 수 있게 했다:
``` bash
$ 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
```
좋았어요!\
파일에 변경사항을 저장할 수 있고 변경된 것을 확인할 수 있다.\
어떻게 옛 버전 파일을 되살릴 수 있을까?\
우연히 파일을 덮어썼다고 가정하자:
``` bash
$ nano mars.txt
$ cat mars.txt
We will need to manufacture our own oxygen
```
이제 `git status`를 통해서 파일이 변경되었다고 하지만,\
변경사항은 아직 준비영역(Staging area)에 옮겨지지 않은 것으로 확인된다: \index{git!status}
``` bash
$ 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` 명령어를 사용해서 과거에 있던 상태로 파일을 되돌릴 수 있다:
``` bash
$ 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`에 기록된 가장 최근에 저장된 파일 버전을 되살린다.\
더 오래된 버전을 되살리고자 한다면, 대신에 커밋 식별자를 사용한다: \index{git!checkout}
``` bash
$ git checkout f22b25e mars.txt
```
``` bash
$ cat mars.txt
Cold and dry, but everything is my favorite color
```
``` bash
$ 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` 명령어를 사용해서 이전 버전으로 되돌아간다:
``` bash
$ git checkout HEAD mars.txt
```
::: callout-tip
### 헤드(HEAD)를 잃지 말자
`f22b25e` 커밋 상태로 `mars.txt` 파일을 되돌리는데 앞서 다음 명령어를 사용했다.
``` bash
$ git checkout f22b25e mars.txt
```
하지만 주의하자!
`checkout` 명령어는 다른 중요한 기능을 가지고 있어서, 만약 타이핑에 오류가 있다면 Git이 의도를 오해할 수 있다.\
예를 들어, 앞선 명령에서 `mars.txt`를 빼먹게 되면...
``` bash
$ 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`) 이전의 상태로 다시 되돌리고자 한다:
![Git 복원(Checkout)](images/git/git-checkout.svg){#fig-git-checkout fig-align="center" width="450"}
그래서, 모두 한데 모아보자.
![Git 동작방식 도식화](images/git/git_staging.svg){#fig-git-checkout-cartoon fig-align="center" width="450"}
::: callout-tip
### 흔한 사례 단순화
`git status` 출력 결과를 주의 깊게 읽게 되면, 힌트가 포함된 것을 볼 수 있다.
``` bash
(use "git checkout -- <file>..." to discard changes in working directory)
```
출력 결과가 언급하는 바는, 버전 식별자 없이 `git checkout` 명령어를 실행하게 되면\
`HEAD`에 저장된 상태로 파일을 원복시킨다는 것이다.
더블 대시 `--`가 필요한 경우는 명령어 자체로부터 복구해야 되는 파일명을 구별할 때다.
없는 경우, Git은 커밋 식별자에 파일명을 사용한다.
:::
파일이 하나씩 옛 상태로 되돌린다는 사실이 사람들이 작업을 조직하는 방식에 변화를 주는 경향이 있다.\
모든 것이 하나의 큰 문서로 되어 있다면,\
나중에 결론 부분에 변경사항을 실행 취소하지 않고, 소개 부분에 변경을 다시 되돌리기가 쉽지 않다(하지만 불가능하지는 않다).\
다른 한편으로 만약 소개 부분과 결론 부분이 다른 파일에 저장되어 있다면,\
시간 앞뒤로 이동하기가 훨씬 쉽다.
::: callout-tip
### 파일의 이전 버전 복구하기
정훈이가 몇 주 동안 작업한 파이썬 스크립트에 변경을 했고, 오늘 아침 정훈이가 작업한 변경 사항이 스크립트를 "망가뜨려서" 더 이상 실행이 되지 않는다.
백업도 없이, 버그를 고치는 데 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번 모두
::: {.callout-caution collapse="true"}
### 해답
정답은 (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`가 분리된 상태로 남게 되는데, 그런 상태에 놓여지는 것은 위험하다.
:::
:::
::: callout-tip
### 커밋 되돌리기
정훈이는 동료와 함께 파이썬 코드를 협업해서 작성하고 있다.
그룹 저장소에 마지막으로 커밋한 것이 잘못된 것을 알게 되어서, 실행 취소하여 원복하고자 한다.
정훈이는 실행 취소를 올바르게 해서 그룹 저장소를 사용하는\
모든 구성원이 제대로 된 변경사항을 가지고 작업을 계속하길 원한다.
`git revert [잘못된 커밋 ID]` 명령어는 정훈이가 이전에 잘못 커밋했던 작업에 대해 실행 취소하는 커밋을 새로 생성시킨다.
\index{git!revert}
따라서, `git revert`는 `git checkout [커밋 ID]`와 다른데 이유는 `checkout`이 그룹 저장소에 커밋되지 않는 로컬 변경사항에\
대해서 적용된다는 점에서 차이가 난다.
정훈이가 `git revert`를 사용할 올바른 절차와 설명이 아래에 나와 있다.
빠진 명령어가 무엇일까?
1. \`\_\_\_\_\_\_\_\_ \# 커밋 ID를 찾을 수 있도록 Git 프로젝트 이력을 살펴본다.
2. ID를 복사한다. (ID의 첫 문자 몇 개만 사용한다. 예를 들어, 0b1d055).
3. `git revert [커밋 ID]`
4. 새로운 커밋 메시지를 타이핑한다.
5. 저장하고 종료한다.
::: {.callout-caution collapse="true"}
### 해답
명령어 `git log`는 커밋 ID와 함께 프로젝트 이력을 나열한다.
명령어 `git show HEAD`는 최신 커밋에서 이루어진 변경 사항을 보여주고, 커밋 ID를 나열한다.
그러나 정훈이는 그것이 정확한 커밋인지, 그리고 다른 누군가 저장소에 변경 사항을 커밋하지 않았는지 다시 한 번 확인해야 한다.
:::
:::
::: callout-tip
### 작업흐름과 이력 이해하기
다음 마지막 명령의 출력 결과는 무엇일까?
``` bash
$ 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.
``` bash
Venus is too hot to be suitable as a base
```
2.
``` bash
Venus is beautiful and full of love
```
3.
``` bash
Venus is beautiful and full of love
Venus is too hot to be suitable as a base
```
4.
``` bash
Error because you have changed venus.txt without committing the changes
```
::: {.callout-caution collapse="true"}
### 해답
정답은 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`는 다음과 같이 출력될 것이다.
``` bash
Venus is beautiful and full of love.
```
:::
:::
::: callout-tip
### `git diff` 이해 확인하기
`git diff HEAD~3 mars.txt` 명령어를 고려해 보자.
이 명령어를 실행하게 되면 실행 결과로 예상하는 바를 말해보자.
명령어를 실행하게 되면 어떤 일이 발생하는가?
그리고 이유는 무엇인가?
\index{git!diff}
또 다른 명령어 `git diff [ID] mars.txt`를 시도해 보자.\
여기서, \[ID\]를 가장 최근 커밋 식별자로 치환한다.
무슨 일이 생길까?
그리고 실제로 생긴 일은 무엇인가?
:::
::: callout-tip
### 준비 단계 변경사항(Staged Changes) 제거하기
`git checkout` 명령어를 통해서 준비영역으로 올라오지 않은 변경사항이 있을 때, 이전 커밋을 복구할 수 있었다.
하지만, `git checkout`은 준비영역에 올라왔지만, 커밋되지 않는 변경사항에 대해서도 동작한다.
`mars.txt` 파일에 변경사항을 만들고, 변경사항을 추가하고 나서,\
`git checkout` 명령어를 사용하게 되면 변경사항이 사라졌는지 살펴보자.
::: {.callout-caution collapse="true"}
### 해답
변경사항을 추가한 후에는 `git checkout`을 직접 사용할 수 없다.
`git status` 출력 결과를 살펴보자.
``` bash
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`을 사용해야 한다고 친절하게 알려준다.
\index{git!reset}
``` bash
$ git reset HEAD mars.txt
Unstaged changes after reset:
M mars.txt
```
이제 `git status`를 실행하면 다음과 같은 결과가 화면에 출력된다.
``` bash
$ 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`을 사용하여 파일을 이전 커밋 상태로 복원할 수 있다는 것을 의미하게 된다.
``` bash
$ git checkout -- mars.txt
$ git status
On branch main
nothing to commit, working tree clean
```
:::
:::
::: callout-tip
### 변경 이력 탐색과 요약
변경 이력 탐색은 Git에 있어 중요한 부분 중의 하나로,\
특히 커밋이 수개월 전에 이뤄졌다면, 올바른 커밋 ID를 찾는 것이 종종 크나큰 도전과제가 된다.
`planets` 프로젝트가 50개 파일 이상으로 구성되었다고 상상해 보자.
`mars.txt` 파일에 특정 텍스트가 변경된 커밋을 찾고자 한다.
`git log`를 타이핑하게 되면 매우 긴 목록이 출력된다.
어떻게 하면 검색 범위를 좁힐 수 있을까?
`git diff` 명령어가 특정 파일만 탐색할 수 있다는 점을 상기하자.
예를 들어, `git diff mars.txt`.
이 문제에 유사한 아이디어를 적용해 보자.
``` bash
$ git log mars.txt
```
불행하게도 커밋 메시지 일부는 매우 애매모호하다.
예를 들어, `update files`.
어떻게 하면 파일을 잘 검색할 수 있을까?
`git diff`, `git log` 명령어 모두 매우 유용하다.
두 명령어 모두 변경 이력의 다른 부분을 요약해 준다.
둘을 조합하는 것은 가능할까?
다음 명령어를 실행해 보자:
``` bash
$ git log --patch mars.txt
```
엄청 긴 출력 목록이 나타난다.
각 커밋마다 커밋 메시지와 차이가 쭉 출력된다.
질문: 다음 명령어는 무슨 작업을 수행할까요?
``` bash
$ git log --patch HEAD~3 *.txt
```
:::