Git을 사용하여 변경 사항 되돌리기

Git은 변경 사항을 되돌리는 옵션을 제공합니다. 변경 사항을 되돌리는 방법은 변경 사항이 스테이징되지 않았는지, 스테이징되었는지, 커밋되었는지 또는 푸시되었는지에 따라 다릅니다.

변경 사항을 되돌릴 수 있는 경우

표준 Git 워크플로우에서:

  1. 파일을 생성하거나 편집합니다. 이 파일은 스테이징되지 않은 상태로 시작됩니다. 새로 생성된 경우 Git에서 아직 추적하지 않습니다.
  2. 파일을 로컬 리포지터리에 추가하여( git add) 파일을 스테이징 상태로 전환합니다.
  3. 파일을 로컬 리포지터리에 커밋합니다( git commit).
  4. 다른 개발자들과 파일을 공유할 수 있으며, 이를 원격 리포지터리로 커밋하여(git push) 공유할 수 있습니다.

이 워크플로 중 어느 시점에서든 변경 사항을 되돌릴 수 있습니다:

로컬 변경 사항 되돌리기

로컬에서 변경 사항을 원격 리포지터리로 푸시하기 전까지는 Git에서 수행한 변경 사항이 로컬 개발 환경에만 있습니다.

스테이징되지 않은 로컬 변경 사항 되돌리기

변경 사항을 적용했지만 아직 스테이징하지 않은 경우 작업을 되돌릴 수 있습니다.

  1. 해당 파일이 스테이징되지 않았는지(git add <file>을 사용하지 않았는지) git status명령으로 확인합니다:

    $ git status
    현재 브랜치 main
    원격 origin/main과 동기화됨
    스테이징되지 않은 변경 사항:
      (커밋될 것들을 업데이트하려면 "git add <file>..."() 사용하십시오)
      (작업 디렉터리의 변경 사항을 버리려면 "git checkout -- <file>..."() 사용하십시오)
           
        수정됨:   <file>
    커밋하지 않은 변경 사항 없음 (커밋: "git add", "git commit -a"를 사용하세요)
    
  2. 옵션을 선택하고 변경 사항을 되돌립니다:

    • 로컬 변경 사항 덮어쓰기:

      git checkout -- <file>
      
    • 나중에 재사용할 수 있도록 로컬 변경 사항 저장:

      git stash
      
    • 모든 파일의 로컬 변경 사항 영구적으로 삭제:

      git reset --hard
      

스테이징된 로컬 변경 사항 되돌리기

파일을 스테이징한 경우, 이를 되돌릴 수 있습니다.

  1. 해당 파일이 스테이징되었는지(git add <file>을 사용했는지) git status명령으로 확인합니다:

    $ git status
    현재 브랜치 main
    원격 origin/main과 동기화됨
    커밋될 변경 사항:
      (스테이징 해제할 파일을 다시 생략하려면 "git restore --staged <file>..."를 사용하십시오)
         
      새 파일:   <file>
    
  2. 옵션을 선택하고 변경 사항을 되돌립니다:

    • 파일의 스테이징을 취소하되 변경 사항은 유지:

      git restore --staged <file>
      
    • 모든 것을 스테이징 취소하되 변경 사항은 유지:

      git reset
      
    • 파일을 현재 커밋(HEAD)의 상태로 스테이징 취소:

      git reset HEAD <file>
      
    • 모든 로컬 변경 사항을 삭제하지만 나중에 저장:

      git stash
      
    • 모든 것을 영구적으로 삭제:

      git reset --hard
      

로컬 변경 사항 빠르게 저장하기

다른 브랜치로 전환하려면 git stash를 사용할 수 있습니다.

  1. 작업을 저장할 브랜치로 이동한 후 git stash를 입력합니다.
  2. 다른 브랜치로 전환합니다(git checkout <branchname>).
  3. 커밋을 생성하고, 푸시를 하고, 테스트합니다.
  4. 변경을 재개할 원래 브랜치로 돌아갑니다.
  5. 모든 이전에 저장한 커밋을 나열하려면 git stash list를 사용합니다.

    stash@{0}: 작업 중(submit): 6ebd0e2... git-stash 문서 업데이트
    stash@{1}: master 브랜치: 9cc0589... git-stash 추가
    
  6. git stash 버전을 실행합니다:

    • 이전에 저장한 변경 사항을 다시 적용하고 저장 디렉터리에서 제거하려면 git stash pop을 사용합니다.
    • 이전에 저장한 변경 사항을 다시 적용하지만 저장 디렉터리에 유지하려면 git stash apply을 사용합니다.

커밋된 로컬 변경 사항 되돌리기

로컬 리포지터리에 커밋(git commit)하면 Git은 변경 사항을 기록합니다. 아직 원격 리포지터리로 푸시하지 않았기 때문에 여러분의 변경 사항은 공개적이지 않습니다(또는 다른 개발자들과 공유되지 않습니다). 이 시점에서 변경 사항을 되돌릴 수 있습니다.

히스토리를 유지하면서 스테이징된 로컬 변경 사항 되돌리기

커밋 히스토리를 유지하면서 커밋을 되돌릴 수 있습니다.

이 예시에서는 A,B,C,D,E 다섯 개의 커밋을 사용하여 설명하며, 이들은 A-B-C-D-E 순서대로 커밋되었습니다. 되돌리고자 하는 커밋은 B입니다.

  1. 되돌리고자 하는 커밋의 커밋 SHA를 찾습니다. 커밋 로그를 살펴보려면 git log를 입력합니다.
  2. 옵션을 선택하고 변경 사항을 되돌립니다:

    • 커밋 B에서 도입된 추가 및 삭제 변경 사항을 교체하려면:

      git revert <commit-B-SHA>
      
    • 커밋 B에서 한 파일이나 디렉터리의 변경 사항을 되돌리지만 스테이징된 상태로 유지하고자 하는 경우:

      git checkout <commit-B-SHA> <file>
      
    • 커밋 B에서 한 파일이나 디렉터리의 변경 사항을 되돌리지만 스테이징되지 않은 상태로 유지하고자 하는 경우:

      git reset <commit-B-SHA> <file>
      

여러 개의 커밋 되돌리기

여러 커밋에서 복구할 수 있습니다. 예를 들어, 기능 브랜치에서 A-B-C-D를 커밋했지만 CD를 잘못했다는 것을 깨달았다면.

여러 개의 올바르지 않은 커밋에서 복구하려면:

  1. 마지막 올바른 커밋으로 전환합니다. 이 예에서는 B입니다.

    git checkout <commit-B-SHA>
    
  2. 새로운 브랜치를 생성합니다.

    git checkout -b 새로운-기능-경로
    
  3. 변경을 추가하고, 푸시하고, 변경을 커밋합니다.

커밋은 이제 A-B-C-D-E입니다.

또는 GitLab을 사용하면, 그 커밋을 새로운 Merge Request으로 cherry-pick할 수 있습니다.

note
다른 해결 방법은 B로 되돌린 후 E를 커밋하는 것입니다. 그러나 이 방법은 로컬로 다른 개발자들이 가지고 있는 것과 충돌하는 A-B-E를 만듭니다.

히스토리를 변경하면서 스테이징된 로컬 변경 사항 되돌리기

다음 작업은 Git 히스토리를 다시 작성합니다.

특정 커밋 삭제

특정 커밋을 삭제할 수 있습니다. 예를 들어, A-B-C-D라는 커밋이 있고 B를 삭제하려면.

  1. 현재 커밋 D에서 B까지 재베이스합니다:

    git rebase -i A
    

    커밋 디렉터리이 편집기에 표시됩니다.

  2. B 커밋 앞에 있는 pickdrop으로 바꿉니다.
  3. 다른 모든 커밋은 기본값인 pick으로 유지합니다.
  4. 편집기를 저장하고 나갑니다.

특정 커밋 수정

특정 커밋을 수정할 수 있습니다. 예를 들어, A-B-C-D라는 커밋이 있고 B에서 도입된 내용을 수정하려면.

  1. 현재 커밋 D에서 B까지 재베이스합니다:

    git rebase -i A
    

    커밋 디렉터리이 편집기에 표시됩니다.

  2. B 커밋 앞에 있는 pickedit로 바꿉니다.
  3. 다른 모든 커밋은 기본값인 pick으로 유지합니다.
  4. 편집기를 저장하고 나갑니다.
  5. 편집기로 파일을 열고 편집하고, 변경을 커밋합니다:

    git commit -a
    

되돌리기

이전 로컬 커밋을 다시 불러올 수 있습니다. 그러나 이전 커밋이 모두 이용 가능한 것은 아닙니다. 왜냐하면 Git이 정기적으로 브랜치나 태그에서 접근할 수 없는 커밋을 정리하기 때문입니다.

리포지터리 이력을 보고 이전 커밋을 추적하려면 git reflog show를 실행하세요. 예를 들면:

$ git reflog show

# 예시 출력:
b673187 HEAD@{4}: merge 6e43d5987921bde189640cc1e37661f7f75c9c0b: Merge made by the 'recursive' strategy.
eb37e74 HEAD@{5}: rebase -i (finish): returning to refs/heads/master
eb37e74 HEAD@{6}: rebase -i (pick): Commit C
97436c6 HEAD@{7}: rebase -i (start): checkout 97436c6eec6396c63856c19b6a96372705b08b1b
...
88f1867 HEAD@{12}: commit: Commit D
97436c6 HEAD@{13}: checkout: moving from 97436c6eec6396c63856c19b6a96372705b08b1b to test
97436c6 HEAD@{14}: checkout: moving from master to 97436c6
05cc326 HEAD@{15}: commit: Commit C
6e43d59 HEAD@{16}: commit: Commit B

위 출력은 리포지터리 이력을 보여주며 다음을 포함합니다:

  • 커밋 SHA.
  • 해당 커밋이 얼마나 많은 HEAD 변경 작업 전에 이루어졌는지 (HEAD@{12}는 12개의 HEAD 변경 작업 전에 이루어진 것입니다).
  • 취해진 동작, 예를 들면: 커밋, 리베이스, 머지.
  • HEAD를 변경한 동작에 대한 설명.

이력 변경 없이 원격 변경 취소

원격 리포지터리에서 변경을 취소하려면 변경을 취소할 새로운 커밋을 만들 수 있습니다. 이 프로세스를 따르면 이력이 보존되고 명확한 타임라인과 개발 구조가 제공됩니다. 그러나 이 작업은 다른 개발자들이 자신의 작업의 기반으로 사용하는 브랜치에 Merge된 경우에만 필요합니다.

특정 커밋 B에서 도입된 변경을 되돌리려면:

git revert B

이력을 변경하면서 원격 변경 취소하기

원격 변경을 취소하고 이력을 변경할 수 있습니다.

갱신된 이력에서도 이전 커밋은 커밋 SHA로 계속해서 접근할 수 있습니다. 이는 모든 분리된 커밋들의 자동 정리가 이뤄지거나 매뉴얼으로 정리가 실행될 때까지의 경우입니다. 심지어 정리가 되더라도 해당 커밋을 가리키는 참조가 여전히 존재한다면 이전 커밋이 제거되지 않을 수 있습니다.

이력을 변경하는 것이 허용되는 경우

당신이 공개 브랜치나 다른 개발자들이 사용할 수 있는 브랜치에서 작업할 때에는 이력을 변경하지 않아야 합니다.

GitLab와 같은 대규모 오픈 소스 리포지터리에 기여할 때에는 여러분의 커밋들을 한 개로 압축할 수 있습니다.

기능 브랜치의 커밋들을 대상 브랜치에서 하나의 커밋으로 압축하려면 Merge 시 git merge --squash를 사용하세요.

note
기본 브랜치나 공유된 브랜치의 커밋 이력을 수정해서는 안 됩니다.

이력을 변경하는 방법

Merge Request의 기능 브랜치는 공개 브랜치이며 다른 개발자들에 의해 사용될 수 있습니다. 그러나 프로젝트 규칙이 코드 리뷰 이후 대상 브랜치에서 표시된 커밋 수를 줄이도록 요구할 수 있습니다.

git rebase -i를 사용하여 이력을 수정, 압축, 삭제할 수 있습니다.

#
# 명령어:
# p, pick = 커밋 사용
# r, reword = 커밋 사용, 그러나 커밋 메시지 편집
# e, edit = 커밋 사용, 그러나 수정을 위해 중단
# s, squash = 커밋 사용, 이전 커밋과 통합
# f, fixup = "squash"와 비슷하지만 이 커밋의 로그 메시지를 버림
# x, exec = 명령어 실행 (줄의 나머지 부분)을 셸에서 실행
# d, drop = 커밋 제거
#
# 이러한 줄들은 순서를 바꿀 수 있습니다. 상단부터 하단까지 실행됩니다.
#
# 한 줄을 제거하면 해당 커밋은 손실됩니다.
#
# 그러나 모든 것을 제거하면 리베이스가 중지됩니다.
#
# 빈 커밋은 주석처리됩니다
note
리베이스를 중단하려면 에디터를 닫지 마세요. 대신 주석을 제거하고 저장하세요.

공유된 원격 브랜치에서 git rebase를 사용할 때 조심하세요. 원격 리포지터리에 푸시하기 전에 로컬에서 실험하세요.

# commit-id부터 HEAD(현재 커밋)까지 이력 수정
git rebase -i commit-id

커밋에서 민감한 정보 제거

과거 커밋에서 민감한 정보를 삭제하기 위해 Git을 사용할 수 있습니다. 그러나 이 과정에서 이력이 변경됩니다.

특정 필터로 이력을 다시 작성하려면 git filter-branch를 실행하세요.

이력에서 파일을 완전히 제거하려면 다음을 사용하세요:

git filter-branch --tree-filter 'rm filename' HEAD

git filter-branch 명령어는 큰 리포지터리에서 실행 시 오랜 시간이 걸릴 수 있습니다. Git 명령어들을 빠르게 실행하는 도구들이 있습니다. 이 도구들은 git filter-branch와 같은 기능을 제공하지 않지만 특정 사용 사례에 중점을 둔 채 더 빠르게 실행됩니다.

리포지터리 이력과 GitLab 스토리지에서 파일을 정리하는 더 많은 정보는 리포지터리 크기 줄이기를 참고하세요.

커밋을 제거하여 돌리기

  • 마지막 커밋을 취소하고 모든 것을 스테이징 영역에 다시 넣으세요:

    git reset --soft HEAD^
    
  • 파일을 추가하고 커밋 메시지를 변경하세요:

    git commit --amend -m "새 메시지"
    
  • 마지막 변경 사항을 취소하고 아직 푸시하지 않았다면 다른 변경 사항을 모두 제거하세요:

    git reset --hard HEAD^
    
  • 마지막 변경 사항을 취소하고 마지막 두 커밋을 모두 제거하세요, 아직 푸시하지 않았다면:

    git reset --hard HEAD^^
    

Git reset 샘플 워크플로우

다음은 일반적인 Git reset 워크플로우입니다:

  1. 파일을 편집하세요.
  2. 브랜치의 상태를 확인하세요:

    git status
    
  3. 잘못된 커밋 메시지로 변경 사항을 브랜치에 커밋하세요:

    git commit -am "kjkfjkg"
    
  4. Git 로그를 확인하세요:

    git log
    
  5. 올바른 커밋 메시지로 커밋을 수정하세요:

    git commit --amend -m "새로운 코멘트 추가됨"
    
  6. 다시 Git 로그를 확인하세요:

    git log
    
  7. 브랜치를 소프트 리셋하세요:

    git reset --soft HEAD^
    
  8. 다시 Git 로그를 확인하세요:

    git log
    
  9. 원격에서 브랜치의 업데이트를 받아오세요:

    git pull origin <branch>
    
  10. 브랜치의 변경 사항을 원격으로 푸시하세요:

    git push origin <branch>
    

새로운 대체 커밋으로 커밋 되돌리기

git revert <commit-sha>

git revertgit reset의 차이

  • git reset 명령어는 커밋을 제거합니다. git revert 명령어는 변경 사항을 제거하지만 커밋은 그대로 남습니다.
  • git revert 명령어는 안전합니다. 왜냐하면 되돌리기를 다시 되돌릴 수 있기 때문입니다.
# 변경된 파일
git commit -am "버그 도입됨"
git revert HEAD
# 변경을 되돌리는 새로운 커밋이 생성됩니다
# 이제 되돌린 커밋을 다시 적용하려고 합니다
git log # 되돌린 커밋의 해시를 가져옵니다
git revert <rev commit hash>
# 되돌린 커밋이 다시 생깁니다 (새로운 커밋이 다시 생성됨)

변경 내용 Unstage하기

Git에서 파일을 stage 할 때, 커밋을 준비하기 위해 파일의 변경 내용을 추적하도록 Git에 지시합니다. 파일의 변경 내용을 무시하고 다음 커밋에 포함시키지 않으려면 파일을 unstage 하세요.

파일 Unstage하기

  • Staging에서 파일을 제거하지만 변경 내용을 유지하려면:

    git reset HEAD <file>
    
  • 마지막 세 개의 커밋을 unstage하려면:

    git reset HEAD^3
    
  • 특정 파일의 변경 내용을 HEAD에서 unstage하려면:

    git reset <filename>
    

파일을 unstage한 후, 파일을 변경 전 상태로 되돌리려면:

git checkout -- <file>

파일 제거하기

  • 디스크와 리포지터리에서 파일을 제거하려면 git rm을 사용하세요. 디렉터리를 제거하려면 -r 플래그를 사용하세요:

    git rm '*.txt'
    git rm -r <dirname>
    
  • 디스크에 파일을 유지하고 리포지터리에서 제거하려면 (예: .gitignore에 추가하려는 파일) rm 명령을 --cache 플래그와 함께 사용하세요:

    git rm <filename> --cache