git에서는 "취소"라는 말이 상황에 따라 조금씩 다르다. 이 포스팅에서는 변경을 "취소"하고 싶은 상황 시나리오를 제시하고 그에 따른 적절한 git 사용 방법을 작성하고자한다.
"퍼블릭"한 변경을 취소
- 시나리오
git push를 실행해서 GitHub에 변경사항을 보낸 후, 하나의 커밋에 문제가 있는 것을 발견했다. 당신은 이 커밋을 취소하고 싶어졌다.
- 사용할 커맨드
git rever <SHA>
- 동작 설명
git revert는 전달된 SHA와 반대의 (원래로 되돌리는) 새로운 커밋을 만든다. 이전 커밋에서 "한 것"은 새로운 커밋에서 "한 것과 반대"가 된다. 즉 이전 커밋에서 삭제된 것은 새로운 커멧에 추가되고, 이전에 커밋에서 추가된 것은 새로운 커밋에서는 삭제된다.
히스토리를 고쳐 쓰지 않는다는 점으로 이것이 git의 안전하고 가장 기본적인 "취소" 시나리오이다. 이것으로 틀린 커밋을 취소하기 위해서 새로운 "반대" 커밋을 git push할 수 있다.
마지막 커밋 메시지를 수정
- 시나리오
커밋 메시지를 잘못입력한 상태인 git commit -m "Fxies bug #42"를 실행시켜버렸고, git push하기 전에 「Fixes bug #42」라고 오타를 낸 것을 발견했다.
- 취소 커맨드
git commit --amend 혹은 git commit --amend -m "Fixes bug #42"
- 동작 설명
git commit --ammned는 이전의 커밋 내용에 대해 스테이지된 모든 변경을 정리해 새로운 커밋으로, 최신 커밋으로 바꿔서 변경한다. 현시점에서 무엇도 스테이지되어 있지 않은 경우에는 이전 커밋 메시지를 바꿀 뿐이다.
"로컬" 변경을 취소
- 시나리오
키보드 위에 고양이가 뛰어들어 무언가 저장된 끝에, 충돌되어 버렸다. 그러나 아직 커밋은 하지 않았다. 파일 안의 모든 변경을 취소해, 이전의 커밋 상태로 돌리고 싶다.
- 취소 커맨드
git checkout --<문제의 파일명>
- 동작 설명
git checkout은 워킹 디렉토리를 git이 관리하고 있는 직전의 상태로 돌린다. 돌리고 싶은 브랜치나 특정 SHA를 지정하는 것도 가능하지만, 기본적으로 git은 HEAD, 즉 현재 체크 아웃되어 있는 브랜치의 최신 상태를 체크아웃한 것으로 다룬다.
- 주의점
이 방법으로 "취소"한 변경은 완전히 사라진다. 지금까지 했던 것은 커밋되지 않으므로, 후에 어떻게든 돌아가고 싶어져도 git은 도와줄 수 없다. 무엇이 삭제되는 것인지 잘 이해한 후에 실행하자 ! (git diff로 확인할 수 있을 것이다)
"로컬" 변경을 리셋
- 시나리오
로컬 상에서 무엇인가를 커밋햇다(그러나 아직 push하지 않은 상태). 그러나, 커밋했던 것들이 별로 좋지 않다고 생각해 마지막 3개의 커밋을 삭제하고, 아무것도 없었던 것으로 하고 싶다.
- 취소 커맨드
git reset<되돌리고 싶은 최신 SHA> 혹은 git reset --hard <되돌리고 싶은 최신 SHA>
- 동작 설명
git reset은 지정된 SHA까지 리포지터리 이력을 되감아, 그 사이의 커밋이 없었던 것으로 한다. 기본적으로는 git reset은 워킹 디렉토리는 그대로 유지된다. 커밋은 취소되지만, 컨텐츠는 디스크상에 남는다. 이 방법이 안전하지만, 커밋뿐만 아니라 변경도 일괄적으로 "취소"하고 싶은 경우도 있을 것이라고 생각된다. 그 때는 --hard 옵션을 사용한다.
"로컬" 변경을 취소한 후에 되돌릴 때
- 시나리오
무엇인가 커밋하여, git reset --hard로 그 변경을 "취소"한 후에, 역시 취소한 것을 되돌리고 싶다!고 생각하고 있을 경우
- 취소 커맨드
git reflog 후 에, git reset 혹은 git checkout
- 동작 설명
git reflog를 사용하면, 프로젝트의 히스토리를 되돌리기 위한 좋은 정보를 얻을 수 있다. reflog를 사용하면, 커밋한 것이라면 무엇이든 되돌릴 수 있다.
커밋의 리스트를 표시해주는 git log 커맨드는 자주 쓰고 있을 것이라고 생각된다. git reflog는 그와 비슷하지만, reflog는 HEAD가 변경되었을 때를 일괄 표시해준다.
다만, 아래의 것들을 주의해야한다.
1. 표시된 것은 HEAD의 변경뿐이다. HEAD는 브랜치를 변경해, git commit으로 커밋하거나, git reset으로 커밋을 취소했을 때만 변경되어, git checkout --<문제가 있던 파일명> 으로 변경되지 않는다.
2. git reflog으로 모든 이력을 볼 수 있는 것은 아니다. Git은 "참고할 수 없는" 오브젝트를 정기적으로 청소한다. 월 단위로 지난 커밋을 볼 수 있는 것을 기대하지 않는 편이 좋다.
3. 실행한 reflog는 당신과 관련된 것만 볼 수 있다. 다른 개발자가 push하지 않은 커밋을 git reflog로 원래대로 되돌릴 수 없다.
위와 같이되면, 어중간한 과거의 커밋을 "취소"하려고 reflog를 사용하려고 할 때, 어떻게 하는 것이 좋은가?는 어디까지 하고 싶은지에 따라 조금씩 다르다.
1) 어느 시점의 상태로 프로젝트 히스토리를 돌리고 싶은 경우는 git reset --hard <SHA>
2) 히스토리를 바꾸지 않고, 어느 시점에 존재한 워킹 디렉토리 내의 파일을 한 번더 작성하고 싶을 때는, git checkout <SHA> --<파일명>
3) 리포지터리 내의 커밋의 어떤 것을 그대로 리플레이하고 싶은 경우, git cherry-pick <SHA>
브랜치를 사용하고 있을 때, 다른 브랜치에 다시 커밋
- 시나리오
커밋한 후, master에서 체크아웃한 상태에서 커밋한 사실을 알아버렸다. 지금의 커밋을 feature 브랜치로 변경하고 싶다고 생각하고 있다.
- 취소 커맨드
git brach feature 후, git reset --hard origin/master하고 다시 git checkout feature
- 동작 설명
git checkout -b <브랜치명>하여 새로운 브랜치를 생성하는 것을 익숙할 것이다, 이것은 새로운 브랜치를 만들어 바로 체크아웃하는 방법으로 일반적으로 사용되지만, 아직 브랜치를 바꾸고 싶지 않을 경우 git brach feature로 새로운 커밋을 가리키는 feature이라는 새로운 브랜치를 만들면서도, master를 체크아웃한 상태 그대로가 된다.
다음은 git reset --hard로 master를 커밋 실행 전의 origin/master로 되돌린다. 걱정하지 않아도 된다. 그 커밋은 feature브랜치에 남아있으니 말이다.
마지막으로 git checkout으로 새로운 feature 브랜치로 바꾸면 아까 한 작업의 내용이 남아있을 것이다.
기존의 브랜치에 최신 내용을 반영
-시나리오
master을 바탕으로한 새로운 feature 브랜치로 개발을 시작했지만, mater은 origin/master의 최신 내용과는 꽤 동떨어져있다. master 브랜치는 origin/master와 동기화하였지만, feature 브랜치도 예전 버전이 아닌 최신버전에서 커밋을 시작하고 싶다고 생각하고 있다.
- 취소 커맨드
git checkout feature하여, git rebase master
- 동작 설명
이번에도 동일하지만, git reset(--hard는 디스크상의 변경을 유지하기 위해서는 붙이지 않는다) 후에, git checkout -b <새로운 브랜치명>하여, 그 상태에서 변경을 커밋할 수 있다. 그러나 이 방법으로 하면 커밋 이력이 삭제된다. 따라서 더욱 좋은 방법은 위에서 설명한 방법이다.
git rebase master은 몇 가지 처리가 실행된다.
1) 먼저 현재 브랜치 아웃하고 있는 브랜치와 master의 공통의 히스토리를 특정한다.
2) 현재 체크아웃하고 있는 브랜치를 그 과거의 히스토리로 리셋한다. 이 때, 그 동안의 커밋은 일시 영역으로 따로 대피시켜둔다.
3) 현재 체크아웃하고 있는 브랜치를 master의 마지막까지 나아가, master의 최후의 커밋보다 위에 일시 영역에 대피시켜둿던 커밋을 리플레이한다.
일괄 취소, 되돌리기
- 시나리오
어떤 방법으로 기능을 만들기 시작했지만, 도중에 다른 방법이 좋을 것 같다는 생각이 들기 시작했다. 이전부터 꽤 많이 커밋을 했뒀지만 그 커밋 중에 일부만 필요하다. 따라서 필요한 커밋 이 외에 커밋은 삭제하고 싶다고 생각하고 있다.
- 취소 커맨드
git rebase -i <이전의 SHA>
- 동작 설명
-i는 rebase를 "인터랙팅 모드"로 실행한다. 위에서 언급하였듯이 rebase가 작동되지만, 각 커밋을 리플레이하기 전에 하나 하나 일시정지하고 각 커밋을 반영할지 확인이 되면 리플레이된다.
아래와 같이 rebase -i는 기본 텍스트에디터를 열어서 적용된 커밋의 목록을 표시한다.
처음의 두 줄은 키가 된다. 첫 번째 행은 두 번째에 있는 SHA로 특정된 커밋을 선택할 것인지에 대한 커맨드이다. rebase -i 의 디폴트로는 각 커밋을 적용한다는 의미인 pick 커맨드가 된다.
커밋 내용은 그대로 유지하고 싶지만, 커밋 메시지를 변경하고 싶은 경우 reword 커맨드를 사용할 수 있다. 1행째의 pick을 reward(혹은 단축형 r)로 바꾸자. rebase -i가 실행된 후에 바꿔 쓸 필요가 있는 것에 대해 새로운 커밋 메시지 입력을 요구받는다.
2개의 커밋을 하나로 묶어 정리하고 싶을 때는 아래와 같이 squash 혹은 fixup 커맨트를 사용할 수 있다.
squash와 fixup은 "위"의 커밋으로 묶여진다. squash를 선턱하면, Git은 새롭게 정리된 커밋에 대해 새로운 커밋 메시지를 입력할 수 있게 된다. 한편 fixme를 선택하면, 리스트의 맨 처음의 커밋 메시지가 새로운 커밋 메시지로 바뀐다.
저장후에 에디터를 종료하면, git은 위에서 부터 순서대로 커밋을 적용해간다. 저장 전에 커밋의 순서대로 변경하면 그 순서로 변경해 적용된다.
이전의 커밋을 수정하기
- 시나리오
이전의 커밋에 파일을 포함하는 것을 잊어버렸다. 잊어버린 파일이 전의 커밋에는 포함됐다면 좋았을걸이라고 생각하고 있다. 다행히도 아직 push는 하지 않았지만, 방금 한 커밋이 아니므로, commit --amend를 사용할 수 없는 상태이다.
- 취소 커맨드
git commit --squash <이전의 커밋의 SHA> 한 후에 git rebase --autosquash -i <최신의 SHA>
- 동작 실행
git commit --squash는 squash! 전의 커밋과 같이 커밋 메시지를 붙인 새로운 커밋을 만든다(commit --squash를 사용하면 타이핑의 수고가 줄어든다). 정리된 커밋에 새로운 커밋 메세지를 작성하는 프롬프터가 나와도 괜찮다면, git commit --fixup를 사용하는 것이 가능하다.
rebase --autosquash -i는 대화형 리베이스 에디터를 기동하지만, 아래의 그림과 같이 커밋의 목록 내에서는 이미 squash!이나 fixup!이 커밋 대상과 페어가 된 상태가 된다.
--squash나 --fixup를 사용할 때에는 수정하고 싶은 커밋의 SHA를 기억할 필요가 없고, 1개 전의 커밋인지 5개전의 커밋인가만을 기억해두면 충분하다. 그리고 기억해 둔 것을 적용할 때는 git의 ^이나 ~가 꽤 편리하다. HEAD^는 HEAD의 1개 전의 커밋이라는 의미이고, HEAD~4는 HEAD의 4개 전의 커밋이라는 의미로 5개 전으로 돌아가게 된다.
버전 관리하고 있는 파일을 제외시키기
- 시나리오
application.log 파일을 틀리게 리포지토리에 넣어버렸기 때문에, 어플리케이션을 기동할 때마다, application.log에 스테이지되어 있지않은 변경이 있다는 표시가 뜨고 있다. *.log를 .gitignore파일에 넣었지만, 동일한 메시지가 출력되고 있다. 어떻게 해야 git이 이 파일을 관리하지 않도록 할 수 있을까?
- 취소 커맨드
git rm --cached application.log
- 동작 설명
.gitignore은 Git이 파일의 변경을 추적하지 않도록 하거나, 추적된 적이 없는 파일의 변경을 통지하지 않도록 할 수 있지만, 한번이라도 add되거나 coomit되게 되면, 파일의 변경을 계속해서 추적하게 된다. 이것은 git add -f로 .gitignore을 무시하도록 강제해도 같은 현상이 일어난다. add -f는 사용하지 않는 편이 좋다.
무시해야될 파일을 git이 트래킹하는 것을 방지하는 방법으로는 git rm --cached를 사용하여, 트래킹 대상으로 부터 제외하면서 디스크 상에는 파일을 남긴다. 이것으로 그 파일이 무시되어, git status에도 표시되지 않게 되어, 틀린 파일을 커밋하는 경우가 없어진다.
참고자료
'IT > 기초 지식' 카테고리의 다른 글
[Docker] Docker에서 사용하지 않는 오브젝트를 삭제하는 "prune" 커맨드와 옵션 (0) | 2022.01.28 |
---|---|
[Docker] Windows10 환경에서 WSL2+Docker사용하기 (0) | 2022.01.28 |
[Linux] sed 커맨드 상황별 사용법 (0) | 2021.11.03 |
[Linux] expect를 이용한 Linux 커맨드 입력 자동화 (0) | 2021.10.20 |
[Singularity] Singularity + headless VNC + Pipenv를 사용한 강화학습 환경 만들기(gym, pybullet) (0) | 2021.10.14 |