이번 포스팅에서는 git 과 GitHub 이라는 remote repository 함께 사용하면서 발생할 수 있는 remote branch name conflict 이슈에 대해 알아보고, 이를 해결하는 방법에 대해 살펴보도록 하자.
1. branch name conflict 문제 이해하기
먼저 글 제목에 써있는 remote branch name conflict 이슈가 무엇인지에 대해 알아보도록 하자. 사실 이 이슈의 이름은 공식적으로 정해진 것은 아니고, 필자가 임의로 만든 이슈의 이름이다. 정확히 말하면 local branch 이름을 짓다가 발생하는 문제 중 하나이다.
사내에서든 팀 프로젝트에서든 다른 개발자 분들과 협업을 하기 위해서는 git에서 master 또는 main branch에서 작업을 하는 것이 아닌 나만의 local branch를 따로 생성해서 작업을 하고, PR(Pull Request)을 생성한 뒤 master(또는 main) 브랜치로 merge 되는 과정이 일반적이다. 그런데 여기서 local branch를 따로 생성할 경우, 개발자가 자유롭게 브랜치 명을 명시해서 생성을 할 수가 있다.
먼저 아래처럼 현재 내가 example 이라는 이름으로 로컬 브랜치를 별도로 생성한 후, 수정사항을 커밋했다. 그리고 PR을 아래처럼 생성했다.
그리고 난 뒤, 터미널에서 생성된 로컬 및 원격 브랜치의 모든 리스트를 살펴보면 아래와 같다.
그리고 이제 난 별도로 새로운 기능을 만들기 위해서 또 별도의 브랜치를 생성하려고 하는데, 이 때 브랜치명을 exmaple/test 라고 생성하려고 한다. 하지만 이렇게 하면 아래와 같이 에러가 발생한다.
fatal: cannot lock ref 'refs/heads/example/test': 'refs/heads/example' exists; cannot create 'refs/heads/example/test'
이유는 example 이라는 로컬 브랜치 이름이 있는데, example/test 라는 sub 브랜치명을 생성할 수 없다는 것이다. sub 브랜치명이라고 한다면 슬래쉬(/)를 의미한다. 바로 이러한 경우가 branch name conflict 이슈라고 볼 수 있다. 물론 반대로, example/test 브랜치명을 먼저 생성한 뒤, example 이라는 브랜치명을 생성하려고 해도 동일한 문제가 발생한다.
방금 알아본 경우는 로컬에 존재하는 브랜치명들에 대한 충돌이었다. 이러한 현상은 원격 브랜치에도 똑같이 해당된다. 원격 브랜치에서 동일한 문제를 발생한 상황을 재현하기 위해서, 위에서 생성했던 example 이라는 remote local branch가 생성한 PR을 먼저 closed 하고example 브랜치를 삭제해보겠다.
자, 이제 원격 레포지토리에는 example 이라는 브랜치는 삭제되었다. 그리고 로컬에서도 동일하게 example 이라는 로컬 브랜치를 아래 명령어로 삭제해보도록 하자.
git branch -D example
앗 그런데 갑자기 example 이라는 브랜치에서 작업한 수정 내역을 복구할 필요가 생겼다. 그래서 위에서 closed한 PR에서 삭제한 브랜치를 restore 하고 PR를 reopen 시켜보자.
그러면 이제 로컬 레포지토리와 원격 레포지토리 각각에 존재하는 브랜치 종류 리스트들은 다음과 같다. 참고로 아래처럼 존재하는 브랜치 명이 무엇이 있는지 살펴보기 위해서 git branch 관련 명령어를 사용해도 되지만 아래의 .git 디렉토리로 들어가면 실제로 존재하는 브랜치명 리스트도 직접 눈으로 확인할 수 있다.
- .git/refs/heads : 로컬에서 생성한 브랜치 명
- .git/refs/remotes/origin : 원격에서 생성한 브랜치 명(원격 레포지토리에 대한 별명을 origin 으로 했을 경우)
아래 그림의 localhost 에서의 브랜치 명 리스트는 .git/refs/heads 라는 디렉토리 내의 브랜치 명 리스트이다.
그런데 해당 원격 레포지토리로 작업하고 있는 다른 개발자분께서 추가 기능을 개발하려고 브랜치명을 별도로 따서 작업을 하고 push 하려고 하는 상황이라고 해보자. 브랜치명은 example/dev 라는 브랜치라고 가정해보자. 해당 브랜치명으로 생성하고 원격 레포지토리에 push를 하려니 다음과 같은 에러가 발생했다.
! [remote rejected] example/dev -> example/dev (cannot lock ref 'refs/heads/example/dev': 'refs/heads/example' exists; cannot create 'refs/heads/example/dev') error: failed to push some refs to 'github-young-hun-jo:young-hun-jo/young-hun-jo.git'
메세지를 잘 읽어보면 example 이라는 브랜치가 이미 있기 때문에 example/dev 라는 브랜치를 생성할 수 없다는 것이다. 직전에 로컬에서 git branch -D 명령어로 example 이라는 브랜치를 분명 삭제했는데, 대체 왜이럴까? 이유는 바로 원격 레포지토리에서 example 이라는 브랜치를 restore 했기 때문에 다시 example 이라는 브랜치가 존재하는 상태이기 때문이다.
이러한 문제를 해결하기 위한 방법은 무엇일까? 만약 당장 example/dev 라는 브랜치를 push 해야 하는 것이 가장 1순위라면 원격 레포지토리에 존재하는 example 이라는 브랜치를 삭제해주어야 한다. 다시 reopen 한 PR로가서 closed를 하던 merge를 하던 하고, 해당 브랜치를 삭제한다.
이렇게 한 뒤, exmaple/dev 브랜치명으로 생성했던 내용을 push 하면 PR이 생성된다. 그런데 push 명령어를 날린 로컬에서 또 하나의 에러가 발생했다.
* [new branch] example/dev -> example/dev
error: update_ref failed for ref 'refs/remotes/origin/example/dev': cannot lock ref 'refs/remotes/origin/example/dev': 'refs/remotes/origin/example' exists; cannot create 'refs/remotes/origin/example/dev'
PR은 정상적으로 되었는데, 왜 로컬에서 이런 에러가 발생하는 걸까? 이유는 바로 로컬에서 바라보고 있는 원격 브랜치명의 리스트에는 example 이라는 브랜치가 여전히 존재하기 때문이다. 터미널에서 .git/refs/remotes/origin 디렉토리에 어떤 파일들이 있는지 살펴보자.
살펴보니 example 이라는 브랜치명이 존재하고 해당 파일의 형태가 디렉토리가 아닌 것을 보아하니 example 이라는 브랜치가 로컬에서는 여전히 존재함을 알 수가 있다.(만약 example/dev 라는 브랜치가 있으려면 example의 파일 형식이 디렉토리여야 한다)
즉, 우리는 원격 레포지토리에서 PR을 close 하면서 example 이라는 브랜치를 삭제해주었는데, 이 '삭제'라는 일종의 업데이트 사항을 로컬에도 반영을 해주어야 한다는 것이다. 반영을 해주기 위해서 로컬에서 아래의 명령어를 실행해주도록 하자.
git remote prune origin
prune 명령어를 수행하면 origin/example 브랜치 즉, 원격의 example 이라는 브랜치를 삭제(prune)했음을 알려주고 있다. 이후 다시 로컬에서 바라보고 있는 원격 브랜치 명의 리스트를 살펴보면 example 이 없어져 있는 것을 알 수 있다. 그리고 이제 로컬에서도 origin/example/dev 라는 원격 브랜치를 생성해주어야 한다. 이를 위해서 다시 한번 push 명령어를 수행해준다. "Everything up-to-date" 라고 이미 최신이라고 메세지가 뜨지만, 원격 브랜치명들이 들어있는 디렉토리를 출력해보면 아래처럼 example 디렉토리 내에 dev 라는 파일이 생성되었음을 볼 수 있고, 이는 곧 로컬에서도 origin/example/dev 라는 원격 브랜치를 갖고 있음을 알 수 있다.
이렇게 해서 branch name conflict 문제를 해결할 수 있다. 지금까지 상황을 재현해보면 억지스럽게 이슈가 될만한 상황을 만든 것 같지만 하나의 원격 레포지토리에서 여러 명의 개발자가 작업하다 보면 이런 상황이 충분히 생길 수 있고, 실제로 최근 현업에서 이런 문제를 몇 번 겪었기에 이번 기회에 그 문제를 파악하고 해결책을 기록해두려고 했다.
2. 그래서 언제 발생했는데?
위에서 살펴보았던 branch name conflict 문제를 push 할 때도 문제가 발생하지만, 특정 remote branch 나 tag를 pull 할 때도 문제가 발생한다. 우선 해당 문제는 아래와 같은 상황에서 발생했다. 현업에서 CI/CD를 하기 위해 Github 과 Jenkins를 사용하고 있다.
문제는 Jenkins Server에서 Github Repository로부터 특정 PR을 pull 할 때, branch name conflict 이슈가 발생해 빌드 자체가 불가능한 문제였다. 이 문제가 발생한 핵심적인 이유는 push하면서 내가 수정한 사항을 반영하거나 pull 하면서 누군가 수정한 사항을 반영할 때 .git 디렉토리 내에 있는 파일들까지도 반영되는 것이 아니기 때문이다.
다시 말해, 위 그림에서 로컬 호스트에 있는 .git 디렉토리와 Github Repository 에 있는 .git 디렉토리, Jenkins Server 에 있는 .git 디렉토리는 push, pull 명령어로 자동 업데이트 되지 않는다는 것이다.(물론 1번 목차에서 알아본 것처럼 git remote prune 명령어로 Github Repository에서 삭제된 브랜치들을 localhost 또는 Jenkins Server에서 동기화시킬 수는 있음. 핵심은 push, pull 명령어로 .git 디렉토리 내용이 업데이트 되지 않는 다는 것)
상황은 이러하였다. 1주일 전, 누군가 feature/dev 브랜치명을 생성해서 작업을 했고, PR을 생성했다. 그래서 localhost, GitHub Repository에는 origin/feature/dev 브랜치명이 존재했다. 그리고 해당 PR로 CI/CD를 진행했기 때문에 Jenkins Server에서도 origin/feature/dev 브랜치가 존재했다. 그런데, 해당 PR이 main branch로 merge가 될 만한 이유가 존재하지 않아서 PR을 close 했고 해당 브랜치도 삭제를 했다. 또 localhost에서도 해당 브랜치를 삭제했다. 이 상황에서 서버 별 갖고 있는 원격 브랜치 명 상태는 아래와 같다.
위 상황에서 일정 시간이 지나고 다른 개발자가 추가 기능 개발을 하려고 하는데, 이 때 브랜치명을 feature/dev/login 으로 명시했고 이 브랜치명으로 PR을 생성 하고 CI/CD를 진행했다고 해보자. 어디 단계에서 문제가 발생할까?
CI/CD를 하기 위해서 새로운 기능이 담겨있는 PR 즉, feature/dev/login 브랜치를 pull 해야하는데, 이 과정에서 바로 에러가 발생하는 것이다. 이유는 이쯤이면 추측이 가능할 것이다. Jenkins Server에서는 여전히 origin/feature/dev 라는 원격 브랜치명을 갖고 있는데, origin/feature/dev/login 이라는 브랜치명을 생성하려고 하니 conflict 가 발생하는 것이다.
이를 해결하기 위해서는 2가지 방법이 있다. 첫 번째는 수동으로 rm 과 같은 명령어를 사용해서 .git/refs/remotes/origin/feature/dev 라는 파일을 삭제하는 것이고, 두 번째는 git remote prune 명령어로 Github Repository의 삭제된 브랜치 상태를 업데이트하는 것이다.
실제로 위와 같은 해결책으로 현업해서 DevOps팀 동료 분들과 해결하였다.
이렇게 해서 git에서 마주칠만한 branch name conflict 라는 문제에 대해 원인을 파악하고 해결책을 알아보았다. 이 이슈를 파악하는 과정에서 브랜치 명들이 실제 어디 디렉토리에 저장되는지 이해할 수 있는 배움을 얻었고, DevOps팀이 관리해주시는 CI/CD 환경에 대한 구조도 보다 자세하게 파악할 수 있었다.
'Git' 카테고리의 다른 글
[Git] 버전 관리 시스템, git (0) | 2022.12.31 |
---|---|
[Git] confilct 와 결부되는 cherry-pick, rebase, revert 알아보기 (0) | 2022.06.06 |
[Git] git의 stash 기능 알아보기 (0) | 2021.08.28 |
[Git] 무시무시한 conflict를 해결해보자 (0) | 2021.08.28 |
[Git] Merge 할 때, Fast-forward 방식은 무엇인가? (2) | 2021.08.27 |