이륙하라

git tip: 두 저장소 중 하나를 버리지 않고도 프로젝트 병합하기 본문

dev

git tip: 두 저장소 중 하나를 버리지 않고도 프로젝트 병합하기

zzJinux 2019. 5. 19. 17:52

이따금씩 학교 과제 중엔 개인별 프로젝트로 주어지다가 팀 프로젝트로 바뀌는 것이 있다.
어떻게 프로젝트를 어떻게 병합하여 서로가 협업할 것인가는 둘째치고 git으로 프로젝트를 관리할 것이라면 해결해야할 사소한 문제가 하나 생긴다.
저장소는 어떻게 관리하지?
이건 이전에 git을 개인별 프로젝트에 썼건 안썼건 발생하는 문제다.

지금까지 변변한 팀프로젝트가 없었기도 해서 별 신경을 안 썼는데 문득 깔끔한 해결방법에 대해 탐구해봤다.

여러가지 해결책들

로컬 저장소 딱 하나만 두기(최악)

팀원들이 접근할 수 있는 어떤 서버에 저장소 하나 만들고 그곳에 커밋을 같이 만드는 것이다. 굳이 더 설명하지 않겠다...

각자의 로컬 저장소를 두고 리모트 저장소 하나를 만든다. (github 기준)

오픈소스 프로젝트가 쓰는 패턴이다. 기준이 되는 리모트 저장소 한 개가 있고 여러 개의 로컬 저장소에서 커밋을 남긴다. 병합만 적절히 처리하면 여러 사람이 커밋해도 전혀 문제 없다.
근데 이 패턴의 특징은 저장소 소유자가 특정 한 명의 개인/단체로 한정된다는 것이다. 해결하려는 문제 상황의 경우에 어떻게 적용할까?

방법 1-1: 팀원 한 명의 개인 저장소를 쓴다. (팀원들이 fork를 뜨거나 팀원들을 collaborator로 추가한다)

단점들:

  • 개인 저장소라 뭔가 그 저장소의 소유자가 프로젝트의 유일한 소유자같이 보인다
  • 다른 팀원들의 github의 프로필 페이지에 안보인다.

명목상 소유자가 단 한 명으로 고정되는 것부터 일단 찝찝하다.

방법 1-2: 팀원 모두가 소유자인 단체(organization)을 하나 판다음 그 아래에 저장소를 하나 만든다.

이것이 가장 괜찮은 대안인 듯한데 단체 기능은 학교에서 내주는 (그 중에서도 수업 내용을 복습하기 위한 용도의) 프로젝트들로 쓰기엔 과한 기능인 듯 보인다.

방법 2: 각자의 로컬 저장소를 두고 리모트 저장소도 각자 둔다.

내가 도달한 결론은 이것이었다. 커밋 이력을 서로 공유/동기화할 수 있다면 리모트 저장소도 각자 따로 못 둘 이유도 없다.
이 판단의 근거는 서로 다른 저장소의 두 커밋을 가지고 병합 커밋을 만들 수 있다는 것이다. 말로 설명하면 복잡하니 바로 예시로 넘어가자.

적용 예시 (Proof of Concept)

이 방법을 보이기 위해 굳이 github에 더미 저장소까지 만들 필요 없다. 바로 지금 로컬 아무 곳에나 저장소를 만들고 더미 커밋도 각각 만들자. 이 때 커밋에는 의도적으로 merge conflict를 낼 파일과 그렇지 않은 파일을 만들어 놓자.

## working directory: /somewhere/repo1
## repo1
$ git init
$ echo "no conflict" > no-conflict1.txt
$ echo "I'm from repo1." > conflict.txt#
$ git add .
$ git commit -m"commit from repo1"
# commit hash "A"

## working directory: /somewhere/repo2
## repo2
$ git init
$ echo "no conflict" > no-conflict2.txt
$ echo "I'm from repo2." > conflict.txt
$ git add .
$ git commit -m"commit from repo2"
# commit hash "B"

repo1과 repo2는 각각 팀원의 리모트 저장소를 표상한다.

이제 각 저장소에 상대의 저장소의 위치에 이름표를 붙이자. 실제 용례에서는 리모트 저장소 주소의 위치에 git@...https://... 같은 형식이 올 것이다. 이 예시에서는 로컬 저장소의 주소로 대신한다.

## working directory: /somewhere/repo1
## repo1
$ git remote add repo2 /somewhere/repo2

## working directory: /somewhere/repo2
## repo2
$ git remote add repo1 /somewhere/repo1

이제 병합을 해야 한다. 병합은 둘 중 하나의 저장소에서 수행돌 수 밖에 없다. repo1에서 수행함을 가정하자.

## working directory: /somewhere/repo1
## repo1
$ git pull --allow-unrelated-histories repo2 master
## ... or equivalently
$ git fetch repo2 master
$ git merge --allow-unrelated-histories repo2/master

## ... merge conflict due to "conflict.txt"
## TODO: resolve conflict and commit.

생소한 옵션이 보인다. 통상 병합 연산은 병합하려는 커밋들의 공통 조상 커밋을 찾는 것부터 시작한다.
그런데 이 경우에는 공통 조상이 존재 하지 않는다. 이 때 병합을 하려면 --allow-unrelated-histories 옵션을 줘야 한다.
git fetch repo2 master 만 한 다음 커밋 그래프 출력을 해보자. 서로 분리된 커밋 트리 2개가 출력된다.

그 외에는 별 특별할만한 것이 없다. 통상적인 병합 절차와 똑같다.

(추가 팁)
일부러 충돌이 나라고 만든 conflict.txt 파일에서 충돌이 났고 no-conflict1.txt, no-conflict2.txt는 모두 인덱스에 반영이 됐다. 그런데 no-conflict1.txt만 받아들이고 싶거나 더 나아가 repo2 측의 working tree를 완전히 무시하고 싶으면 어떻게 할까?
merge에 알맞는 옵션이 있다.

# working directory: /somewhere/repo1
# repo1
$ git pull --allow-unrelated-histories -s ours repo2 master
# ... or equivalently
$ git fetch repo2 master
$ git merge --allow-unrelated-histories -s ours repo2/master

# ... no conflict!!

충돌을 해결하여 병합을 끝마쳤다.
병합이 끝난 끝난 뒤에 repo1 커밋 트리를 출력해보면 대략 이렇게 나올 것이다.

C        <-- master
|\
| B      <-- repo2/master
A 

repo1에서 병합 커밋을 만들었으니 repo2에선 그 커밋을 불러올 차례다.
특별할 것 없다. 그냥 pull하면 된다.
github에 원격 저장소가 따로 있는 경우에는 repo1에서 push를 한 상태여야 한다.

## working directory: /somewhere/repo2
## repo2
$ git pull repo2 master

커밋 트리를 출력해보면...

C        <-- master, repo1/master
|\
| B
A 

끝으로 repo1에서 fetch를 하여 repo2/master를 최신 상태로 옮겨주자.

이제 우린 두 개의 원격 저장소를 가진 프로젝트를 구성할 수 있게 됐다. :)
상대방의 결과물을 pull하고, 내가 수정한 결과물은 내 원격 저장소에 push하는 매우 익숙한 방법을 사용하여 손쉽게 동기화를 할 수 있다!

Comments