따로 관리하던 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:maingit 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 문서