💡 Intro
지금까지 다른 사람과 협업을 해본 적이 없다. 개인 프로젝트만 진행했었고, git의 리포지터리 역시 혼자만 사용했었다. 그러니 git은 단지 코드를 올려놓는 곳으로만 사용했고, 명령어도 push와 pull만 사용했다.
이번에 우테코에서 프로젝트를 하면서 한 리포지터리를 팀이 같이 사용하게 되었고, git의 히스토리를 관리하는 것이 중요해졌다. fetch가 무엇인지, 언제 사용하는지 그리고 fetch와 pull의 차이는 무엇인가에 대해 알아보자.
❓Git이 커밋을 구성하고 저장하는 방식
일단 git이 커밋을 어떻게 구성하고 있고 저장하는지에 대해 간단하게 알아보자. 어떻게 보면 꼭 알아야 하는 내용은 아닐 수 있다. 그런데 나는 찾아보다보니 이것도 재밌어서 알아보게 되었다.
- Git
git은 리포지터리에./.git/objects
디렉터리에 모든 커밋을 로컬 및 원격으로 저장한다.
- 로컬브랜치
git은 브랜치 참조를 통해 원격 및 로컬 브랜치 커밋을 명확하게 분리한다.
로컬브랜치 참조는./.git/refs/heads
디렉터리 아래 저장된다.
현재 프로젝트에 있는 모든 로컬브랜치가 여기에 저장되어 있다. git branch
명령어는 로컬 브랜치 참조목록을 보여준다.
- 원격브랜치
원격브랜치는 로컬브랜치와 혼동되지 않도록 원격 리포지터리에 의해 미리 고정되어 있다.
git은 원격브랜치에 대한 참조가 있다.git branch -r
명령어를 통해 원격 브랜치 참조 목록을 확인할 수 있다.
원격 브랜치는 일반적으로origin/
이라는 접두사가 붙어있다.
1️⃣ git fetch
git fetch
명령어는 원격 저장소의 최신 변경 사항을 가져오지만, 현재 작업 중인 브랜치에는 변경 사항을 자동으로 병합하지 않는다.
원격 저장소의 최신 커밋들을 FETCH_HEAD 이름의 브랜치로 내려받지만 병합을 하지 않는 것이다.
가져온 원격 저장소의 최신 변경 사항을 병합하지 않기 때문에 병합하기 전 어떤 내용이 변경되었는지 먼저 확인할 수 있고 이를 통해 로컬브랜치와 원격저장소의 상태를 비교하고 충돌을 예방할 수 있다.
FETCH_HEAD 파일이란?
git에서 fetch를 할 때마다 업데이트 되는 .git 디렉터리 하위에 위치한 파일이다. 이 파일에는 원격 저장소가 병합할 때마다 생기는 최신 커밋의 정보가 들어있다.
git fetch 명령 및 옵션
git fetch <remote>
리포지터리에서 모든 브랜치를 가져온다.git fetch <remote> <branch>
지정된 브랜치만 가져온다.git fetch -p origin
/git fetch --prune origin
p 옵션은 git에서 Prune 옵션을 사용하여 로컬 저장소에 없는 원격 브랜치와 관련된 로컬 참조를 제거한다.
만약 원격 저장소에서 A브랜치가 삭제되었다면, A브랜치에 대한 로컬 참조도 삭제된다.
이 옵션을 통해 로컬 저장소의 상태를 정리하고 필요하지 않는 브랜치의 참조를 제거할 수 있다.
2️⃣ git pull
원격 저장소의 최신 변경 사항을 가져오면서 동시에 현재 작업 중인 브랜치에 해당 변경 사항을 자동으로 병합(merge)한다.
자동으로 병합을 진행하기 때문에 원격 저장소의 변경사항에 대한 병합 커밋을 만들며 이 과정에서 충돌이 발생할 수 있다.
내부적으로 fetch + merge 방식으로 동작한다.
git pull --help
를 통해 Description을 확인해보면 다음과 같은 내용이 있다.
... In its default mode, git pull is shorthand for git fetch followed by git merge FETCH_HEAD.
기본모드(default)인 경우 git pull
은 git merge FETCH_HEAD
와 같이 동작하고 머지 커밋을 생성한다.
pull의 옵션을 지정해줄 수도 있다.
- default
git config pull.rebase false
git merge FETCH_HEAD
와 같이 동작하여 머지 커밋을 생성한다.
현재 작업하고 있는 브랜치에 머지 된다.
- --ff only
git config pull.ff only
머지 대상의 브랜치와 fast-forward 관계인 경우 머지 커밋은 만들어지지 않고 브랜치의 참조만 변경된다.(fast-forwarded 가 새로운 커밋이 발생하지 않고 실행된다.)
fast-forward 의 관계가 아닌 경우 머지 커밋이 생성된다.
fast-forward 의 관계는 분기한 브랜치의 커밋 히스토리가 기존 브랜치의 커밋 히스토리를 포함하고 있는가 이다.
(직접그린 그림 추가하기)
- --rebase
git config pull.rebase true
현재 로컬 작업 브랜치에 쌓은 커밋들의 base를 원격 저장소의 최신 커밋 다음으로 옮긴다.
잘못된 rebase 사용 예시)
로컬 작업 브랜치 feat/#30 에서 작업을 하고 있다고 가정하자.
작업을 마친 뒤 원격 저장소에 새로운 브랜치(feat/#30)를 생성해서 push하고 merge(remote/dev 브랜치에)를 하려는데 원격 저장소에서 내가 작업한 파일과 같은 파일에 변경사항이 있는 경우 merge가 되지 않는다.
이 경우 원격 저장소의 변경사항을 pull 받아야 한다. 만약 git pull 옵션 중 rebase가 true로 설정되어 있다면 pull을 받을 때 원격 저장소의 최신 커밋 위에 로컬의 feat/#30에서 작업한 커밋들이 새로 쌓이게 된다.
이렇게 쌓인 커밋들은 새로운 커밋으로 보기 때문에 원격 저장소의 feat/#30 브랜치에 올라간 커밋들과 해시코드가 달라지고 이후에 feat/#30에는 push되지 않는다.
따라서 이 경우 force push를 진행하거나 브랜치를 삭제하고 다시 생성해서 push해야 한다.
(직접 그린 그림 추가하기)
3️⃣ 경험한 문제 상황(시나리오)
1. origin의 dev로부터 크롱이 feat/#30 브랜치를, 뽀또가 feat/#34 브랜치를 파서 작업중이다. 이 때 크롱과 뽀또의 변경사항에 같은 파일이 있다.
2. 뽀또가 작업을 크롱보다 빨리 마무리해서 origin의 feat/#34 브랜치로 push하고 dev에 PR을 날린다.
3. 리뷰가 마무리되어 dev에 feat/#34 브랜치가 머지된다.
4. 크롱은 feat/#30 기능을 구현을 마무리하고 push를 하려고 한다. push 하기 전 뽀또가 올린 변경사항을 feat/#30 브랜치에 반영하기 위해 origin의 dev를 pull 받는다.
5. 왜인지 모르게 pull이 받아지지 않았고(이 경우는 더 생각해봐야한다.) 크롱은 아는 척을 하기 위해 git fetch origin
을 냅다 해버렸다. 그리고 나니 더더욱 pull이 안되는 상태다. 해결해보자.
6. 현재 작업하고 있는 브랜치에서 git merge FETCH\_HEAD
명령어를 통해 원격 저장소에서 받아온 변경이력을 머지한다.
이 때 뽀또와 크롱은 같은 파일에서 작업했기 때문에 머지하는 과정에서 충돌이 발생한다.
7. 그래서 머지하기 전 충돌을 해결해야 한다.
git status
명령어를 통해 현재 어떤 파일이 충돌이 났는지 확인할 수 있다.
8. 충돌이 난 파일들을 해결하고 커밋한다.
git add .
git commit -m “커밋 메시지”
커밋이 잘 되면 자동으로 머지된다.
git fetch origin 을 하게 되면 원격 저장소의 변경사항이 로컬의 FETCH_HEAD브랜치로 받아와진다. 하지만 그러한 내용이 터미널에는 나타나지 않았고 fetch에 대한 지식이 없어서 때문에 무엇이 문제인지 알지 못했다.
아마 pull이 받아지지 않은 이유는 git pull 은 fetch + merge인데, 이미 git fetch origin 명령어로 fetch 해왔기 때문이 아닐까싶다. (뇌피셜이기 때문에 틀렸다면 지적해주세요!)
❗️ 정리
사실 git에 대해 깊이 공부해본 적이 없고 단순히 블로그에 있는 명령어를 따라치면서 그 때 발생했던 문제를 해결하면 끝! 이었다.
이번에 처음으로 왜 지금의 문제가 발생했는지, 발생한 이유는 무엇인지 차분하게 생각해봤고, 제시된 해결방법이 왜 그리고 어떻게 해결할 수 있는 것인지 곰곰히 생각해봤다.
덕분에 git의 흐름을 생각하는데 조금 익숙해졌고 논리적인 사고가 가능해진 것 같다. git에 대해 모르는 만큼 git문제에 맞딱뜨리면 겁부터 났는데 조금씩 알아가고 해결하는 재미가 쏠쏠한 것 같다.
참고
[1] git fetch | Atlassian Git Tutorial
[2] Git pull 전략