노트

여러 저장소 합치기

Merging Git Repositories

인프라#git · 연결된 개념 2개

쉽게 말하면

여러 저장소 합치기는 따로 쓰던 두 집안 족보를 한 권으로 묶되, 각자의 옛 기록을 버리지 않는 거예요. 통째로 이어 붙이거나, 한쪽을 다른 쪽의 한 장(하위 폴더)으로 넣을 수 있어요.

비유가 깨지는 곳 반대로 폴더 하나를 떼어 낼 땐 족보를 새로 베껴 쓰듯 히스토리를 다시 써요. git filter-repo는 새로 클론한 저장소에서만 돌고, 끝나면 origin을 지워 원본에 잘못 푸시하지 않게 해요.

따로 관리하던 Git 저장소를 하나로 합칠 때, 히스토리를 버리지 않고 옮기는 방법이 두 가지 있다. 저장소 루트끼리 그대로 합치거나(unrelated histories 머지), 한쪽을 다른 쪽의 하위 폴더로 넣는다(subtree). 모노레포로 옮길 때 자주 쓴다.

루트끼리 합치기

Git은 공통 조상이 없는 두 히스토리의 머지를 기본으로 거부하므로 옵션을 준다.

cd project-dest
git remote add src ../project-src
git fetch src --tags
git merge --allow-unrelated-histories src/main
git remote remove src

같은 경로의 파일이 겹치면 충돌이 나므로, 먼저 원본 쪽에서 파일을 하위 폴더로 옮겨 두면 편하다.

하위 폴더로 넣기

git subtree add --prefix=packages/api <원본 저장소 URL> main

--prefix 아래로 원본 히스토리가 들어온다. 이후 git subtree pull로 원본의 갱신을 계속 받을 수도 있다.

히스토리를 유지한 폴더 이동

mkdir -p packages/api
git mv src package.json packages/api/
git commit -m "api 코드를 packages/api로 이동"

Git은 이동을 따로 기록하지 않고 내용 유사도로 추적하므로, 이동과 수정을 한 커밋에 섞지 않는 편이 git log --follow에 유리하다(원자적 커밋).

반대로: 폴더 하나를 새 저장소로 떼어 내기

모노레포의 한 폴더를 히스토리째 별도 저장소로 옮길 때는 히스토리를 다시 쓴다. 그 폴더를 건드린 커밋만 남기고, 폴더 내용이 저장소 루트가 되도록 각 커밋의 트리를 고친다.

git clone --no-local --single-branch --branch main ../monorepo /tmp/api-export
cd /tmp/api-export
git filter-repo --subdirectory-filter packages/api   # packages/api/src → src
git remote add origin <새 저장소 URL>
git push -u origin HEAD:main
  • git filter-repo는 Git에 기본으로 들어 있지 않아 따로 설치한다. 공식 문서도 권하는, git filter-branch의 대체 도구다
  • 새로 클론한 저장소에서만 돈다. 로컬에만 있는 작업을 실수로 덮어쓰지 않으려는 안전장치이고, 로컬 경로를 클론할 때는 --no-local을 줘야 원본과 객체를 공유하지 않는 진짜 새 클론이 된다
  • 실행이 끝나면 origin 리모트를 지운다. 다시 쓴 히스토리를 원본에 잘못 푸시하지 않게 하려는 것이니, 새 저장소 주소를 직접 추가한다
  • 커밋된 것만 옮겨 간다. gitignore된 빌드 산출물이나 생성 파일은 따라오지 않는다

Git에 들어 있는 git subtree split --prefix=packages/api -b api-only도 같은 결과(그 폴더를 루트로 한 새 히스토리)를 만든다. 설치가 필요 없지만 셸 스크립트로 커밋을 하나씩 처리해서, 커밋이 수만 개인 저장소에서는 매우 느릴 수 있다. 큰 저장소라면 filter-repo가 낫다.

출처: git-merge --allow-unrelated-histories · git-filter-repo 문서 · git-subtree 문서

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 머지와 리베이스

    두 브랜치의 작업을 합치는 두 방법이다. 머지는 히스토리를 그대로 두고 합치는 커밋을 하나 더 만들고, 리베이스는 내 커밋들을 상대 브랜치 끝 위로 다시 적용해 일직선 히스토리를 만든다.

  • GitOps

    GitOps는 "환경이 어떤 상태여야 하는가"를 전부 git 저장소에 선언해 두고, git에 적힌 것을 유일한 진실(single source of truth)로 삼아 실제 환경을 맞추는 운영 방식이다. 서버에 직접 손대지 않고, 바꾸고 싶으면 git을 고친다.

  • git reset (soft·mixed·hard)

    git reset <커밋>은 현재 브랜치가 가리키는 커밋(HEAD)을 옮기는 명령이다. 옵션에 따라 스테이징 영역(staging area, index)과 작업 디렉터리(working directory)까지 함께 되돌릴지가 달라진다.

  • 인터랙티브 리베이스로 커밋 정리

    git rebase -i는 최근 커밋 목록을 편집기로 열어 순서를 바꾸고, 합치고, 메시지를 고치게 해 주는 명령이다. 리뷰 전에 "작업하면서 생긴 지저분한 커밋"을 읽기 좋은 단위로 다듬을 때 쓴다.

  • 정리(Tidying)

    동작을 바꾸지 않는 아주 작은 구조 변경. 켄트 벡은 리팩터링이라는 말이 "기능 개발 중간의 긴 공사"처럼 쓰이며 동작 보존 원칙이 흐려지자, 더 작고 겁나지 않는 단위를 정리라는 이름으로 따로 불렀다. refactoring의 부분집합이다.

보기 옵션