100명 가까이 참여하는 프로젝트를 하다 보면, 작은 사이드 프로젝트와는 다르게 역할이 참 많다.
역할에 따라 번역을 할 때도 있고, 개발을 할 때도 있는 등 여러가지 일을 해 볼수록 재미가 더해지는데, Git Conflict를 한번 겪어보면 머리가 뜨거워짐과 함께 성장통이 반드시 오게 마련이다. 해결하면 재미가 2배지만, 포기하고 최후의 방법인 다시 fork뜨기, repository 삭제 등을 하다보면 git이라는 이름만 들어도 러다이트 운동을 일으키고 싶을 만큼의 분노가 발생한다.
언제 Conflict가 발생할까?
나는 3주동안 다음과 같은 팀에 참여했다.
1주차 : 번역 및 검수
2주차 : 번역 및 검수, 개발 1팀
3주차 : 번역 및 검수, 개발 2팀, 개발 3팀
번역 및 검수팀과 개발팀은 서로 역할이 다른데, 먼저 PR 프로세스와 Git 전략을 보고 가자.
PR Process
PR Process는 어렵지 않았다. 사실 어렵게 하면 안 되는게, 비개발자들도 많은 참여를 하고 있는 소스라 최대한 단순하게 가는 것이 좋다. 한 파일의 사이클은 아래와 같이 돌린다.
1주 : 개발팀 개발 -> Peer Review 1 / 2 -> Merge
2주 : 번역검수팀 Review 1 / 2 -> 최종 Merge
Git 전략
Git을 모르는 사람들이 많았어서, 깃 전략 또한 Git Flow 또는 Github Flow같은 정형화된 프로세스를 가져가지 않고, trunk-based보다는 "Fork and Pull"(사실 나도 이름모름) 전략을 따른다.
PC github github organization
local -----> origin ------ > upstream
main main main
Fork and Pull(또는 Fork-based Workflow) 전략은 간단하다. Upstream(원본)에서 Fork를 해서 각 기여자가 본인의 Repository에 Origin을 구성한 후, local에서 작업해서 origin에 push을 하고, origin에서 upstream으로 PR(Pull Request)을 날려서 관리자가 Merge를 하는 방식이다. 이 방법은 단순하면서도 git 초보자가 하기에 정말 좋은 전략이다.
스멀스멀 올라오는 Conflict의 기운
Git 전략은 좋았지만, git branch에 대해서는 따로 정해진 것이 없었다. 깃 초보자가 있는 프로젝트에서 " 'feature branch'를 이용해보세요 ^^ checkout만 해주면 된답니다~" 하면 경험상 무조건 '파일이 날라갔는데요? 제 파일 어디갔죠?'라는 답변이 오기 마련이다. 나 또한 같이 해본 프로젝트가 5인 미만 프로젝트만 있거나, 기능을 혼자 가져가거나 했기 때문에 local에서는 따로 브랜치를 쓸 필요가 없어서 main 이외의 브랜치를 가져간다는 생각조차 하지 못했다.
문제는 여기서 발생했다.
보통 두가지 경우에서 문제가 일어나는데, 다음과 같다.
검수팀
검수팀의 경우, 어떤 부분에서 문제가 발생했는지 쉽게 알려주기 위해 다음과 같은 프로세스를 따른다.
local -----> origin -----> upstream
main push main
작업 작업물
Upstream까지 가지 않고, Origin에 본인의 작업물을 push하고, upstream에는 issue를 발행하며 본인의 작업물을 그대로 올려서 개발자들이 어떤 부분이 바뀌어야 하는지 쉽게 알려준다. 그러면 개발팀은 2주차에 본인의 작업물을 PR해서 반영하게 된다. 그러면 local, origin, upstream은 다음과 같이 구성된다.
사람 | local | origin | upstream |
개발 | A' | A' | |
검수 | A'' | A'' | A'' |
이 때, 검수는 origin을 upstream과 맞추기 위해 sync fork를 하는데, 여기서 origin 저장소는 A'를 받아들여야 하는지, A''를 받아들여야 하는지 헷갈려서 conflict를 내거나 discard A'(A'의 커밋을 버리고 A''로 동기화하기)를 지시한다.
잔디에 관심이 없거나 커밋에 쿨한 상남자 바이킹들은 '어 커밋 버려~' 할 수 있겠지만, 깃헙 잔디 수호자나 내 '귀엽고소중하고깜찍한' 커밋이 버려지는 것을 싫어하는 사람들은 쉽게 discard 할 수 없다. 이때부터 '내 커밋을 보호하면서 upstream을 받아오는 일'에 대해 검색하거나, 내 리포지토리를 세련된 닥스훈트로 만드려 수술을 집도하지만 merge 과정에서 수없이 많은 resolve conflict를 하다 케르베로스가 되고 눈물을 흘리며 저장소를 지우고 새로 fork를 따게 된다.
개발팀
개발팀의 경우, 문제가 조금 더 복잡해진다. 위에서 설명했듯, 한 주마다 신규 개발건을 맡기 때문에 타임라인이 다음과 같아진다.
1주차 : 1주차 내용 개발 -> peer review 1 / 2 -> PR Merge
2주차 : 2주차 내용 개발 -> peer review 1 / 2 -> PR Merge
+ 1주차 내용 검수 -> 번역검수팀 review 1 / 2 -> PR Merge
1주차는 문제가 없지만, 2주차에 돌입하면 2주차의 내용을 개발함과 동시에 1주차의 검수 교정까지 들어가야 한다. 그러면 local, origin, upstream은 다음과 같이 구성된다.
주차 | local | origin | upstream |
1주차 | A | A | A |
2주차 | A' B |
A' B |
??? |
의도(개발자) : "나는 A' 따로, B 따로 PR 날려야지"
현실(github) : "제가 최신꺼 다 같은 PR에 묶었어요 ㅎㅎ 잘했죠?"
이때부터 github 공포증이 돌면서 러다이트운동이 마려워지는 일이 생긴다. 내가 원하는 커밋만 보내고 싶은데, 보낼줄 모르기 때문이다.
어떻게 해결해야 할까?
사실 본인의 의도에 맞춰서 branch를 따로 따면 된다.
주의 : 초보자라면 항상 파일 백업을 따 두고 시작하자.
브랜치 생성
검수팀이라면, proofread branch를 따로 따서
1주차
local -----> origin -----> upstream
main A A A
proofread A' A'
개발팀의 검수 이후 최종 PR 결과(A'')을 local에 sync
2주차
local -----> origin -----> upstream
main A'' A'' A''
proof(1주) A' A' A'
proof(2주) A''+B' A''+B'
이렇게 관리하면 절대 conflict가 나지 않는다.
본인의 repository(origin)에 가서, branch를 보면 'view all branches'가 보인다.
여기 브랜치 관리에서 'New branch'를 누르게 되면
upstream / origin의 어떤 브랜치를 source root로 브랜치를 딸 지 결정할 수 있다.
위의 예시를 기반으로 위를 선택하면 A''가, 아래를 선택하면 A' 소스부터 시작하는 branch가 새로 파진다.
그러면 local에서는 해당 소스를 받아와서 새로 작업하면 된다.
git fetch origin # 내 origin의 정보를 가져오고 싶어.
git checkout -b proofread origin/tutorial # 내 local에 proofread라는 branch를 만드는데, 내 github 저장소의 tutorial 브랜치에 있는 내용을 가져오고 싶어. 그리고 branch를 proofread 브랜치로 바꿔줘.
원래 브랜치에서 개발하고 있는 내용이 있다면, 잠깐 stash라는 무적의 명령어를 사용해서 각 브랜치에 임시보관할 수 있다. 물론 개발하고 있는 내용을 현재 branch에 커밋하는게 가장 안전하지만, 혹시나 secret key같은 것이 같이 커밋되면 안되는 경우에는 stash를 사용한다.
git stash
git fetch origin
git checkout -b proofread origin/tutorial
# 작업 시작
git add .
git commit -m "작업 다함 ㅋㅋ 개꿀"
git push origin/proofread
# 작업 끝
git checkout main
git stash pop
그리고 github에 가 보면, PR을 날릴 수 있다.
나오지 않는 경우는 직접 눌러서 compare across forks
버튼을 눌러 서로 다른 저장소의 서로 다른 브랜치끼리의 PR이 가능하다. 이렇게 하면 원하는대로 특정 커밋만 보낼 수 있다.
이런 식으로 작업하면 된다. 혹시나 수술 집도가 들어갔을 경우는 아래에서 다루겠다.
이미 수술 집도가 들어간 경우
커밋이 진행됐으면 진행됐을수록, 작업을 중첩으로 했으면 했을수록 더더욱 손을 대기가 어렵다.
conflict가 나지 않으면, github(origin)에서 sync fork를 눌렀을 때 다음과 같은 화면이 뜬다.
이럴때는 무소유 정신으로 Discard 9 commit을 해서 아예 upstream과 똑같이 동기화를 해주거나, Update branch로 최신 upstream 위에 내 commit을 얹어주면 될 일이다. 그러나 문제는 다음과 같다.
이 경우에는 충돌이 나서 Sync fork가 되지 않는다. 그러면 로컬에서 충돌 해결을 한 후 동기화해줘야 하는데, 그 과정이 쉽지 않다.
나는 급하다
지금 동기화가 문제가 아니라 작업을 제출해야 하는 상황이면, 파일을 백업해두고, 브랜치를 위의 방법처럼 새로 파서 파일을 넣어서 먼저 제출한다. 추후 cherry-pick 기능을 이용해 main branch에서 해당 커밋을 가져올 수 있다.
난 너보다 더 급해서 이미 main 브랜치에 커밋을 해버렸다
이럴 때에는 체리픽( cherry-pick
) 기능을 이용해 원하는 커밋을 가지고 올 수 있다.
체리픽의 어원은 체리 피킹에서 나왔는데, 체리 피킹(cherry picking)은 '케이크 위에 얹어져 있는 체리만 집어먹는 행위'라는 뜻으로, 어떤 회사의 제품이나 서비스 중 인기 있는 특정 요소만을 골라 경제적으로 취하려는 소비 현상을 비유하는 용어였다. 깃에서는 이 단어를 가지고 내가 원하는 커밋만 가져올 수 있게끔 지원한다.
내가 원하는 이럴 때에는 다음의 방법대로 할 수 있다.
git log
---
commit 2ef0c22671ab9b72534c2f57966a4e97494f188c (HEAD -> main, origin/main, origin/HEAD)
Merge: 6f97cad 4d92521
Author:
Date: Mon Jan 13 22:26:25 2025 +0900
Merge branch 'LangChain-OpenTutorial:main' into main
commit 4d92521a5378a37225b5336f470f5a09a0561fb5
Merge: 5a125c5 4731410
Author:
Date: Mon Jan 13 06:27:12 2025 +0900
[GitBook] Update: Uploaded README.md file (#332)
commit 47314103c61e9e9f1e92a0d0fa2ef1101601c820
Author:
Date: Mon Jan 13 06:25:40 2025 +0900
[GitBook] Update: Uploaded README.md file
[GitBook] Update: Uploaded README.md file
git log 명령어를 입력하면, 커밋에 대한 해시번호가 나온다. 이 해시번호를 사용해서, 내가 있는 브랜치에서 명령어를 입력하면 된다.
git cherry-pick 2ef0c22671ab9b72534c2f57966a4e97494f188c 4d92521a5378a37225b5336f470f5a09a0561fb5
git add . # 또는 git add 파일명
git cherry-pick --continue
git commit -m "원하는 것만 가지고 오기"
git push origin/proofread
만약, cherry-pick 중 문제가 터졌다면 충돌을 해결하고 다시 add 후 --continue로 마무리하면 된다.
답이 나오지 않는다면
git cherry-pick --abort
로 체리픽을 중단하고 원래 상태로 돌아가면 된다.
Sync 맞추기
이제, 남은 Sync를 맞추는 일만 남았다. 도식화하면 아래의 작업을 할 것이다.
1. fetch
↙─────────────────────────────────┐
local -------> origin -------> upstream
2. push
resolve conflict
local에서 upstream의 작업 과정을 가지고 와서, local에서 충돌을 해결한 뒤에 다시 origin에 변경 사항을 작업해 줄 것이다.
git fetch upstream
git merge upstream/main
# 충돌 해결 : IDE 또는 VIM 등...
git add file # 충돌 해결한 파일 커밋
git commit -m "resolve conflict"
git push origin main
결론
현실이 차가워도 내 깃은 더 차갑다. 위에서 설명한 것은 이상적인 시나리오고, 현재 작업하고 있는 상황에 따라 충돌이 날 확률이 높고, abort나 rebase등을 섞어 사용하면서 해결하다보면 어느새 내 완성품이 아래처럼 보인다.
이걸 설명하고 있는 나도 리포지토리를 수없이 날려봤고, 아직도 깃 명령어도 잘 모른다. branch를 새로 파는것도 2년 전쯤 한번 해보고 말았다. cherry-pick이 있는 것도 알았지만, 왜 쓰는지도 잘 몰랐고 git 전략도 불필요하다고 생각했으나 실제 상황에 직면해보니 너무나 좋은 기능이고 또 잘 알게 되었다. 이런 기회가 아니었다면 아마 더 나중에 까마득할 때 써볼 것 같아 이런 상황이 너무 즐겁고 재미있게 공부하고 있다.
'GIT' 카테고리의 다른 글
TIL - 아무것도 못 본척 해야 돼! "REVERT" (0) | 2023.02.06 |
---|---|
TIL - 협업시 Repository는 FE/BE로 나눈다. (0) | 2023.02.06 |