일이 지났습니다. 시의성에 유의하세요
서문
자주 git을 사용하지만, 항상 시간이 없어서 작성하지 못했는데 이제야 시간이 생겨서 자주 사용하는 명령어를 정리해보려고 합니다.
본문
새로운 브랜치를 생성하고 해당 브랜치로 전환하기:
1 | |
브랜치 전환:
1 | |
브랜치 삭제 (먼저 checkout 다른 브랜치로 이동해야 함):
1 | |
주의: 로컬에서 새 브랜치를 생성하고 git push하지 않은 경우, 다음 명령어로 브랜치를 삭제하려고 하면 경고가 표시됩니다.
1 | |
정말 삭제하려면 설명에 따라 강제 삭제하면 됩니다.
파일 제출(commit) 절차는 다음과 같습니다:
1 | |
git add에는 주로 세 가지 매개변수가 사용됩니다:
git add -A모든 변경 사항(추가, 삭제, 수정)을 스테이징 영역에 추가합니다.git add .파일 추가 및 수정 사항을 스테이징 영역에 추가하지만, 삭제된 파일은 포함하지 않습니다.git add -u파일 삭제 및 수정 사항을 스테이징 영역에 추가하지만, 새로 추가된 파일은 포함하지 않습니다.
인생에는 후회약이 없지만 git은 있습니다. 실수로 잘못된 파일을 제출했거나 커밋 메시지를 잘못 작성한 경우, 다음 명령어를 상황에 따라 실행할 수 있습니다.
먼저 git status로 버전 상태를 확인합니다. (저는 zsh를 사용합니다.) 빨간색 파일은 아직 스테이징 영역에 git add되지 않은 상태이고, 녹색 파일은 이미 git add 스테이징 영역에 추가되어 git commit을 기다리는 상태입니다.
이때 스테이징 영역에 git add하고 싶지 않다면, git reset HEAD을 사용하여 모든 파일을 원래의 git add되지 않은 상태로 되돌릴 수 있습니다. 특정 파일만 스테이징 영역에 git add하지 않으려면 파일 이름을 지정해야 합니다:
1 | |
때로는 수정된 파일을 스테이징 영역에 git add하지 않을 뿐만 아니라, 해당 파일의 모든 수정 사항을 무시하고 원래 상태로 되돌리고 싶을 때가 있습니다. 이 경우 파일을 다시 체크아웃하면 됩니다:
1 | |
여기서 철자 오류가 아님에 유의하세요. checkout와 filename 사이에는 양쪽에 공백이 있는 하이픈이 있습니다.
git add 이후에는 git commit이(가) 옵니다. -m 매개변수를 사용하여 커밋 메시지를 작성할 수 있는데, 이는 좋은 습관이며 강제됩니다. -m 매개변수가 없으면 vim 편집 상태로 들어가 메시지를 추가하라는 프롬프트가 표시됩니다:
1 | |
이때 커밋 메시지를 잘못 작성했고, commit을(를) 원하지 않는 경우(HEAD이(가) 이미 변경된 상태), 두 가지 방법이 있습니다:
하나는 commit을(를) 제거하는 것입니다. (아직 push되지 않았고, 로컬 저장소에만 commit된 상태이기 때문입니다.)
먼저 git log을(를) 실행하고, 실수로 commit하기 전의 commit id을(를) 기억해 둡니다. 이는 hash(SHA)입니다. 예를 들어: 73cf3bfc3419a85e959d7ecfcb917d9cdc24b3c9 또는 최근 push의 HEAD을(를) 직접 사용해도 됩니다:
1 | |
(이때는 git add 때처럼 git reset HEAD을(를) 사용할 수 없습니다. git commit 이후 HEAD이(가) 이미 변경되었기 때문입니다(즉, git log을(를) 통해 커밋 기록을 확인할 수 있는 상태입니다).)
여기서 git reset 명령어의 세 가지 매개변수인 --mixed, --soft, --hard에 대해 잠시 설명하겠습니다:
인터넷에는 다양한 튜토리얼이 있고, 그림 설명이나 공식 브랜치 설명을 인용한 것도 많지만 복잡합니다. 제가 간단히 설명하겠습니다.
간단하게, 예를 들어 설명하는 방식이 더 쉽습니다. —Xheldon
git reset의 기본값은 mixed 매개변수입니다. 즉, git reset xxxx을(를) 실행하면(xxx는 SHA 또는 commit되지 않은 상태일 때 HEAD임) git reset --mixed xxxx이(가) 실행됩니다. 이 명령어는 파일을 수정한 후 아무 git 명령어도 실행하지 않은 상태(파일이 빨간색 상태)로 복원합니다.
--soft 매개변수는 파일을 커밋되지 않은 상태로만 복원하며, 파일은 여전히 git add된 상태(녹색 상태)로 유지됩니다.
--hard은(는) 더 강력합니다. 지정한 커밋 기록의 상태로 파일을 완전히 복원하며, add했는지, commit했는지, 파일을 수정했는지 여부를 전혀 고려하지 않습니다. git reset --hard xxx을(를) 실행하는 것은 위험할 수 있으며, 현재의 모든 수정 사항을 로컬에서 삭제합니다.
git reset --hard xxx 이후에는 파일이 로컬에서 삭제되고 모든 수정 사항도 삭제되었습니다. 하지만 hard 삭제된 파일의 수정 기록을 복구하려면 어떻게 해야 할까요? git reflog을(를) 사용하세요.
당신의 모든 commit 및 reset 작업은 git이(가) 기록을 생성합니다. 이 기록은 git reflog을(를) 통해 찾을 수 있으며, 각 기록 앞에는 짧은 hash이(가) 있습니다. 이 짧은 hash을(를) 복사하여 git reset --hard short_hash을(를) 다시 실행하면 됩니다.
주의: git commit을(를) 실행한 후에는 HEAD이(가) 이미 제출된 파일의 변경 상태로 바뀝니다. 이때 git reset --mixed HEAD 또는 git reset --soft HEAD을(를) 실행해도 효과가 없습니다(현재의 HEAD이(가) 당신이 commit한 시점이기 때문입니다(심지어 push하지 않았더라도)). git add 이후, git commit 이전의 상태로 돌아가려면 git reset --soft commit_id이(가) 필요합니다. git add 이전의 상태로 돌아가려면 git reset --mixed commit_id이(가) 필요하거나, 직접 git reset commit_id을(를) 실행하면 됩니다.
git commit 후회약의 다른 이름은 --amend입니다. 커밋한 후 후회하고, 메시지를 잘못 작성했거나 파일을 또 수정했을 때, commit 기록을 또 생성하기 싫다면(보기 흉하고 당신이 커밋 메시지를 잘못 작성하는 초보 같은 실수를 했다는 인상을 주기 때문입니다), 다음을 실행하세요:
1 | |
그러면 됩니다.
후회약을 먹고 git commit 이후 문제가 없음을 확인했다면 git push을(를) 할 수 있습니다. 이때 현재 브랜치에서 분기된 원격 브랜치에 다른 사람이 업데이트를 제출하지 않았다면 fast forward 모드(한국어로는 '빠른 전진 모드’라고 번역됨)를 사용하여 직접 병합할 수 있습니다. 병합 상황을 시각적으로 확인하려면 다음을 사용하세요:
1 | |
만약 당신이 브랜치를 분리하는 동안 다른 사람도 브랜치를 커밋했다면, 충돌이 없다면 바로 병합할 수 있습니다. 먼저 git pull을 수행한 후 git push을 실행해야 합니다.
충돌이 있는 경우 다른 사람의 수정과 당신의 수정 사이에 어떤 충돌이 있는지 알려줄 것입니다. 이 경우 수동으로 해결해야 하며, 해결 후에는 git add, git commit을 수행하면 됩니다.
브랜치 병합:
1 | |
여기서의 상황은 非fast forward 모드입니다. 즉, 브랜치 B가 브랜치 A에서 분리된 후, 부모 브랜치인 A가 변경되었고, 브랜치 B도 일부 내용을 수정한 상태에서 다시 브랜치 A로 병합하려 할 때 발생하는 상황입니다:
주의할 점은, 당신이 브랜치 A에 있다면 merge해야 하는 것은 브랜치 B이며, merg된 브랜치 B는 반드시 git push되어 있어야 합니다. 브랜치 B가 단순히 git commit된 상태라면 병합되지 않습니다. 왜냐하면 git merge branch_name의 branch_name는 branch_name의 원격 origin에서 merge된 것이며, commit은 로컬 HEAD만 수정했고, push이 없으면 원격 origin은 수정되지 않기 때문입니다.
브랜치 B가 이미 자신의 변경 사항을 원격에 git push했고, 로컬 A도 자신의 변경 사항을 로컬 저장소에 git add했다면, 브랜치 A에서 git merge B를 실행할 때 (변경된 파일이 config.js이라고 가정) 다음과 같은 상황이 발생합니다:
1 | |
이는 두 브랜치를 병합할 때 오류가 발생하여 중단되었다는 의미입니다. 현재 브랜치의 변경 사항을 임시 저장(git stash)하거나 변경 사항을 커밋(git commit)하라는 메시지가 표시됩니다. 이후 git merge을 수행한 후 충돌이 발생하면 수동으로 수정하고 다시 커밋합니다.
OK, 먼저 현재 변경 사항을 git commit한 후 다시 git merge B을 실행하면 (충돌 파일이 config.js라고 가정) 다음과 같이 표시됩니다:
1 | |
수동으로 해결한 후 다시 git add을 수행하고 git commit을 실행하면 됩니다.
주의할 점은, 한 브랜치에서 파일을 수정한 후 checkout를 다른 브랜치로 수행할 때 충돌이 발생하지 않으면 아무런 메시지가 표시되지 않으며, 파일 변경 사항은 그대로 유지됩니다. 따라서 파일을 한 브랜치에서 수정한 후 다른 브랜치에 커밋할 수 있습니다. 그러나 한 브랜치에서 파일을 수정한 후 checkout을 다른 브랜치로 수행할 때 충돌이 발생하면 (예: 다른 브랜치가 git pull되었거나 다른 브랜치가 동일한 파일의 동일한 부분을 git commit한 경우):
1 | |
이 경우 먼저 git stash을 사용하여 변경 사항을 저장해야 합니다 (이 stash储藏은 git add暂存이 아닙니다. stash은 파일을 특정 영역에 저장한 후 브랜치를 전환한 다음, 패치처럼 이 stash을 전환된 checkout 브랜치에 적용하거나, stash을 적용하지 않고 전환한 브랜치에서 작업을 완료한 후 다시 돌아와서 이 stash을 적용할 수 있습니다. 물론 stash을 계속 적용하지 않으려면 영원히 적용하지 않아도 됩니다).
여기서 git stash에 대해 언급되었습니다. 이 명령은 두 브랜치의 수정이 동시에 필요한 경우에 사용됩니다. 한 브랜치에서 작업을 반쯤 완료한 상태에서 다른 브랜치도 수정해야 할 때, 반쯤 완료된 내용을 버릴 수 없고 commit할 수도 없습니다. 왜냐하면 checkout을 통해 전달할 때 conflict을 초래할 수 있기 때문입니다. 따라서 stash을 저장해야 합니다:
먼저 현재 상태를 확인합니다. git status:
1 | |
변경 사항을 저장합니다. git stash:
1 | |
다시 git status을 확인합니다:
1 | |
이제 안심하고 다른 브랜치로 전환할 수 있습니다. 여기서 주의할 점은, 다른 브랜치로 전환할 때 저장된 브랜치를 현재 브랜치에 적용할 수도 있습니다 (충돌을 걱정하지 않는다면). 저장 목록을 확인하려면 git stash list:
1 | |
이것은 방금 stash된 변경 사항입니다. git stash apply을 사용하여 가장 최근의 변경 사항을 적용할 수 있습니다. stash의 변경 사항이 여러 개라면 stash의 이름을 지정합니다:
1 | |
적용 후에는 이 stash이 저장 영역에서 삭제됩니다. 삭제하지 않으려면:
1 | |
나중에 삭제하려면:
1 | |
위의 두 명령에 stash_name을 포함하지 않으면 기본적으로 가장 최근의 stash이 삭제됩니다.
지금 당장 생각나는 것은 이 정도입니다. 익숙하지 않다면 Sourcetree을 사용하세요.
업데이트
git revert/reset/rebase은 설명만으로는 이해하기 어렵습니다. 직접 명령을 입력해 테스트해 보아야 합니다.
나는 깔끔한 커밋 기록을 위해 git rebase을 사용하는 것을 좋아합니다. 하지만 이 명령은 위험하며, 몇 가지 사용 시나리오에 주의해야 합니다. git rebase보다 더 위험한 것은 git reset입니다. 이 명령은 현재 프로젝트를 특정 커밋으로 재설정합니다. git revert은 상대적으로 안전하지만, revert을 이전 커밋에 수행하려면 캐시 영역이 비어 있는지 확인해야 합니다. 그렇지 않으면 오류 메시지가 표시될 수 있습니다.
물론, git 명령은 수없이 많습니다(과장). 일부 명령은 특정 시나리오를 위해 설계되었으며, 경험해 보기 전에는 그 용도를 이해하기 어려울 수 있습니다. 이는 정상적인 현상입니다.
예를 들어, git reset commitId은 HEAD을 commitId이 있는 위치로 이동시킵니다. 당신은 혼란스러울 수 있습니다. HEAD을 이동시키는 것이 무슨 의미가 있을까? 이 명령의 목적은 무엇일까? 또 다른 예로, git revert commitId은 commitId의 커밋을 제거하지만 HEAD의 포인터는 이동시키지 않습니다.
코드 확인하기(d0b9def은 commit -m 'reset/revert test 3' 커밋에 해당하며, 현재 HEAD은 'reset/revert test 4'에 있음):
git revert d0b9def 이후에 여러분의 코드는 다음과 같이 보일 수 있습니다:
1 | |
동일한 HEAD에서 git reset d0b9def를 실행한 후, 작업 공간은 다음과 같이 보일 수 있습니다:
1 | |
HEAD 이동 여부의 차이점을 보셨나요? revert는 반드시 수동으로 충돌을 해결하도록 요구할 것입니다. 왜냐하면 이 명령은 commitId 이전의 부모 commit부터 현재 HEAD까지의 모든 변경 사항 중 d0b9def을 제외하고 보존하기 때문입니다. 반면 reset는 충돌 해결을 요구하지 않고 조용히 HEAD를 이동시켜 d0b9def 이후의 모든 변경 사항을 파일 수정으로 표시하며, 수동으로 git add/commit를 수행해야 합니다. 물론 push --force도 빠지지 않습니다.
따라서 revert는 公开 커밋을 안전하게 되돌리기 위해 설계되었으며, reset은 本地 변경 사항을 재설정하기 위해 설계되었습니다. 두 명령어의 목적이 다르기 때문에 구현 방식도 다릅니다: 재설정은 변경 사항 집합을 완전히 제거하는 반면, 되돌리기는 원래 변경 사항을 보존하면서 새로운 커밋으로 되돌리는 방식을 사용합니다.
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.