만약 git을 경험해 본 적이 있다면, 다음 질문에 대답해 보자. git reset
명령어를 잘못 사용해서 작업 중인 코드가 날아가는 경험을 해본 적이 있는가? 혹은 협업하는 레포지토리의 커밋 이력이 꼬이는 경험을 해본 적이 있는가? 여기에 두 가지 경험을 모두 겪어 본 사람이 있다.
오랜만에 git reset
명령어를 사용하면 좋을만한 상황에 직면했다. 사이드 프로젝트를 진행하던 중, github에서 dev
브랜치를 main
브랜치로 merge 하는 과정에서 conflicts가 많이 발생하는 이슈를 해결하기 위해서였다.
해당 프로젝트에서는 dev
→ main
→ prd
브랜치로 커밋을 옮겨가는 브랜치 전략을 사용하고 있다. 이와 관련해서, squash and merge
를 허용하지 않고 Create a merge commit
을 권장하는 merge 전략을 가져가고 있다.
github merge 종류에는 총 3가지가 있다.
더 자세하게 알고 싶다면 github merge에 대해 잘 정리된 공식 문서를 보면서 공부해 보자.
이전에 dev
→ main
브랜치 merge 하는 과정에서 squash merge
를 했었던 바람에, squash merge
로 생겨난 커밋 이후로 dev
와 main
브랜치 사이에 commit 이력들이 달라지게 된 것이다. 그래서 Pull Request를 시도했더니 conflicts가 발생한 것이다.
Resolve conflicts 버튼을 클릭해서 conflicting files를 히나 하나 확인하고 conflicts를 직접 고치는 방법도 있지만, 브랜치 전략을 유지하고 문제를 근본적으로 해결하기 위해서는 git reset 명령어를 사용하여 squash merge
를 한 커밋 이전으로 되돌아가야 한다는 판단을 내렸다.
그동안 git reset 명령어로 피해를 봤었던 이유는, 당연하게도 명령어의 동작 원리를 제대로 이해하지 못하고 사용해서였다. 그래서 이번에는 git reset 명령어에 대해 충분히 이해하는 시간을 가졌다.
reset
명령은 아래와 같은 순서대로 동작한다.
HEAD
가 가리키는 브랜치를 옮긴다. (--soft
옵션이 붙으면 여기까지)HEAD
가 가리키는 상태로 만든다. (--mixed
옵션이 붙거나, 옵션을 붙이지 않으면 여기까지)--hard
옵션이 붙으면 여기까지)더 자세하게 알고 싶다면 github reset 명령어에 대해 잘 정리된 Pro Git book을 보면서 공부해 보자.
이번 이슈를 해결하기 위해, 내가 세운 작업 계획은 이렇다.
git reset --hard { commit id }
dev
→ main
브랜치 기본 merge 진행main 브랜치에서 혹시나 실수할 경우 브랜치를 복구할 수 있도록 백업 브랜치를 생성해 준다.
git pull origin main
명령어로 main 브랜치를 최신화한 후,
git checkout -b
명령어를 사용해서 backup/main-after-v0.0.7
라는 이름의 백업 브랜치를 생성했다.
그런 다음 해당 브랜치를 git push origin {branch_name}
명령어를 통해 원격 저장소에 반영했다.
이제 본격적으로 과거 커밋 이력으로 돌아가는 작업을 수행한다. 아래 이미지는 백업용 브랜치지만, 해당 시점에는 main 브랜치와 커밋 이력이 동일했기 때문에 이 이미지로 main 브랜치를 어떻게 조작할지 설명해 보고자 한다.
현재 main 브랜치의 HEAD 포인터는 가장 최신 커밋 이력인 d53c427
커밋을 가리키고 있다. 문제가 되었던 Squash Merge 커밋은 8864150
커밋이다. 나의 목표는 8864150
커밋 바로 이전 커밋인 8fd8ed6
으로 돌아가는 것이다. (물론 실제 커밋ID(SHA)의 길이는 7글자보다 훨씬 길지만, 편의상 보이는 7글자로 설명했다.)
git checkout main
명령어로 다시 main 브랜치로 checkout 해서 돌아왔다. git log
명령어로 깃 이력을 터미널에서 확인할 수 있다. 커밋 이력을 다시 한번 확인하고 해야 할 작업 과정을 복기했다.
해당 커밋으로 돌아가 보자.
git reset --hard { commit id }
명령어로 HEAD 포인터가 해당 커밋 이력을 가리키도록 했다. 이제 이후 커밋 이력은 로컬 저장소에서 삭제된 셈이다. 이제 원격 저장소에 반영하자.
git push
명령어를 사용해서 원격 저장소에 수정된 커밋 내역을 반영하려고 보니, 업데이트가 rejected 되었다는 문구와 함께 명령어가 실행되지 못했다. 원격 저장소를 pull 받아서 동기화하라는 내용인데, 우리는 반대로 로컬 저장소의 내용을 원격 저장소에 반영해야 하므로, -f(force) 옵션을 추가한 git push -f
명령어를 실행한다.
원격 저장소 main 브랜치에 가서 원하는 방향으로 커밋 내역이 잘 반영되었는지 체크한다. 만약 아니라 해도 걱정할 필요 없다. 이를 대비해 백업용 브랜치를 생성해뒀으니까!
forced push 되었다는 커멘트와 함께, 기존에 발생했던 많은 양의 conflicts가 더 이상 뜨지 않는 것을 확인할 수 있다. 이제 Squash Merge
가 아닌, 기본 Merge
를 수행하면 안전히 dev
→ main
브랜치 merge가 끝난다.
이 글을 읽은 개발자분들은 git reset 명령어가 어떻게 동작하는지 잘 숙지하고 사용해서,
Git History 를 안전하게 관리해보아요!
글을 읽으면서 해당 프로젝트가 궁금해졌다면,
Scents Note APP 을 다운로드하거나, Scents Note Github 를 방문해 보세요.