git commit 을 원복하는 방법
Git 을 사용하다 보면 저장소에 작업한 commit 을 원복해야 하는 경우가 종종 발생한다. 로컬에서 혼자서 작업한다면 reset 을 사용해서 이전 commit 으로 쉽게 돌릴 수 있지만 이미 원격 저장소에 push 한 상태라면 revert 를 사용하여 이전 commit 을 취소하는 새로운 commit 을 만들어야 한다.
명료하게 아래 2가지 경우만 생각하면 된다.
아직 원격 저장소에 push 하지 않은 경우 : reset 사용
원격 저장소에 push 한 경우 : revert 사용
예외적으로 원격 저장소에 push 한 경우라도 reset 을 사용해서 commit 을 돌릴 수 있다. 하지만 이 때는 원격의 commit 도 같이 삭제하는 작업이 필요하므로 git push 를 할 때 -f 으로 강제 push 를 해야하는 문제가 있어 여러명이 함께 작업하는 경우라면 다른 사람들에게 문제가 발생할 수 있다. (웬만하면 하지 말아야 한다)
git reset : 로컬 환경을 특정 commit 위치로 되돌리기
아래와 같은 commit log 가 있다고 보자.
$ git log --oneline -n 5
4e61ca5 (HEAD -> main, origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 10:15 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 10:16 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 10:17 c.txt
Initial 커밋에는 README.md 파일이 추가되어 있고, 이후 각 커밋은 a.txt, b.txt, c.txt 가 추가되어 있는 상태이다. 여기서 a.txt 만 남기고 b.txt, c.txt 를 지운 상태의 돌아가고 싶다고 하면 a.txt 를 추가한 be0d36b add a.txt 커밋 상태로 돌아가면 된다.
$ git reset --hard be0d36b
HEAD is now at be0d36b add a.txt
로그와 파일을 조회해 보면 정상적으로 commit 이 이전 상태로 돌아온 것을 알 수 있다.
$ git log --oneline -n 5
be0d36b (HEAD -> main) add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 10:15 a.txt
reset 명령을 수행하면 커밋이 이전 상태로 돌아간 것이기 때문에 다시 원상태로 돌릴려면 원격 저장소에서 다시 pull 로 가져오면 된다.
$ git pull
Updating be0d36b..4e61ca5
Fast-forward
b.txt | 0
c.txt | 0
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 b.txt
create mode 100644 c.txt
$ git log --oneline -n 5
4e61ca5 (HEAD -> main, origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
파일도 원상태로 생성된 것을 알 수 있다.
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 10:15 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 10:16 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 10:17 c.txt
git revert : 이전 commit 제거하는 신규 commit 을 추가
현재 커밋 로그는 다음과 같습니다.
$ git log --oneline -n 5
4e61ca5 (HEAD -> main, origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
여기서 3bd328a add b.txt 을 삭제하고 싶을 때 revert 를 할 수 있다.
$ git revert 3bd328a --no-edit
[main 5a9e9f1] Revert "add b.txt"
Date: Thu Mar 14 10:51:58 2024 +0900
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 b.txt
git log 를 보면 다음과 같다.
$ git log --oneline -n 5
5a9e9f1 (HEAD -> main) Revert "add b.txt"
4e61ca5 (origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
5a9e9f1 (HEAD -> main) Revert "add b.txt" 커밋 로그를 보면 revert 하면서 새로운 commit 이 생긴 것을 알 수 있다. reset 과는 다르게 commit 의 순서와 내용은 그대로 살아있으면서 revert 가 추가된 것이기 협업할 때 아무런 문제가 없다.
리스트를 보면 b.txt 가 삭제되어 다음과 같다.
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 10:15 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 10:38 c.txt
revert 와 reset 은 둘 다 파라미터로 commit hash 값을 넣는 것은 동일하나 동작되는 의미는 다르다. reset 은 해당 commit 으로 돌아가기 때문에 그 이후의 commit 은 없어지는 반면에 revert 는 해당 commit 만 제거하는 의미가 있다.
revert 를 다시 revert 할 수 있다
revert 하여 b.txt 를 삭제한 commit 은 5a9e9f1 (HEAD -> main) Revert "add b.txt" 이다. 이 commit 을 revert 하면 다시 b.txt 파일이 살아날 수 있다. revert 할 때 commit hash 값과 이를 가리키는 HEAD 도 같은 의미이기 때문에 HEAD 를 이용하여 revert 해보자.
$ git revert HEAD --no-edit
[main d2b2258] Revert "Revert "add b.txt""
Date: Thu Mar 14 11:00:28 2024 +0900
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 b.txt
$ git log --oneline -n 10
d2b2258 (HEAD -> main) Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 (origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 10:15 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 10:38 c.txt
여러 commit 을 revert 하기
commit hash 값을 나열하면 여러 commit 을 revert 할 수 있다.
3bd328a add b.txt 커밋과 4e61ca5 (origin/main, origin/HEAD) add c.txt 커밋을 동시에 revert 해보자.
$ git revert --no-edit be0d36b 4e61ca5
[main 3b6ada1] Revert "add a.txt"
Date: Thu Mar 14 11:11:07 2024 +0900
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 a.txt
[main bd91431] Revert "add c.txt"
Date: Thu Mar 14 11:11:07 2024 +0900
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 c.txt
$ git log --oneline -n 20
bd91431 (HEAD -> main) Revert "add c.txt"
3b6ada1 Revert "add a.txt"
d2b2258 Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 (origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
revert 가 잘 되었지만 각각의 revert 에 대한 커밋이 2개 추가되었다. 3b6ada1 Revert "add a.txt" , bd91431 (HEAD -> main) Revert "add c.txt"
여러 revert 를 하나의 commit 으로 만들기
앞에서 작업한 2개의 revert commit 을 원상태로 되돌려 보자. 원격으로 push 하지 않았으므로 reset 을 사용해도 문제가 없다.
여기서는 d2b2258 Revert "Revert "add b.txt"" 커밋으로 돌아가면 된다.
$ git reset --hard d2b2258
HEAD is now at d2b2258 Revert "Revert "add b.txt""
$ git log --oneline -n 20
d2b2258 (HEAD -> main) Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 (origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:19 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:19 c.txt
revert -n 옵션을 사용하면 revert 할 때 index 는 사용하지만 commit 을 하지 않은 상태가 된다. 그러므로 git revert --continue 로 commit 을 진행하면 된다.
$ git revert -n be0d36b 4e61ca5
현재 상태를 보면 index 에 저장된 상태임을 알 수 있다.
$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
(use "git push" to publish your local commits)
You are currently reverting commit 4e61ca5.
(all conflicts fixed: run "git revert --continue")
(use "git revert --skip" to skip this patch)
(use "git revert --abort" to cancel the revert operation)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: a.txt
deleted: c.txt
이제 commit 을 하면서 커밋 메세지를 추가할 수 있다.
$ git revert --continue
메세지는 다음과 같이 입력했다.
Revert "add c.txt"
Revert "add a.txt"
git log 를 보면 커밋은 a4b6156 (HEAD -> main) Revert "add c.txt" Revert "add a.txt" 하나만 생성되었음을 알 수 있다.
$ git log --oneline -n 20
a4b6156 (HEAD -> main) Revert "add c.txt" Revert "add a.txt"
d2b2258 Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 (origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
다음 설명을 위해서 다시 reset 을 하자. commit 하나만 뒤로가면 되므로 HEAD^1 을 사용해도 된다.
$ git reset --hard HEAD^1
git revert: merge commit 에 대한 revert 하기
현재 커밋 로그는 다음과 같다.
$ git log --oneline -n 20
d2b2258 (HEAD -> main) Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 (origin/main, origin/HEAD) add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
git push 를 해서 원격 저장소에 저장한 다음 merge commit 을 만드는 작업을 한다.
$ git push
$ git switch -c merge_branch
$ touch d.txt
$ git add -A
$ git commit -m "add d.txt"
$ git push --set-upstream origin merge_branch
github 에서 pr 을 올리고 main branch 로 merge 한다.
이후에 main branch 에서 pull 한 다음에 커밋 로그를 보면 다음과 같다.
$ git switch main
$ git pull
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:44 d.txt
$ git log
commit 409bf49c1b05a39609207da03f28f782c3b8a0b9 (HEAD -> main, origin/main, origin/HEAD)
Merge: d2b2258 9fdd01f
Author: Seungkyu Ahn <seungkyua@gmail.com>
Date: Thu Mar 14 11:42:42 2024 +0900
Merge pull request #1 from seungkyua/merge_branch
add d.txt
commit 9fdd01fb9b0eff870093f15e246c998ae1fac452 (origin/merge_branch, merge_branch)
Author: Seungkyu Ahn <seungkyua@gmail.com>
Date: Thu Mar 14 11:40:46 2024 +0900
add d.txt
commit d2b22584cb8108cd7bc1eaaaa5775e1f19f330fa
Author: Seungkyu Ahn <seungkyua@gmail.com>
Date: Thu Mar 14 11:00:28 2024 +0900
Revert "Revert "add b.txt""
This reverts commit 5a9e9f14b9b7403ad5aef1df83d14f1a1d4938dd.
첫번째 커밋 로그를 보면 Merge: d2b2258 9fdd01f 와 같이 Merge 임을 알 수 있다. merge 의 경우 revert 는 -m 옵션으로 첫번재 hash 값을 적용할지 두번째 hash 값을 적용할지를 결정해 주어야 한다.
merge 바로 이전 커밋인 d2b2258 으로 revert 할 것이기 때문에 첫번재를 선택해 준다.
$ git revert 409bf49c -m 1
[main d53c40d] Revert "Merge pull request #1 from seungkyua/merge_branch"
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 d.txt
로그를 보면 revert 되었음을 알 수 있다.
$ git log --oneline -n 20
d53c40d (HEAD -> main) Revert "Merge pull request #1 from seungkyua/merge_branch"
409bf49 (origin/main, origin/HEAD) Merge pull request #1 from seungkyua/merge_branch
9fdd01f (origin/merge_branch, merge_branch) add d.txt
d2b2258 Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
patch 로 commit 삭제하기
commit 에 대한 패치 파일을 만들고 -R 옵션을 사용하여 패치 파일을 apply 하면 해당 패치 파일에 대한 commit 을 삭제할 수 있다.
현재 커밋 로그는 다음과 같다.
$ git log --oneline -n 20
d53c40d (HEAD -> main, origin/main, origin/HEAD) Revert "Merge pull request #1 from seungkyua/merge_branch"
409bf49 Merge pull request #1 from seungkyua/merge_branch
9fdd01f (origin/merge_branch, merge_branch) add d.txt
d2b2258 Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
$ ls -l
total 8
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
여기서 3bd328a add b.txt 에 대한 패치 파일을 만들어 보자.
$ git format-patch -1 3bd328a
아래와 같이 하나의 0001-add-b.txt.patch 패치 파일이 생성되었다.
$ ls -l
total 16
-rw-r--r--@ 1 ask staff 368 3 14 13:46 0001-add-b.txt.patch
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:00 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
이제 -R 옵션을 적용하여 patch 파일을 적용하자. -R 옵션은 reverse 로 패치 파일을 삭제하는 역할을 한다.
$ git apply -R 0001-add-b.txt.patch
상태를 보면 다음과 같다.
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: b.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
0001-add-b.txt.patch
no changes added to commit (use "git add" and/or "git commit -a")
파일 리스트를 보면 b.txt 가 삭제되어 있다.
$ ls -l
total 16
-rw-r--r--@ 1 ask staff 368 3 14 13:46 0001-add-b.txt.patch
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
이제 삭제된 파일을 stage 에 add 한 후 commit 한다.
$ git add b.txt
$ git commit -m "-R patch to b.txt"
커밋 로그를 보면 b.txt 가 삭제된 것을 알 수 있다.
$ git log --oneline -n 20
44e55ce (HEAD -> main) -R patch to b.txt
d53c40d (origin/main, origin/HEAD) Revert "Merge pull request #1 from seungkyua/merge_branch"
409bf49 Merge pull request #1 from seungkyua/merge_branch
9fdd01f (origin/merge_branch, merge_branch) add d.txt
d2b2258 Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
패치 파일이 있으니 apply 로 해당 commit 을 다시 살려보자. 실제로는 commit 을 살리는 것이 아니라 해당 commit 의 변경된 파일을 되살리는 것이다.
$ git apply 0001-add-b.txt.patch
$ git add b.txt
$ git commit -m "restore b.txt using patch"
아래 디렉토리에 b.txt 가 살아난 것을 알 수 있다.
$ ls -l
total 16
-rw-r--r--@ 1 ask staff 368 3 14 13:46 0001-add-b.txt.patch
-rw-r--r--@ 1 ask staff 17 3 14 10:12 README.md
-rw-r--r--@ 1 ask staff 0 3 14 11:34 a.txt
-rw-r--r--@ 1 ask staff 0 3 14 14:03 b.txt
-rw-r--r--@ 1 ask staff 0 3 14 11:34 c.txt
필요없는 패치 파일은 삭제한다.
$ rm 0001-add-b.txt.patch
여러 커밋을 하나의 패치 파일로 만들기
현재 커밋 로그는 아래와 같다.
$ git log --oneline -n 20
9aced43 (HEAD -> main, origin/main, origin/HEAD) restore b.txt using patch
44e55ce -R patch to b.txt
d53c40d Revert "Merge pull request #1 from seungkyua/merge_branch"
409bf49 Merge pull request #1 from seungkyua/merge_branch
9fdd01f (origin/merge_branch, merge_branch) add d.txt
d2b2258 Revert "Revert "add b.txt""
5a9e9f1 Revert "add b.txt"
4e61ca5 add c.txt
3bd328a add b.txt
be0d36b add a.txt
205a70c Initial commit
여기서 be0d36b add a.txt , 3bd328a add b.txt, 4e61ca5 add c.txt 을 하나의 패치 파일로 만들고 싶으면 다음과 같이 하면 된다.
시작 hash값 ^.. 종료 hash값
만약 .. 만 사용하면 시간 hash값은 포함되지 않는다(여기서는 be0d36b add a.txt 커밋이 포함되지 않는다). 그러므로 시작 hash값을 포함하고 싶으면 ^.. 을 사용해야 한다.
$ git format-patch be0d36b^..4e61ca5 --stdout > commits.patch
한가지 더 설명하자면 format-patch 는 커밋 히스토리까지 파일에 포함 시킨다. diff 를 사용하면 커밋 히스토리를 제외할 수 있다.
$ git diff be0d36b^..4e61ca5 > diff.patch