티스토리 뷰

728x90
반응형

Git 파일의 라이프 사이클

Git을 사용하고 있다면, 파일을 수정하다가 현재까지 작업한 내용을 저장하고 싶을 때 스냅샷을 커밋하면 된다(여기서 수정은 파일의 생성과 삭제를 모두 포함). 이를 위해 워킹 디렉토리(Working Directory or Tree)에서 작업한 파일들을 Staging Area에 올려 스냅샷에 포함시켜야 한다. 방법은 간단하다.

스냅샷(Snapshot) : Staging Area에 올라간 파일들의 최상위 디렉토리(트리) - 이전 글 참고

워킹 디렉토리의 모든 파일은 크게 Tracked(관리대상)Untracked(관리대상이 아님)로 나눈다. Tracked 파일은 이미 스냅샷에 포함돼 있던 파일이다.

Tracked 파일은 또 Unmodified(수정하지 않음)Modified(수정함) 그리고 Staged(커밋으로 저장소에 기록할) 상태 중 하나다. 간단히 말하자면 Git이 알고 있는 파일이라는 것이다.

Tracked 파일 : 스냅샷에 포함돼 있는 파일. 즉, Staging Area에 올라가 Git에 의해 관리되고 있는 파일

그리고 나머지 파일은 모두 Untracked 파일이다. Untracked 파일은 워킹 디렉토리에 있는 파일 중 스냅샷에도 Staging Area에도 포함되지 않은 파일이다. 처음 저장소를 Clone 하면 모든 파일은 Tracked이면서 Unmodified 상태다.

마지막 커밋 이후 아직 아무것도 수정하지 않은 상태에서 어떤 파일을 수정하면 Git은 그 파일을 Modified 상태로 인식한다. 실제로 커밋을 하기 위해서는 이 수정한 파일을 Staged 상태로 만들고, Staged 상태의 파일을 커밋한다. 이런 라이프사이클을 계속 반복한다.

파일의 라이프 사이클

간단한 예시로 위 파일의 라이프 사이클을 따라가 보겠다.

Untracked

➜  git-study git:(master) git status
현재 브랜치 master
커밋할 사항 없음, 작업 폴더 깨끗함
➜  git-study git:(master) vim test.txt
➜  git-study git:(master) ✗ mkdir commit-directory
➜  git-study git:(master) ✗ vim commit-directory/test2.txt
➜  git-study git:(master) ✗ git status
현재 브랜치 master
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    test.txt
    commit-directory/

커밋할 사항을 추가하지 않았지만 추적하지 않는 파일이 있습니다 (추적하려면 "git
add"를 사용하십시오)

워킹 디렉토리가 깨끗한 상태에서 총 2개의 파일을 생성했다. 그 중 하나는 디렉토리를 하나 추가한 뒤 그 안에 생성했다.

  • test.txt
  • commit-directory/test2.txt

그 다음 현재 상태를 확인해 보면, 새로 생성한 파일들이 아직 추적되지 않은 상태라고 알려준다(track이 추적하다라고 번역됨). 다만 Git에서는 디렉토리도 tree라는 객체로 다루고, 이 tree 안에 추가한 파일 객체(정확히는 객체의 링크 주소)가 포함되는 형태이기 때문에 추가한 디렉토리를 그대로 표시해 준다. 이 상태에서 test.txt 파일만 Staging Area에 올려 Staged 상태로 만들어 보겠다.

git status : Staging Area와 워킹 디렉토리를 비교해 달라진 부분을 표시해 주는 명령어

Staged

➜  git-study git:(master) ✗ git add test.txt
➜  git-study git:(master) ✗ git status
현재 브랜치 master
커밋할 변경 사항:
  (use "git restore --staged <file>..." to unstage)
    새 파일:       test.txt

추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    commit-directory/

커밋할 변경 사항에 test.txt가 추가됐다. 따라서 현재 워킹 디렉토리의 상황은 이렇다.

  • test.txt : Staging Area에 올라간 상태 == Tracked 파일 == Git에 의해 관리되고 있는 파일 == Staged
  • commit-directory/test2.txt : Untracked 파일

Unmodified (commit)

➜  git-study git:(master) ✗ git commit -m "커밋 테스트를 위한 커밋-1"    
[master 8d21d31] 커밋 테스트를 위한 커밋-1
 1 file changed, 2 insertions(+)
 create mode 100644 test.txt

➜  git-study git:(master) ✗ git log

commit 8d21d31ec45206cc2fa021ec7bbc943fad9632fc (HEAD -> master)
Author: on1ystar <tjdwls0607@naver.com>
Date:   Thu Oct 24 17:39:31 2024 +0900

    커밋 테스트를 위한 커밋-1

git commit 명령어를 사용해 Staging Area에 있는 파일들을 커밋했다. 이 순간 새로운 commit 객체가 생성되고, 그 안에 test.txt 파일이 포함되어 있는 스냅샷 링크 주소가 포함될 것이다. 이 전 포스팅에서 한 번 해봤듯이 생성된 commit 의 링크 주소인 sha-1을 조회해 볼 수 있다. 그리고 그 안에 있는 tree 객체의 링크 주소까지 조회해 보겠다.

➜  git-study git:(master) ✗ git cat-file -p 8d21d31ec45206cc2fa021ec7bbc943fad9632fc
tree 9ae300a719831c469963aae88287347c75103047
author on1ystar <tjdwls0607@naver.com> 1729759171 +0900
committer on1ystar <tjdwls0607@naver.com> 1729759171 +0900

커밋 테스트를 위한 커밋-1

➜  git-study git:(master) ✗ git cat-file -p 9ae300a719831c469963aae88287347c75103047
100644 blob dd4c20b86bdac91798f87657a48e2f83af4fea82    test.txt

새로 생성된 commit 객체에 포함된 tree 객체의 링크 주소 및 메타 데이터들을 확인할 수 있고, tree 객체 안에는 예상대로 test.txt 파일의 링크 주소(Git에서는 blob 객체)가 포함되어 있다. (이에 대한 자세한 내용은 이전 포스팅 참고)

➜  git-study git:(master) ✗ git status
현재 브랜치 master
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    commit-directory/

커밋할 사항을 추가하지 않았지만 추적하지 않는 파일이 있습니다 (추적하려면 "git
add"를 사용하십시오)

git status 로는 이제 test.txt 파일이 보이지 않는다. Unmodified 상태의 파일들은 표시하지 않기 때문이다. 마지막으로 test.txt 파일을 살짝 수정해 보겠다.

Modified

➜  git-study git:(master) ✗ vim test.txt 
➜  git-study git:(master) ✗ git status
현재 브랜치 master
커밋하도록 정하지 않은 변경 사항:
  (무엇을 커밋할지 바꾸려면 "git add <파일>..."을 사용하십시오)
  (use "git restore <file>..." to discard changes in working directory)
    수정함:        test.txt

추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    commit-directory/

커밋할 변경 사항을 추가하지 않았습니다 ("git add" 및/또는 "git commit -a"를
사용하십시오)

test.txt 파일이 수정함으로 표시되는데 이 상태가 modified 상태다. Git이 이전 commit과 비교했더니 달라진 부분이 생겼기 때문이다.

그러면 만약에 Git이 이 파일을 다시 관리하지 않도록 하는(Staging Area에 내리는) 방법이 있을까? 물론 있다. 또한 commit을 되돌리는 방법도 있다. Git은 모든 파일들을 스냅샷의 스트림으로 관리하는 방식이기 때문에 사용자가 원하면 어떤 순간이든 돌아갈 수 있다. 그것도 매우 빠르고 쉽게 가능해 Git의 큰 장점 중 하나다. 물론, 어디까지나 Git이 관리하고 있는 파일들이며, 스냅샷에 포함된 적이 있어야 한다.

파일의 상태 되돌리기

파일의 상태들을 되돌리기 전에 알아야 할 것이 있다. Git에서는 이전 상태도 되돌리는 것은 쉽다. 하지만 되돌린 것을 다시 복구하기는 쉽지 않다. 특히나 커밋되지 않은 경우에는 복구할 방법이 없다. 때문에 되돌리기를 위한 명령어들은 더 자세히 알 필요가 있다.

restore : Modified 파일 되돌리기(수정사항 되돌리기)

이전에 Modified 예시를 보면, Git에서 친절하게 아래 메세지를 출력해 준다.

  • use "git restore <file>..." to discard changes in working directory

워킹 디렉토리의 변경 사항을 버리러면 git restore 명령어를 사용하라고 한다. 한 번 해보자.

➜  git-study git:(master) ✗ git restore test.txt
➜  git-study git:(master) ✗ git status
현재 브랜치 master
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    commit-directory/

커밋할 사항을 추가하지 않았지만 추적하지 않는 파일이 있습니다 (추적하려면 "git
add"를 사용하십시오)

test.txt 파일이 사라졌다. 실제로 파일 내용을 확인해 보면, 수정한 부분이 모두 사라진 것을 확인할 수 있다. 이는 Git이 관리하고 있는 파일이면서, 이전에 Commit된 적이 있는 Modified 상태여서 가능한 것이다.

Git이 이전 커밋의 스냅샷에서 test.txt 파일의 최신 상태를 가져과 간단하게 덮어씌우는 것이다. 때문에 아쉽게도 수정한 내용을 다시 되돌릴 수는 없다.

💡 혹여나 restore 명령어가 먹히지 않는다면 Git 버전이 2.23 이전인지 확인해 봐야 한다. 이전 버전에서는 checkout 명령어가 이를 대체했다. 하지만 checkout 명령어의 역할이 너무 많아 restore, switch 명령어로 역할을 나눠 적재적소에 사용하도록 개선했다. - Highlights from Git 2.23

restore --staged : 파일 상태를 Unstage로 변경하기

다음은 Staging Area와 워킹 디렉토리 사이를 넘나드는 방법이다. 새로운 파일을 생성한 뒤, Staging Area에 추가해 보자.

➜  git-study git:(master) vim test3.txt
➜  git-study git:(master) ✗ git add .         
➜  git-study git:(master) ✗ git status
현재 브랜치 master
커밋할 변경 사항:
  (use "git restore --staged <file>..." to unstage)
    새 파일:       test3.txt

이전에 사용했던 restore 명령어에 --statged 옵션만 추가해 주면 된다.

➜  git-study git:(master) ✗ git restore --staged test3.txt 
➜  git-study git:(master) ✗ git status
현재 브랜치 master
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
    test3.txt

커밋할 사항을 추가하지 않았지만 추적하지 않는 파일이 있습니다 (추적하려면 "git
add"를 사용하십시오)
💡 참고로 원래는 reset 명령어를 사용해야만 했다. 하지만 위험 부담이 크기 때문에 2.23 버전부터 추가된 restore 명령어를 사용하라고 Git에서 추천하고 있다.

commit --amend : 최신 커밋 간단 수정

이전의 커밋을 완전히 새로 고쳐서 새 커밋으로 변경하는 것을 의미한다. 이전의 커밋은 일어나지 않은 일이 되는 것이고 당연히 히스토리에도 남지 않는다. 만약 마지막으로 커밋하고 나서 수정한 것이 없다면(커밋하자마자 바로 이 명령을 실행하는 경우) 조금 전에 한 커밋과 모든 것이 같다.

우리가 아직 Staging Area에 추가하지 않은 파일이 있었다. 이 파일을 이전 커밋에 추가해 보자.

➜  git-study git:(master) ✗ git add commit-directory/
➜  git-study git:(master) ✗ git commit --amend -m "commit --amend 테스트를 위한 커밋"
[master ed84b90] commit --amend 테스트를 위한 커밋
 Date: Thu Oct 24 17:39:31 2024 +0900
 2 files changed, 3 insertions(+)
 create mode 100644 commit-directory/test2.txt
 create mode 100644 test.txt
➜  git-study git:(master) git status
현재 브랜치 master
커밋할 사항 없음, 작업 폴더 깨끗함

워킹 디렉토리가 깨끗해 졌다(이전 예시에서 새로 생성한 test3.txt 파일은 삭제함). 커밋 로그를 보면 commit-directory/test2.txt 파일이 추가된 것을 확인할 수 있다. 그런데 이전 커밋에 추가했던 test.txt 파일도 새로 추가된 것처럼 보인다. 그럼 이전 커밋은 어떻게 됐을까? 직접 다 확인해 보자.

➜  git-study git:(master) git log

commit e8abaeb47ecf55eb8693ae7fd35b5a906d40a378 (HEAD -> master)
Author: on1ystar <tjdwls0607@naver.com>
Date:   Thu Oct 24 17:39:31 2024 +0900

    commit --amend 테스트를 위한 커밋

가장 먼저 보이는 차이점은 이전 커밋이 사라지고 새로운 커밋이 만들어 졌다는 것이다. --amend 옵션은 이전 커밋 정보에 새로운 커밋 정보를 덮어씌우는 옵션이다.

이전 예시에서 이전 커밋 로그를 가져와 비교해 보자.

➜  git-study git:(master) ✗ git log

commit 8d21d31ec45206cc2fa021ec7bbc943fad9632fc (HEAD -> master)
Author: on1ystar <tjdwls0607@naver.com>
Date:   Thu Oct 24 17:39:31 2024 +0900

    커밋 테스트를 위한 커밋-1

git log 에서 확인할 수 있는 차이점은 커밋 객체의 sha-1 체크섬(링크 주소) 값과 새로 작성한 커밋 메세지다. 즉, 완전 새로운 커밋 객체가 생성됐다는 것이다. Date 값을 보면 같은 것을 확인할 수 있는데, 이는 이전 커밋의 메타 데이터 값을 그대로 사용하기 때문이다. 만약 커밋 메세지를 새로 작성하지 않았다면 이 역시 같았을 것이다.

➜  git-study git:(master) git cat-file -p e8abaeb47ecf55eb8693ae7fd35b5a906d40a378
tree 2961b358eca986a3e92a437d56e114a22cd5851d
author on1ystar <tjdwls0607@naver.com> 1729759171 +0900
committer on1ystar <tjdwls0607@naver.com> 1729764440 +0900

commit --amend 테스트를 위한 커밋
➜  git-study git:(master) git cat-file -p 2961b358eca986a3e92a437d56e114a22cd5851d
040000 tree a8c6a1e96f754658ae9115d56dce510cd55e1a4a    commit-directory
100644 blob dd4c20b86bdac91798f87657a48e2f83af4fea82    test.txt

실제 체크섬을 확인해 보면, test.txt 파일의 blob 체크섬 값은 같지만 tree 의 값은 바뀐 것을 확인할 수 있다.

  • 이전 커밋의 tree 체크섬 : 9ae300a719831c469963aae88287347c75103047
  • 이전 커밋의 test.txt blob 체크섬 : dd4c20b86bdac91798f87657a48e2f83af4fea82

tree 체크섬 값이 바뀐 이유는, 새로운 파일이 스냅샷에 추가됐기 때문에 이를 포함하는 최상위 tree 객체를 생성한 것이다. 그 외의 값들은 덮어씌운다는 의미 그대로 이전 커밋에서 생성한 정보를 그대로 사용한다.

이 명령어를 사용하는 경우는 깜빡한 아주 간단한 작업들을 이전 커밋에 추가하고 싶을 때 유용하게 사용할 수 있다. 쓸데없이 새로운 커밋을 새로 생성하지 않아도 되기 때문이다.

Reset 명확히 알고 가기

세 개의 트리

여기서 “트리” 란 실제로는 “파일의 묶음” 이다. 자료구조의 트리가 아니다 세 트리 중 Index는 트리도 아니지만, 이해를 쉽게 하려고 일단 트리라고 한다.

Git은 일반적으로 세 가지 트리를 관리하는 시스템이다.

HEAD 현재 브랜치를 가리키는 포인터, 마지막 커밋 스냅샷, 다음 커밋의 부모
Index Staging Area, 다음에 커밋할 스냅샷
워킹 디렉토리(working directory) 실제 파일이 존재하는 공간

예로 들어 file.txt 파일 하나를 수정하고 커밋한다. 이것을 세 번 반복한다. 그러면 히스토리는 아래와 같이 된다.

https://git-scm.com/book/ko/v2/images/reset-start.png

reset 명령은 이 세 트리를 간단하고 예측 가능한 방법으로 조작한다. 트리를 조작하는 동작은 세 단계로 이루어진다.

1 단계: HEAD 이동

reset 명령이 하는 첫 번째 일은 HEAD 브랜치를 이동시킨다. HEAD는 계속 현재 브랜치를 가리키고 있고, 현재 브랜치가 가리키는 커밋을 바꾼다. HEAD가 master 브랜치를 가리키고 있다면(즉 master 브랜치를 Checkout 하고 작업하고 있다면) git reset 9e5e6a4 명령은 master 브랜치가 9e5e6a4 를 가리키게 한다.

https://git-scm.com/book/ko/v2/images/reset-soft.png

reset 명령에 커밋을 넘기고 실행하면 언제나 이런 작업을 수행한다. reset --soft 옵션을 사용하면 딱 여기까지 진행하고 동작을 멈춘다.

reset 명령은 가장 최근의 git commit 명령을 되돌린다. git commit 명령을 실행하면 Git은 새로운 커밋을 생성하고 HEAD가 가리키는 브랜치가 새로운 커밋을 가리키도록 업데이트한다. reset 명령 뒤에 HEAD~ (HEAD의 부모 커밋)를 주면 Index나 워킹 디렉토리는 그대로 놔두고 브랜치가 가리키는 커밋만 이전으로 되돌린다. git commit 명령을 실행하면 git commit --amend 명령의 결과와 같아진다.

2 단계: Index 업데이트 (--mixed)

여기서 git status 명령을 실행하면 Index와 reset 명령으로 이동시킨 HEAD의 다른 점이 녹색으로 출력된다.

reset 명령은 여기서 한 발짝 더 나아가 Index를 현재 HEAD가 가리키는 스냅샷으로 업데이트할 수 있다.

https://git-scm.com/book/ko/v2/images/reset-mixed.png

--mixed 옵션을 주고 실행하면 reset 명령은 여기까지 하고 멈춘다. reset 명령을 실행할 때 아무 옵션도 주지 않으면 기본적으로 --mixed 옵션으로 동작한다(예제와 같이 git reset HEAD~ 처럼 명령을 실행하는 경우).

위의 다이어그램을 보고 어떤 일이 일어날지 한 번 더 생각해보자. 가리키는 대상을 가장 최근의 커밋으로 되돌리는 것은 같다. 그러고 나서 Staging Area 를 비우기까지 한다. git commit 명령도 되돌리고 git add 명령까지 되돌리는 것이다.

3 단계: 워킹 디렉토리 업데이트 (--hard)

reset 명령은 세 번째로 워킹 디렉토리까지 업데이트한다. --hard 옵션을 사용하면 reset 명령은 이 단계까지 수행한다.

https://git-scm.com/book/ko/v2/images/reset-hard.png

이 과정은 어떻게 동작하는지 가늠해보자. reset 명령을 통해 git addgit commit 명령으로 생성한 마지막 커밋을 되돌린다. 그리고 워킹 디렉토리의 내용까지도 되돌린다.

--hard 옵션은 매우 매우 중요하다. reset 명령을 위험하게 만드는 유일한 옵션이다. Git에는 데이터를 실제로 삭제하는 방법이 별로 없다. reset 명령을 어떻게 사용하더라도 간단히 결과를 되돌릴 수 있다. 하지만 --hard 옵션은 되돌리는 것이 불가능하다. 이 옵션을 사용하면 워킹 디렉토리의 파일까지 강제로 덮어쓴다. 이 예제는 파일의 v3버전을 아직 Git이 커밋으로 보관하고 있기 때문에 reflog 를 이용해서 다시 복원할 수 있다. 만약 커밋한 적이 없다면 Git이 덮어쓴 데이터는 복원할 수 없다.


References

728x90
반응형
댓글