오우 쉿, Git? 책 출간
(ohshitgit.com)- Git은 실수 상황의 이름을 모르면 해결법을 검색하기 어렵기 때문에, 흔한 복구 시나리오별 명령을 바로 찾아 쓰게 정리한 자료임
- 저장소를 크게 망가뜨렸거나 삭제·병합 실수를 되돌릴 때는
git reflog로 작업 이력을 찾고git reset HEAD@{index}로 이전 상태로 돌아갈 수 있음 - 마지막 커밋의 작은 수정이나 메시지 변경은
git commit --amend로 빠르게 처리하되, 이미 공개/공유 브랜치에 푸시한 커밋에는 쓰면 안 됨 - 잘못된 브랜치에 커밋한 경우
reset,stash,checkout,stash pop흐름이나cherry-pick으로 옮길 수 있지만, 푸시 여부에 따라 안전한 방법이 달라짐 - 원격 저장소 상태로 강제 복구하는
reset --hard와git clean -d --force는 추적되지 않는 파일까지 지울 수 있어 실행 전 확인이 필요함
Git 실수를 복구하기 어려운 이유
- Git은 실수하기 쉽고, 문제를 고치려면 필요한 개념 이름을 이미 알아야 검색할 수 있는 경우가 많음
- 이 자료는 Git에서 자주 겪는 나쁜 상황을 평이한 영어와 명령 예제로 정리함
- 완전한 레퍼런스가 아니며, 같은 작업을 처리하는 다른 방법도 있을 수 있음
저장소를 과거 상태로 되돌리기
- 저장소를 크게 망가뜨렸거나 삭제한 내용을 되살리고 싶을 때는
git reflog로 작업 이력을 확인함
git reflog
# 모든 브랜치에 걸친 Git 작업 목록을 볼 수 있음
# 각 항목은 HEAD@{index} 형태의 인덱스를 가짐
git reset HEAD@{index}
reflog는 다음 상황에서 쓸 수 있음- 실수로 삭제한 내용 복구
- 저장소를 망가뜨린 시도 제거
- 나쁜 병합 이후 복구
- 실제로 동작하던 시점으로 되돌아가기
마지막 커밋을 고치는 방법
- 마지막 커밋 직후 작은 수정을 추가해야 하면
git commit --amend --no-edit를 사용함
# 변경 후
git add . # 또는 개별 파일 추가
git commit --amend --no-edit
- 이 명령은 마지막 커밋에 새 변경을 포함시킴
- 같은 작업을 새 커밋으로 만든 뒤
rebase -i로 합칠 수도 있지만,amend가 훨씬 빠름 - 공개/공유 브랜치에 이미 푸시된 커밋에는
amend를 쓰면 안 됨 - 마지막 커밋 메시지만 바꾸려면 다음 명령을 사용함
git commit --amend
# 안내에 따라 커밋 메시지 변경
잘못된 브랜치에 커밋했을 때
master에 실수로 커밋했지만 새 브랜치에 있어야 했다면, 현재 상태에서 새 브랜치를 만들고master의 마지막 커밋을 제거함
git branch some-new-branch-name
git reset HEAD~ --hard
git checkout some-new-branch-name
- 이 방법은 이미 공개/공유 브랜치에 푸시한 커밋에는 맞지 않음
- 다른 시도를 먼저 했다면
HEAD~대신HEAD@{number-of-commits-back}형태가 필요할 수 있음 - 아예 잘못된 브랜치에 커밋했다면
reset --soft,stash,checkout,stash pop흐름으로 옮길 수 있음
git reset HEAD~ --soft
git stash
git checkout name-of-the-correct-branch
git stash pop
git add . # 또는 개별 파일 추가
git commit -m "your message here"
- 같은 상황에서
cherry-pick을 쓰는 방법도 있음
git checkout name-of-the-correct-branch
git cherry-pick master
git checkout master
git reset HEAD~ --hard
변경 내용과 예전 커밋 되돌리기
- 파일을 변경했는데
git diff가 비어 있다면 이미 스테이징했을 수 있으므로--staged플래그가 필요함
git diff --staged
- 몇 커밋 전의 변경을 되돌리려면
git log로 해시를 찾고git revert를 실행함
git log
git revert [saved hash]
git revert는 해당 커밋을 되돌리는 새 커밋을 만듦- 전체 커밋이 아니라 특정 파일만 되돌릴 때는 파일의 예전 버전을 인덱스에 올린 뒤 커밋하는 흐름을 사용함
git log
git checkout [saved hash] -- path/to/file
git commit -m "Wow, you don't have to copy-paste to undo"
원격 저장소 상태로 강제 복구하기
- 저장소를 지우고 다시 클론하는 대신, 원격 저장소 상태로 맞추는 Git 명령을 사용할 수 있음
git fetch origin
git checkout master
git reset --hard origin/master
git clean -d --force
git reset --hard origin/master는 브랜치를 원격master상태로 맞춤git clean -d --force는 추적되지 않는 파일과 디렉터리를 삭제함- 망가진 브랜치마다
checkout,reset,clean을 반복해야 함 - 이 작업들은 파괴적이고 복구 불가능하므로 실행 전에 주의해야 함
범위와 기여
- 이 자료는 완전한 Git 레퍼런스가 아니라, 시행착오로 찾은 실전 복구 절차 모음임
- 번역 추가를 돕고 싶다면 GitHub에 PR을 제출할 수 있음
댓글과 토론
Hacker News 의견들
-
바꾸고 싶은 점이 몇 가지 있음: 항상
git checkout대신git switch를 쓰고,reset --hard는 최대한 피하겠음
예를 들어 “새 브랜치에 있어야 할 커밋을 실수로 master에 한 경우”는git branch some-new-branch-name,git switch -d HEAD~,git switch -C master,git switch some-new-branch-name순서로 처리할 수 있음
“잘못된 브랜치에 커밋한 경우”도 올바른 브랜치로git switch한 뒤git cherry-pick master,git switch -d master~,git switch -C master로 처리하겠음
“다 포기하고 원격 상태로 되돌리기”도git fetch origin,git restore -WS .,git clean -d --force,git switch -d origin/master,git switch -C master처럼 구성하겠음- Git의 내부 모델은 블롭, 커밋 트리, 커밋 포인터로 아름다운데, 명령줄 인터페이스와의 괴리가 너무 큼
Git 모델을 잘 이해해도 이런 레시피들은 직관적이지 않고, 각 명령의 특이한 동작까지 알아야 함
예컨대git switch -c some-new-branch-name은 이미 있지만, 브랜치만 다른 커밋으로 옮기는git move-branch master HEAD~같은 명령이 더 직관적일 것 같음 git checkout대신git switch와git restore를 쓰는 방향을 반대하려는 건 아니지만, 이 명령들은 Git v2.23에서 도입됐고 그 릴리스는 약 5년 전임
도움말 페이지에는 아직도THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.라는 경고가 들어 있음
물론 이 경고는 명령이 생긴 거의 처음부터 있었고, 5년이면 이제 실험적이라고 부르지 않아도 될 때가 된 듯함
그래도git checkout이 더 하위 호환적이고, 원래 이 웹사이트가 만들어졌던 시기의 Git 사용법을 반영한다고 볼 수 있음
https://github.com/git/git/blob/757161efcca150a9a96b312d9e78...
https://github.com/git/git/releases/tag/v2.23.0
https://github.com/git/git/commit/4e43b7ff1ea4b6f16b93a432b6...- 변경 사항을 스테이징 영역에 올릴 때 기본으로
git add .를 가르치는 건 별로임
특정 파일을 명시해서 추가하는 쪽이 나중에 “아 망했다” 할 여지가 적음 - 왜 이런 방식을 권하는지 근거가 궁금함
switch가checkout보다 나은 이유와reset --hard를 쓰지 말아야 하는 이유를 알고 싶음 - “
reset --hard를 피하라”는 게 무슨 뜻인지 모르겠음
실제로 왜 충분하지 않은지 궁금하고,alias git-restore-file='git restore --source=HEAD --'와 함께 꽤 자주 쓰는데 잘 동작하는 것 같음
- Git의 내부 모델은 블롭, 커밋 트리, 커밋 포인터로 아름다운데, 명령줄 인터페이스와의 괴리가 너무 큼
-
Git을 배울 때 기본 경로로 GUI 클라이언트를 추천하기 시작해야 함
그러면 이런 문제의 3분의 1은 해결되고, 또 다른 3분의 1은 애초에 생기지도 않을 것임
나중에 명령줄이 더 빠르다고 판단하면 그때 쓰면 되고, 먼저 사람들이 트리와 상호작용하는 방식을 시각적으로 봐야 함
개인적으로는 fork.dev를 좋아하지만, 요즘 클라이언트들은 대체로 비슷함- GUI가 Git에 더 나은 사용자 경험을 주는 경우가 많다는 데 동의함
Magit을 쓰면 “cherry-pick 중단” 같은 동작이 인터페이스 안에서 발견 가능하고, 다른 “X 중단” 작업과 같은 단축키를 씀
Git 명령줄만 써야 했다면 어디서 시작해야 할지 감이 없었을 것임
인터랙티브 리베이스 중 삭제하면 안 되는 커밋을 지운 적도 있는데, 리베이스 시작 시점마다 reflog에서 접근 가능한 스냅샷이 생겨서 잘못된 리베이스를 되돌리기 안전함
Magit의 reflog UI는 log UI와 같아서 처음 봐도 길을 잃지 않았지만, Git CLI였다면 무슨 일이 벌어지는지 몰랐을 가능성이 큼 - Git 호환 대안으로 jj를 추천하기 시작했음
jj를 인자 없이 실행하면 커밋 트리의 관련 부분과 현재 위치를 보여주는 점이 좋음
지금 작업 중인 커밋, 연결된 브랜치와 히스토리, 저장소 안의 다른 활성 브랜치, 각 커밋의 메시지와 변경 여부를 한눈에 볼 수 있어 방향을 다시 잡는 데 훌륭함 - Git 초보자에게는 항상 자신에게 맞는 UI를 찾아 명령줄보다 그걸 쓰라고 권함
하지만 거의 아무도 듣지 않고, 결국 CLI에서 고생하다가 브랜치를 망치고 main과 뒤처지며 rebase를 무서워하게 됨
UI에서는 끌어다 놓기나 비슷한 방식으로 해결될 일들이 많은데, 일종의 자기학대처럼 보임 - 트리와 상호작용하는 방식을 시각적으로 봐야 한다면, 동료들에게 도움이 됐던 인터랙티브 튜토리얼이 있음: https://learngitbranching.js.org/
- 예전에 버전 관리 GUI 프런트엔드를 써본 경험상, 모든 게 잘 돌아갈 때는 괜찮지만 한번 꼬인 걸 정리해야 하면 아무도 도와주기 어려웠음
인터페이스가 끔찍하더라도 팀의 다른 사람들이 쓰는 것과 같은 인터페이스를 쓰는 편이 소통하기 쉬웠고, 온라인 문제 해결 자료도 대체로 “보통 방식”을 가정하므로 활용하기 좋았음
- GUI가 Git에 더 나은 사용자 경험을 주는 경우가 많다는 데 동의함
-
2025년 4월이면 Git 20주년이라니 꽤 이상하게 느껴짐
그때 현장에 있었고, SVN이나 CVS를 대체하려고 git, darcs, bazaar 중 뭘 배워야 하나 고민했던 기억이 있음
Mercurial도 시도했는지 모르겠음
“GitHub 효과”가 버전 관리 시스템 분야에서 새 진입자의 필요성을 사실상 죽였는지 궁금함
어느 순간 야크 털 깎기가 충분히 끝난 걸지도 모름- GitHub 효과는 모든 네트워크 효과처럼 실제로 있지만, 그렇다고 개선이 불가능하다는 뜻은 아님
완전히 jj로 갈아탔고, Git 호환성이 있어서 다른 사람이 같이 바꿔주기를 기다릴 필요가 없음
GitHub의 여러 측면에 점점 더 좌절하고 있어서 언젠가는 모두가 그것도 넘어섰으면 하지만, 그쪽은 앞으로 가는 길이 그렇게 단순해 보이지 않음 - SVN은 늘 잘 동작했음
SVN은 직관적이고 99%의 경우에 충분히 잘 돌아가서 사람들에게 따로 “가르칠” 필요가 없음
우리 모두 1337 해커 흉내를 그만두고, Git이 대다수에게 과한 도구라는 걸 인정했으면 함 - 어떤 개선이든 최소한 Git 호환이어야 할 것임, 예를 들면 jj가 그럼
- GitHub 효과는 모든 네트워크 효과처럼 실제로 있지만, 그렇다고 개선이 불가능하다는 뜻은 아님
-
Git이 뭘 하는지에 대한 정신 모델은 꽤 잘 갖고 있다고 생각하지만, 명령이 조금만 복잡해지면 인자를 기억하지 못함
명령들이 발견 가능하지도 않고 외우기 쉽지도 않음
텍스트 UI가 나쁜 건지, 아니면 트리를 조작하는 일을 텍스트로 설명하는 것 자체가 어려운 건지 모르겠음- 둘 다임
복잡한 트리를 텍스트로 조작하는 건 쉽지 않고, 텍스트 UI도 객관적으로 나쁨: https://stevelosh.com/blog/2013/04/git-koans/
- 둘 다임
-
이 스레드에서 hg와 Mercurial 검색 결과가 나와서 반가움
Mercurial이 분산 버전 관리 전쟁에서 졌다는 게 아직도 믿기지 않고, 더 나은 도구라고 봄- 지금 기준으로도 Mercurial이 의심할 여지 없이 더 나은 도구임
사용 가능하고, 안정적이며, 실전 검증됐고, 활발히 지원됨
“보고 싶은 변화가 되어라”에 가까우니 그냥 쓰면 됨
졌다는 식의 표현은 역효과가 크고, 그런 말이 주는 암묵적 억제가 Mercurial 채택률을 낮게 유지하고 있음
호스팅은 적어도 Sourcehut과 heptapod.host에서 가능함
개인 Heptapod 인스턴스도 운영 중인데, Gitlab 포크에 Mercurial 직접 지원이 들어간 형태이고 그냥 잘 동작함 - 흥미롭긴 한데, GitHub 링크가 있는지 궁금함
- Mercurial은 더 느린 도구임
- Mercurial 호스팅이 필요하면 이 호스팅을 쓸 수 있음: https://hg.reactionary.software/
- 지금 기준으로도 Mercurial이 의심할 여지 없이 더 나은 도구임
-
Git은 여러 면에서 직관을 따르지 않아서 끝내 머리에 잘 안 들어오는 기술 중 하나였음
오래 써온 게 아니라면 거의 모든 작업마다 Google을 찾거나 man 페이지를 봐야 할 가능성이 큼- SVN에서 시작했다가 Git으로 넘어오니, 차이점을 배울 필요는 있었지만 여러 면에서 더 쉬워졌다고 느꼈음
특히 큰 코드베이스를 SVN에서 많은 브랜치로 관리하고 다시 병합하려던 공포를 겪고 나면, Git이 그 부분에서는 말도 안 되게 훨씬 낫다는 게 보임
처음부터 Git으로 들어오는 사람에게는 학습 곡선이 훨씬 클 수 있겠지만, 사람들이 어디서 막히는지 여전히 이해하기 어려울 때가 있음 - 어제 히스토리에서 파일을 삭제하려고 했음
내장 방식인filter-branch를 쓰면 긴 지연과 함께 Control+C를 누르고filter-repo라는 서드파티 Python 스크립트를 내려받으라는 경고가 뜸
- SVN에서 시작했다가 Git으로 넘어오니, 차이점을 배울 필요는 있었지만 여러 면에서 더 쉬워졌다고 느꼈음
-
자랑스럽진 않지만, 제일 자주 쓰는 “망했다” Git 작업은 로컬 저장소를 지우고 다시 클론한 뒤 변경 사항을 다시 적용하는 것임
95%는 아주 잘 먹히고, 나머지는 DevOps 담당자에게 도움을 요청함- Git을 거의 15년 썼고, Git을 내부적으로 써서 특정 결과를 내는 프로그램/제품도 두 번 만들어봤지만, 아직도 Git에서 조금 험한 작업을 하기 전에는
cp -R .git ../git-backup같은 걸 해둘 때가 있음
너무 크게 망치면.git디렉터리 전체를 예전 복사본으로 바꾸면 되기 때문임
특정 작업이나 작업 묶음을 올바르게 되돌리는 방법을 찾는 것보다 훨씬 빠름 - 혼자가 아님: https://xkcd.com/1597/
- Git을 거의 15년 썼고, Git을 내부적으로 써서 특정 결과를 내는 프로그램/제품도 두 번 만들어봤지만, 아직도 Git에서 조금 험한 작업을 하기 전에는
-
Git에 대해 사람들이 꼭 체화했으면 하는 건, Git이 추가 전용 데이터 저장소라는 점임
작업을 저장소에 넣으려면 커밋하면 되고, 한 번 들어간 작업은 Git 명령으로 제거할 수 없음
reflog도 이 원리로 동작함
뭘 하든 이전 브랜치 상태로 돌아가 데이터 저장소에서 작업을 복원할 수 있음
하지만 커밋하지 않으면 Git은 도와줄 수 없으니 그냥 커밋해야 함
항상 커밋하고, 부담이 너무 크거나 자꾸 잊는다면 도구를 고쳐야 함
하루 종일 코드를 쓴다면 일반적으로 최소한 한 시간에 한 번은 커밋해야 함 -
사소한 얘기지만,
git reflog를 볼 때마다 re-flog처럼 보임
그래서 이 명령을 잘 못 외우는 걸지도 모르겠고, 무의식적으로 “reference log”로 생각하지 못하는 듯함
대신 Git에게 뭔가를 다시 싸게 팔라고 시키는 느낌이 듦 -
Git 사용자는 아니지만, 이런 걸 보면
git commit자체는 의미가 없고 중요한 건 커밋이 push되거나 merge되는 시점뿐이라는 생각이 강해짐
텍스트 파일 저장과 비슷함
파일을 저장할 때마다 짧은 메시지를 쓰지는 않으니, 그냥 넘어가면 됨- 힘든 디버깅 여정 끝에 생긴 변경이고 다른 사람들이 나중에 다시 참고할 가능성이 있다면 메시지를 씀
짧게가 아니라, 변경이 작을수록 설명은 더 길어질 때도 있음
영향이 큰 2~3줄 변경에 2~3문단을 쓴 적도 있음 - 로컬 커밋과 원격 커밋은 목적이 다르다는 게 너무 명확해 보임
로컬 커밋은 모든 게 컴파일되고 동작하는 지점에서 체크포인트를 만드는 방법임
그래야 변경을 stash하거나 망쳤을 때 동작하던 상태로 돌아갈 수 있음
push하기 전에는 reset해서 적절한 커밋들로 나누면 원격 히스토리는 깔끔하게 유지되고wip더미가 되지 않음
아니면 PR을 squash merge해서 양쪽 장점을 모두 얻을 수도 있음 - 커밋은 Git이 내부적으로 파일 복사본을 만들게 하는 동작이라 매우 중요함
다만more fixes같은 의미 없는 메시지를 입력할 필요는 없음
보통(WIP) 기능명같은 초기 커밋을 만들고, 만족할 때까지 amend 커밋을 반복한 뒤 마지막에 메시지에서(WIP)를 제거함 - 텍스트 파일 저장과 비교하지만, 잠들 때마다 하루에 대한 짧은 글을 쓰는 사람들도 많음
Git 리비전 로그도 그런 일기와 비슷하다고 봄
별도의 TODO 문서보다 정리되어 있고, 이미 한 일을 읽기 쉬운 방식으로 설명하도록 유도함
얼마나 되돌릴지 판단할 때 당시 커밋에서 무슨 생각을 했는지 아는 게 유용할 수 있고, 접근 방식이 틀렸다면 사후 분석에도 도움이 됨
merge나 push되지 않은 작업이 목표가 붙은 별도 변경으로 정리돼 있으면, 그중 일부가 좋고 일부가 나쁠 때 좋은 부분만 보존하기가 훨씬 쉬워짐 - push 전까지는 전부 로컬이니 원하는 대로 해도 됨
그래도 기술적으로 커밋은 의미 없지 않음
커밋이 없으면 push하거나 merge할 대상도 없음
비기술적으로도 왜 뭔가를 바꾸는지 평문으로 기록하는 건 매우 유용함
그런 설명을 너무 자주 쓰게 된다면 “변경”의 단위를 더 좁히거나, 동시에 건드리는 일을 더 엄선해야 함
Git을 쓰지 않는다면 버전 관리와 변경 관리는 무엇으로 하는지 궁금함
- 힘든 디버깅 여정 끝에 생긴 변경이고 다른 사람들이 나중에 다시 참고할 가능성이 있다면 메시지를 씀