Oh Shit, Git!?!

Git은 어렵다. 툭하면 실수를 저지르게 되고, 실수를 되돌리는 방법을 찾는 건 너무너무 복잡하다. Git의 공식 문서에서 문제에 대한 해결책을 찾으려면, 이미 그 해결책의 이름을 알고 있지 않고서는 불가능하다. 마치 닭이 먼저냐 달걀이 먼저냐의 문제처럼.

그래서 이 글에서는 Git을 사용하면서 내가 겪었던 여러가지 문제들을 소개하고, 그걸 어떻게 해결했는지를 알기 쉬운 한국어로 설명하고자 한다.

젠장, 뭔가 단단히 잘못됐는데, 다 뒤엎고 예전으로 돌리고 싶어!

git reflog
# git의 모든 브랜치에서 있었던
# 지금까지의 모든 기록을 볼 수 있다
# 각각 HEAD@{index} 형태로 index를 가지고 있으니,
# 잘못되기 전에 해당하는 index를 찾고
git reset HEAD@{index}
# 타임머신을 타자!

이 기능은 실수로 지운 파일을 되돌리거나, 뭔가 잘못 수정한 걸 되돌리거나, 실수로 머지한 걸 되돌리거나, 됐고 그냥 잘 작동했던 때로 돌아가고 싶을 때 사용하면 된다. 나는 개인적으로 reflog엄청 많이 사용하고, 당신들도 사용해볼 것을 아주 아주 아주 추천한다!

젠장, 방금 커밋했는데 하나 깜빡한 걸 발견했어!

# 새로 바뀐 파일들을 add 하고
git add . # 또는 각각의 파일들을 add
git commit --amend --no-edit
# 마지막 커밋에 바뀐 파일이 등록된다
# 주의: 절대로 원격 저장소에 push 된 커밋을 고치지 말 것

커밋을 하고나서 테스트/린터를 돌렸더니... 아 망할, 등호 뒤에 띄어쓰기를 깜빡했네, 같은 상황에서 내가 자주 사용하는 기능이다. 새로 커밋을 하고 rebase -i를 이용해서 두 커밋을 squash하는 것도 가능하지만, 이 방법이 만 배는 더 빠르다.

주의: 이 기능은 반드시 아직 로컬에만 있는 커밋에 사용해야 한다. 이미 공개 브랜치에 push된 커밋은 절대로 수정하면 안 된다!

젠장, 커밋 메세지를 잘못 썼어!

git commit --amend
# 에디터가 켜지고 커밋 메세지를 수정할 수 있다

요상한 커밋 메세지 포맷을 정확히 지켜야 한다면, 자주 쓰게 될 기능이다.

젠장, 다른 브랜치에 커밋해야 하는 걸 실수로 master에 커밋해 버렸어!

# 현재 master의 상태로 새로운 브랜치를 만든다
git branch some-new-branch-name
# master 브랜치의 마지막 커밋을 제거한다
git reset HEAD~ --hard
git checkout some-new-branch-name
# 이 브랜치에는 그 커밋이 남아있다 :)

주의: 이미 원격 저장소에 커밋을 push 했다면 슬프게도 이 방법은 소용이 없다. 또, 만약 여러 개의 커밋을 했다면, HEAD~ 대신 git reset HEAD@{돌아갈-커밋-수}를 사용하면 된다. 수많은 사람들이 (내가 몰랐던 방식으로) 이 방법을 최대한 간결하게 만드는 데 기여해주었다. 감사를 표한다!

젠장, 실수로 이상한 브랜치에 커밋을 해버렸어!

# 마지막 커밋을 취소하되, 변경된 사항은 남겨둔다
git reset HEAD~ --soft
git stash
# 올바른 브랜치로 이동
git checkout name-of-the-correct-branch
git stash pop
git add . # 또는 각각의 파일들을 add
git commit -m "your message here";
# 이제 올바른 브랜치에 커밋이 됐다

이 상황에서 cherry-pick을 사용하는 방법을 많은 사람들이 추천해주었다. 원하는 방법을 골라 사용하길!

git checkout name-of-the-correct-branch
# master의 마지막 커밋을 선택
git cherry-pick master
# master에서 해당 커밋을 제거
git checkout master
git reset HEAD~ --hard

젠장, diff를 실행했는데 아무 것도 안 보이잖아?!

분명히 뭔가를 바꿨는데, diff의 결과가 아무 것도 나타나지 않는다면, 아마 add를 실행해서 파일을 staging 상태로 만들었을 가능성이 높다. 이 경우에는 특별한 옵션을 주어야 한다.

git diff --staged

이제 기대했던 결과가 나올 것이다 ¯\_(ツ)_/¯ (이건 버그가 아니라 올바른 Git의 동작인데, 처음 겪으면 아주 당혹스럽고 이상한 결과처럼 느껴진다!)

젠장, 다섯 커밋 전에 한 커밋을 되돌려야 하잖아!

# 되돌려야 할 커밋을 찾는다
git log
# 방향키로 예전 커밋을 살펴보고
# 원하는 커밋을 찾으면 해당 커밋의 hash를 기억한다
git revert [saved hash]
# git이 해당 커밋을 되돌리는 새로운 커밋을 생성할 것이다
# 에디터 창이 나타나면, 새로 커밋 메세지를 입력하거나,
# 그냥 저장하고 종료한다

일일이 옛날 커밋을 찾아서 예전 파일 내용을 복사-붙여넣기 할 필요가 없다! 버그를 커밋했다면, revert로 그 커밋을 되돌리는 것이 가능하다.

물론 전체 커밋이 아니라 한 파일만 되돌리는 것도 가능하다! 그치만 물론, git의 방식이 다 그렇듯이, 생판 다른 명령어를 써야한다...

젠장, 파일을 수정한 걸 되돌려야 하잖아!

# 해당 파일이 수정되기 전의 커밋 해시를 찾는다
git log
# 방향키로 예전 커밋을 살펴보고
# 커밋을 찾으면, 해시를 기록
git checkout [saved hash] -- path/to/file
# 예전 버전 파일로 바뀌어 있을 것이다
git commit -m "Wow, you don't have to copy-paste to undo"

이 방법을 알고 모르고 차이는 엄청 크다. 엄-청. 근데 솔직히 checkout --은 봐도 봐도 이상하다. 진짜 파일 하나 되돌리는데 이런 요상한 명령어를 써야할까요, 리누스 토르발즈씨?

아 몰라 좆까, 다 포기할래.

cd ..
sudo rm -r fucking-git-repo-dir
git clone https://some.github.url/fucking-git-repo-dir.git
cd fucking-git-repo-dir

Eric V. 가 이 방법(장난)을 알려줬다. 그러니 sudo를 쓰는 것에 대해서 불평하고 싶다면 부디 그에게 하기를.

좀 더 진지한 버전으로, 만약에 레포지토리가 너무 너무 엉망이 돼서 원격 레포지토리 버전으로 "git에서 허락해준 방식"으로 초기화하고 싶다면, 아래 방법을 쓰면 된다. 단, 이건 되돌릴 수 없는 명령어니 주의할 것!

# origin에서 최신 상태를 받아온다
git fetch origin
git checkout master
git reset --hard origin/master
# 추적되고 있지 않은 파일/폴더를 모두 삭제한다
git clean -d --force
# 초기화하고 싶은 각 branch에 대해서 checkout/reset/clean을 반복

*Disclaimer: 이 사이트를 엄밀한 레퍼런스 용도로 사용하지 말 것. 더 엄밀하고 깔끔한 방법으로 이 사이트의 내용과 같은 기능을 하는 해결책이 분명히 있을 수 있다. 그러나 여기 소개한 내용은 내가 직접 수많은 욕설을 내뱉고 실수를 거듭하여 발견한 방법들이고, 그렇기에 이 사이트에 *적절한 양의 욕설을 섞어서* 소개한다. 쓰든지 말든지는 맘대로 하시길!

이 사이트를 다양한 언어로 번역해준 많은 사람들에게 감사를 전합니다! Björn Söderqvist (sv) · Moritz Stückler (de) · Daniil Golubev (ru) · Łukasz Wójcik (pl) · fedemcmac (it) · Michel (fr) · Andriy Sultanov (ua) · Meiko Hori (ja) · Alex Tzimas (gr) · Martijn ten Heuvel (nl) · Elad Leev (he) · Franco Fantini (es) · Catalina Focsa (ro) · Davi Alexandre (pt_BR) · Nemanja Vasić (sr) · Tao Jiayuan (zh) · Eduard Tomek (cs) · Ricky Gultom (id) · Khaja Md Sher E Alam (bn) · Rahul Dahal (ne) · Taha Paksu (tr) · Kitt Tientanopajai (th) · Gyeongjae Choi (ko) . 다른 방식으로 이 글에 기여해준 Iain Murray · Frank Taillandier · David Fyffe · Lucas Larson · Artem Vorotnikov 에게도 감사를 표합니다.

이 글을 새로운 언어로 번역하는 것에 관심이 있다면, GitHub에 PR을 날려주세요.