git reset으로 merge conflicts 해결하기

JOY·2023년 2월 16일
1

만약 git을 경험해 본 적이 있다면, 다음 질문에 대답해 보자. git reset 명령어를 잘못 사용해서 작업 중인 코드가 날아가는 경험을 해본 적이 있는가? 혹은 협업하는 레포지토리의 커밋 이력이 꼬이는 경험을 해본 적이 있는가? 여기에 두 가지 경험을 모두 겪어 본 사람이 있다.

오랜만에 git reset 명령어를 사용하면 좋을만한 상황에 직면했다. 사이드 프로젝트를 진행하던 중, github에서 dev 브랜치를 main 브랜치로 merge 하는 과정에서 conflicts가 많이 발생하는 이슈를 해결하기 위해서였다.

해당 프로젝트에서는 devmainprd 브랜치로 커밋을 옮겨가는 브랜치 전략을 사용하고 있다. 이와 관련해서, squash and merge를 허용하지 않고 Create a merge commit을 권장하는 merge 전략을 가져가고 있다.

github merge 종류에는 총 3가지가 있다.

  • merge : 브랜치 commit들이 base 브랜치에 병합되고, 하나의 merge commit이 base 브랜치에 추가된다.
  • squash and merge : 브랜치 commit들이 하나의 새로운 commit으로 묶여서 base 브랜치에 병합된다.
  • rebase and merge : 브랜치 commit들이 그대로 병합되는 것이 아니라, 새로운 commit들이 생성되어 base 브랜치에 추가된다. (기존 브랜치 commit과 commit id가 다르다.)

더 자세하게 알고 싶다면 github merge에 대해 잘 정리된 공식 문서를 보면서 공부해 보자.

이전에 devmain 브랜치 merge 하는 과정에서 squash merge를 했었던 바람에, squash merge로 생겨난 커밋 이후로 devmain 브랜치 사이에 commit 이력들이 달라지게 된 것이다. 그래서 Pull Request를 시도했더니 conflicts가 발생한 것이다.

Resolve conflicts 버튼을 클릭해서 conflicting files를 히나 하나 확인하고 conflicts를 직접 고치는 방법도 있지만, 브랜치 전략을 유지하고 문제를 근본적으로 해결하기 위해서는 git reset 명령어를 사용하여 squash merge를 한 커밋 이전으로 되돌아가야 한다는 판단을 내렸다.

그동안 git reset 명령어로 피해를 봤었던 이유는, 당연하게도 명령어의 동작 원리를 제대로 이해하지 못하고 사용해서였다. 그래서 이번에는 git reset 명령어에 대해 충분히 이해하는 시간을 가졌다.

reset 명령은 아래와 같은 순서대로 동작한다.

  1. HEAD가 가리키는 브랜치를 옮긴다. (--soft 옵션이 붙으면 여기까지)
  2. Index를 HEAD가 가리키는 상태로 만든다. (--mixed 옵션이 붙거나, 옵션을 붙이지 않으면 여기까지)
  3. 워킹 디렉터리를 Index의 상태로 만든다. (--hard 옵션이 붙으면 여기까지)

더 자세하게 알고 싶다면 github reset 명령어에 대해 잘 정리된 Pro Git book을 보면서 공부해 보자.

Merge Conflicts 해결 과정

이번 이슈를 해결하기 위해, 내가 세운 작업 계획은 이렇다.

  1. main 브랜치에 대한 백업 브랜치 생성
  2. git reset --hard { commit id }
  3. devmain 브랜치 기본 merge 진행

1. main 브랜치에 대한 백업 브랜치 생성

main 브랜치에서 혹시나 실수할 경우 브랜치를 복구할 수 있도록 백업 브랜치를 생성해 준다.

git pull origin main 명령어로 main 브랜치를 최신화한 후,
git checkout -b 명령어를 사용해서 backup/main-after-v0.0.7라는 이름의 백업 브랜치를 생성했다.

그런 다음 해당 브랜치를 git push origin {branch_name} 명령어를 통해 원격 저장소에 반영했다.

2. git reset —hard { commit id }

이제 본격적으로 과거 커밋 이력으로 돌아가는 작업을 수행한다. 아래 이미지는 백업용 브랜치지만, 해당 시점에는 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 브랜치에 가서 원하는 방향으로 커밋 내역이 잘 반영되었는지 체크한다. 만약 아니라 해도 걱정할 필요 없다. 이를 대비해 백업용 브랜치를 생성해뒀으니까!

3. dev → main 브랜치 기본 merge 진행

forced push 되었다는 커멘트와 함께, 기존에 발생했던 많은 양의 conflicts가 더 이상 뜨지 않는 것을 확인할 수 있다. 이제 Squash Merge가 아닌, 기본 Merge를 수행하면 안전히 devmain 브랜치 merge가 끝난다.

이 글을 마치며,

이 글을 읽은 개발자분들은 git reset 명령어가 어떻게 동작하는지 잘 숙지하고 사용해서,
Git History 를 안전하게 관리해보아요!

글을 읽으면서 해당 프로젝트가 궁금해졌다면,
Scents Note APP 을 다운로드하거나, Scents Note Github 를 방문해 보세요.

profile
Backend Developer

0개의 댓글