git 알아보기 #4 : git diff/restore/rm/revert/clean

git diff

최신 commit과 이전 commit에서 코드가 어떻게 달라진건지 차이점을 확인하기 위해선 git diff를 사용한다.

Zsh
git diff <commit1> <commit2>

Diff
~/g/myproject │ main>  git diff 0d52a6 046e34
diff --git a/sayings.py b/sayings.py
index 8323c67..23ccfef 100644
--- a/sayings.py
+++ b/sayings.py
@@ -8,5 +8,8 @@ def hello(name):
 def goodbye(name):
     print(f"goodbye, {name}")
 
+def sleep(name):
+    print(f"good night, {name}")
+
 if __name__ == "__main__":
-    main()
\ No newline at end of file
+    main()

git diff <commit1> <commit2> 와 같이 사용한다. commit 지정은 id로 사용되는 hashcode를 써주는데, 40자를 다 안쓰고 앞에 6-7자만 사용해도 왠만해서는 중첩되지 않으므로 작동한다. 그냥 최신 commit의 변경사항을 알고 싶다면, 다음과 같이 git show로 간단한 명령어로도 작동된다.

Zsh
~/g/myproject │ main>  git show

git diff는 나중에 branch간 비교에도 사용되며, 설정을 하면 외부 compare툴을 사용할 수도 있다. 파일이 많아지고 복잡해지는 경우, GUI를 쓰는 외부툴을 사용하는게 훨씬 수월할 것이다. 이 부분은 gitconfig 부분에서 다뤘다.

만약에 인자없이 git diff만 한다면, 작업 디렉토리와 staging area간의 비교를 해준다. 이게 좀 혼란스러울 수 있으니 잘 알아두자. 만약 작업 디렉토리의 변경사항과 repository의 최신 commit을 비교하려면 HEAD를 추가로 써준다.

Zsh
git diff HEAD

staging area내용과 repository의 최신 commit사이의 차이를 보고싶다면 –cached 옵션을 쓴다.

Zsh
git diff --cached

특정 파일만 보고싶다면, 파일을 적어주면 된다.

Zsh
git diff <file1> <file2> ...

git restore

작업을 하다가 복잡한 수정을 했는데, 뭔가 꼬여서 git에 있는 원본으로 되돌리고 싶을 때가 있다. 다음처럼 readme.txt 파일을 망쳐놨다고 생각해보자.

readme.txt
this is the first file.
asdfjijowqjojgalgjafile name is 01.txt
I'm studying git.
Why am I so idiotadiofjaoidofjaifo?

이럴 때 repository에 있는 파일로 되돌리고 싶으면 다음과 같이 restore를 사용한다.

Zsh
~/g/myproject │ main !1 ?1>  git restore readme.txt

그런데 만약, git add로 수정한 내용이 이미 staged 되어 있다면, 이 명령어가 먹히지 않는다. 그럴 땐, 앞에서 얘기했던 staged 취소 옵션 “–staged”로 이 restore를 먼저 사용해야 한다.

Zsh
~/g/myproject │ main +1 ?1>  git restore --staged readme.txt 

만약에 특정 commit으로 파일을 되돌리고 싶으면 –source 옵션을 사용한다.

Zsh
~/g/myproject │ main !1 ?1>  git log --graph --oneline
* a67ee7e (HEAD -> main) add more text to readme.txt
* 046e349 add sleep function
* 0d52a66 the first commit.

git log로 commit들을 확인한 후, readme.txt 파일을 텍스트 추가하기 전 commit인 두번째로 되돌려보자.

Zsh
~/g/myproject │ main ?1>  git restore readme.txt --source 046e34
~/g/myproject │ main !1 ?1>  git status
현재 브랜치 main
커밋하도록 정하지 않은 변경 사항:
  (무엇을 커밋할지 바꾸려면 "git add <파일>..."을 사용하십시오)
  (작업 디렉토리의 변경을 무시하려면 "git restore <file>..."을 사용하시오)
	수정함:        readme.txt
...

위에서 –source 옵션에 hashcode 6자리를 사용했다. 로컬 파일의 내용이 2번째 commit으로 변경되었으므로 현재 repository에 있는 마지막 commit 내용과 달라져서 git status에 변경된 파일로 표시되는걸 볼 수 있다. git diff를 사용해보면,

Diff

위와같이 마지막에 추가한 라인 “Why am I so idiot?” 문장이 ‘-‘ 표시로 삭제된 차이가 있다고 알려주고 있다. git resotre 로 일단, 최근 commit으로 다시 돌려놓자.

Zsh
git restore readme.txt

git rm

작업을 하다보면, 더이상 필요없어진 파일들이 생겨난다. 문제는 이 파일들을 임의로 삭제했을 때, repository에는 반영이 안된다는 점이다. 이미 repository에 반영된 파일을 삭제할 땐 git rm을 이용한다.

테스트를 위해, 우선 나중에 삭제할 파일을 추가해보자.

Zsh
~/g/myproject │ main ?2>  git add dont_needed_anymore.txt
~/g/myproject │ main +1 ?1>  git commit -m "add file maybe dont needed anymore later"
~/g/myproject │ main ?1>  git show --name-status
commit b5028821bbcfe7736a4cf6c15bc5dbff72e8f8c0 (HEAD -> main)
Author: batmask <batmask@zzzz.zzz>
Date:   Sun Apr 26 16:19:15 2026 +0900

    add file maybe dont needed anymore later

A       dont_needed_anymore.txt

  

위의 내용은 dont_needed_anymore.txt 파일을 추가해서 commit한 후에, “git show –name-status” 를 사용해서 파일들 변경사항만 보여주도록 표시한 결과다. ‘A’로 표시되는게 Add로 추가됐다는 의미이다.

이제 이 파일을 rm으로 지워보자.

Zsh
~/g/myproject │ main ?1>  git rm dont_needed_anymore.txt
rm 'dont_needed_anymore.txt'
~/g/myproject │ main +1 ?1>  ls
dump  readme.txt  sayings.py
~/g/myproject │ main +1 ?1>  git status
현재 브랜치 main
커밋할 변경 사항:
  (스테이지에서 제외하려면 "git restore --staged <file>..."을 사용하시오)
	삭제함:        dont_needed_anymore.txt

...

git rm으로 파일을 삭제하면, 실제로 로컬 디렉토리에서 파일이 삭제되는 걸 확인 할 수 있다. 그리고 staging area에 “삭제됨”이 추가된다. 이걸 commit해야 repository에서도 삭제가 된다.

Zsh
~/g/myproject │ main ?1>  git ls-files
readme.txt
sayings.py

git ls-files 는 현재 repository의 파일들을 보여주는 명령어인데, dont_needed_anymore.txt 파일이 삭제된걸 확인할 수 있다. 삭제도 staging area를 이용하고 새로운 commit으로 삭제 변경사항이 기록되는걸 알 수 있다.

Zsh
~/g/myproject │ main ?1>  git log --oneline --graph
* 650a1e6 (HEAD -> main) delete dont_needed_anymore.txt
* b502882 add file maybe dont needed anymore later
* a67ee7e add more text to readme.txt
* 046e349 add sleep function
* 0d52a66 the first commit.

만약에 git rm 없이 파일을 삭제하면 어떻게 될까? readme.txt 파일을 한 번 지워보자.

Zsh
~/g/myproject │ main ?1>  rm readme.txt
 ~/g/myproject  main !1 ?1>  git status
현재 브랜치 main
커밋하도록 정하지 않은 변경 사항:
  (무엇을 커밋할지 바꾸려면 "git add/rm <파일>..."을 사용하십시오)
  (작업 디렉토리의 변경을 무시하려면 "git restore <file>..."을 사용하시오)
	삭제함:        readme.txt

쉘의 칼라가 워드프레스 코드블럭에 제대로 반영이 안되서 그런데, 앞에서 git rm으로 지웠던 “삭제함”은 트래킹중인 파일의 표시 칼라가 사용되지만(녹색) 여기서는 색이 다르게 트래킹 되지 않은 파일의 표시색(빨강)으로 나온다. 즉, 삭제되서 repository와 로컬 파일이 다른 상태이나 staging되지 않았다는 얘기다. 커밋관련해서는 git add/rm 을 사용하라고 안내하고 있다. 지금이라도 git rm을 사용하면, 앞에서와 같이 staging area에 추가되며, commit으로 repository에도 삭제가 된 스냅샷이 기록된다.

Zsh
~/g/myproject │ main !1 ?1>  git rm readme.txt
rm 'readme.txt'

git revert / reset

앞에서 rm커맨드를 연습하며 readme.txt 파일을 지워버렸다. 사실, 이건 원하던 작업이 아니다. 이 커밋 자체를 취소하고 싶은데, 이럴 때 사용하는게 git revert이다. git log로 최근 commit을 살펴보고, 마지막 commit을 되돌려보자.

Zsh
~/g/myproject │ main ?1>  git log --oneline -3
863481a (HEAD -> main) delete readme.txt
650a1e6 delete dont_needed_anymore.txt
b502882 add file maybe dont needed anymore later
~/g/myproject │ main ?1>  git revert 863481a
[main 7deb397] Revert "delete readme.txt"
 1 file changed, 5 insertions(+)
 create mode 100644 readme.txt

git revert로 마지막 commit의 6자리 해쉬코드를 사용했고, 마지막 commit이 revert 됐다는 정보들이 표시되는걸 볼 수 있다. 여기서 commit의 해쉬코드를 쓰는게 복잡하게 보일 것이다. 바로 이전의 부모 commit 이나 2~3단계정도를 앞으로 가는 경우 간단하게 다음과 같이 쓸 수도 있다.

Zsh
~/g/myproject │ main ?1>  git revert HEAD^
~/g/myproject │ main ?1>  git revert HEAD~1
~/g/myproject │ main ?1>  git revert HEAD~2

HEAD^ 는 HEAD의 부모 commit을 의미하며, HEAD~1 도 바로 이전 부모 commit, HEAD~2 는 부모의 부모인 2단계 이전의 commit을 나타내는 식이다.

HEAD^부모 commit
HEAD~22단계 이전 commit
HEAD~nn단계 이전 commti

git log를 보면, 마지막 commit이 삭제되는게 아니고, 이전으로 되돌린 새로운 commit이 생성된걸 볼 수 있다.

Zsh
~/g/myproject │ main ?1>  git log --oneline
7deb397 (HEAD -> main) Revert "delete readme.txt"
863481a delete readme.txt
650a1e6 delete dont_needed_anymore.txt
b502882 add file maybe dont needed anymore later

비슷한 동작을 하지만, 굉장히 위험한 명령어가 있다. 바로 git reset 이다. git revert는 revert한 내용 자체를 새로운 commit으로 기록하지만, git reset은 특정 commit으로 돌아간 후, 그 이후 commit을 다 지워버린다. 어떠한 변경사항도 기록이 남는게 무조건 좋기 때문에 가능하면 사용하지 말고, 사용시에는 정확한 의도를 가지고 사용하기 바란다.

git reset에는 –soft, –mixed, –hard 의 3가지 옵션이 있다. 앞에서 dont_needed_anymore.txt파일을 지운 commit인 650a1e6 으로 돌아가보자.

Zsh
~/g/myproject │ main ?1>  git reset --soft 650a1e6

~/g/myproject │ main ?1>  git log --oneline --graph
* 650a1e6 (HEAD -> main) delete dont_needed_anymore.txt
* b502882 add file maybe dont needed anymore later
* a67ee7e add more text to readme.txt
* 046e349 add sleep function
* 0d52a66 the first commit.

git log로 확인시, 실제로 650a1e6 commit이 최신 commit이 되고 그 뒤의 commit들이 더이상 보이지 않는다는 걸 알 수 있다. 그렇다면, –soft 옵션은 뭘까? repository만 되돌릴 때 사용하고, –mixed는 stage 까지 되돌리고, 마지막으로 –hard를 쓰면 로컬 파일들까지 돌아간다.

실제로 파일을 생성하고 stage에 add한 후 현재 commit에 사용해보면, stage영역이나 로컬은 변경되지 않는걸 알 수 있다.

Zsh
~/g/myproject │ main ?1>  vim reset_option.txt
~/g/myproject │ main ?2>  git add reset_option.txt
~/g/myproject │ main +1 ?1>  git reset --soft 650a1e6
~/g/myproject │ main +1 ?1>  git status
현재 브랜치 main
커밋할 변경 사항:
  (스테이지에서 제외하려면 "git restore --staged <file>..."을 사용하시오)
	 파일:       reset_option.txt

차례대로 –mixed, –hard를 사용해보자.

Zsh
~/g/myproject │ main +1 ?1>  git reset --mixed
~/g/myproject │ main ?2>  git status
현재 브랜치 main
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
	dump
	reset_option.txt

git reset 뒤에 commit 해쉬코드가 없으면 최신 commit을 지칭하므로 생략 가능하다. –mixed를 사용했더니 stage에 있던 파일이 사라져 reset_option.txt가 추적하지 않는 파일로 표시된다. 마지막으로 –hard를 사용해보면,

Zsh
~/g/myproject │ main ?2>  git reset --hard
HEAD의 현재 위치는 650a1e6입니다 delete dont_needed_anymore.txt
~/g/myproject │ main ?2>  git status
현재 브랜치 main
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
	dump
	reset_option.txt

git reset –mixed 와 차이가 없다는걸 알 수 있다. reset_option.txt가 지워져야 하지 않나 생각이 될 수도 있는데, 이미 –mixed옵션으로 tracking 되지 않는 파일이기 때문에 건드리지 않는다.

이번엔, git add reset_option.txt 를 사용해 stage 에 올려놓고 git reset –hard를 시도해보자.

Zsh
~/g/myproject │ main ?2>  git add reset_option.txt
~/g/myproject │ main +1 ?1>  git reset --hard
HEAD의 현재 위치는 650a1e6입니다 delete dont_needed_anymore.txt
~/g/myproject │ main ?1>  git status
현재 브랜치 main
추적하지 않는 파일:
  (커밋할 사항에 포함하려면 "git add <파일>..."을 사용하십시오)
	dump

커밋할 사항을 추가하지 않았지만 추적하지 않는 파일이 있습니다 (추적하려면 "git
add"를 사용하십시오)
~/g/myproject │ main ?1>  ls
dump  readme.txt  sayings.py

예상했을지 모르지만, 놀랍게도 로컬과 stage 전부에서 reset_option.txt 파일이 사라졌다!

reset에 대해 다시 정리해보면, –soft : repository만 되돌림, –mixed : repository와 stage를 되돌림, –hard : repository, stage, local file을 전부 되돌림. 과 같이 작동한다. 하지만, 마지막 –hard에서 봤듯이, 어디까지나 트래킹하고 있는 파일들에 대해서만 해당한다. 만약에 임의로 로컬에 파일들을 추가해놓고 repository의 상태로 되돌리고 싶다고 git reset –hard 를 한다고 해도, 추가된 파일들은 남아 있다는 얘기다. 내가 초기에 매우 혼란스러웠던 내용이기도 하다.

git clean

그렇다면, repository에 없는 파일들이 추가된 상태에서 repository상태와 같게 만들고 싶으면 어떻게 해야할까? git reset –hard와 더불어 untracked 파일들을 삭제해야 한다. 이때는 git clean 명령어를 쓴다. 이 명령어도 의도치 않게 사용자 파일들을 삭제할 수 있으니 꼭 주의해서 사용해야 한다. 임시로 이런 파일들을 stash에 저장하는 명령으로 git stash가 존재하는데, 여기서는 다루지 않겠다. 또 한가지, .gitignore에 명시된 파일들은 git clean도 건드리지 않는다. 이런 파일도 지우려면 -x 옵션을 사용한다.

제일 먼저 해볼 수 있는건 -n(또는 –dry-run) 이다. 이는 실제 삭제 없이 삭제할 파일들을 보여준다.

Zsh
~/g/myproject │ main ?1>  git clean -n
dump 제거할 예정

삭제를 하려면 forced를 의미하는 -f 옵션을 써야하고, 디렉토리도 지우려면 -d 옵션을 써야한다. 앞서 말한대로 .gitignore 에 명시된 파일들도 삭제하려면 -x 옵션을 써야한다. 예제의 현재 작업 폴더를 보면,

Zsh
~/g/myproject │ main ?1>  ls
dump  readme.txt  sayings.py  temp

dump 파일과 temp 디렉토리가 untracked 파일들이다. 한번 clean 명령어를 사용해보자.

Zsh
~/g/myproject │ main ?1>  ls
dump  readme.txt  sayings.py  temp
~/g/myproject │ main ?1>  git clean -fdx
dump 제거
temp/ 제거
 ~/g/myproject  main  ls     
readme.txt  sayings.py

원하는대로 untracked 파일들이 삭제된 걸 확인 할 수 있다.

Previous post git 알아보기 #3 : git init/status/add/commit/log

관련 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다