Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

git filter-branch로 저장소 분리하기

프로젝트를 구성할 때 처음에는 요구사항이 많지 않으므로 Monolithic 아키텍처로 저장소 하나에 프로젝트 전체를 넣어서 구성하지만, 나중에는 너무 커져서 프로젝트를 구분하고 싶은 경우가 생긴다. 요즘은 Microservice 아키텍처가 인기이기도 하고 프로젝트가 커지면 API 서버를 기능별로 나누게 된다거나 어드민을 분리하거나 API와 웹을 분리하게 되는 경우가 생긴다. 처음부터 잘 구성해서 저장소를 나누는 것도 방법이지만 초기에는 하나의 저장소로 관리할 때의 장점이 있으므로 꼭 어느 쪽이 좋다고 할 수는 없다.

이 저장소를 Git으로 관리한다고 했을 때 원하는 파일이나 디렉터리만 복사해서 새로운 프로젝트를 구성해도 된다. 그냥 복사해서 새로 구성하는 경우에는 새로운 프로젝트 쪽에는 파일에 대한 Git으로 관리한 그동안의 히스토리가 없어지므로 좋은 방법이 아니다. Git은 분산 저장소이므로 A, B로 프로젝트를 분리한다고 하면 저장소를 2개 복사해서 A 쪽에서는 B 관련 파일을 다 지우고 B에서는 A 관련 파일을 다 지운 다음에 새로 만든 각 저장소에 푸시를 해도 되기는 한다.

git filter-branch

하지만 git에서는 filter-branch라고 브랜치를 재작성할 수 있는 기능을 제공하고 있다. 간단히 말하면 필터를 제공해서 필터에 적용된 파일만 가지고 히스토리를 다시 구축하는 기능이고 최근에 프로젝트를 분리해야 할 일이 있어서 이 기능을 사용해 봤다.

예시로 테스트할 저장소가 필요하므로 Node.js의 홈페이지인 nodejs.org 저장소를 가지고 프로젝트를 분리해 보자.

$ git log --oneline | wc -l
    1547

이 저장소에는 현재 1,547개의 커밋이 있고 다음과 같은 디렉터리구조로 되어 있다.

├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── README.md
├── TRANSLATION.md
├── build.js
├── events/
├── layouts/
├── locale/
├── package.json
├── scripts/
├── server.js
├── source/
├── static/
└── test/

filter-branch는 꽤 다양한 기능을 제공하고 있는데 문서를 봐도 사용법을 다 파악하기 어려우므로 내가 겪은 몇 가지 상황만 살펴보자.

특정 디렉터리 분리하기

특정 디렉터리가 하나의 프로젝트가 되는 구조라면 디렉터리 하나만 불리하면 별도의 프로젝트를 만들 수 있다. 이런 경우에는 gilter-branch로 쉽게 분리할 수 있다. filter-branch에는 다양한 필터를 제공하는 데 여기서는 특정 디렉터리만 분리하는 것이므로 --subdirectory-filter를 사용해 보자.

--subdirectory-filter
Only look at the history which touches the given subdirectory. The result will contain that directory (and only that) as its project root.

지정한 디렉터리의 히스토리만 모아서 프로젝트 루트로 만들어 준다. 위 디렉터리 구조에서 언어별 번역이 있는 locale 디렉터리만 별도 저장소로 분리한다고 하면 다음과 같이 사용하면 된다.

$ git filter-branch --subdirectory-filter locale -- --all
Rewrite d86dcffa9c5b5ab7c89dce8620a1a4cedc05abc8 (751/763) (27 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten
Ref 'refs/remotes/origin/master' was rewritten
Ref 'refs/remotes/origin/add/mailchimp-form-partial' was rewritten
WARNING: Ref 'refs/remotes/origin/master' is unchanged
Ref 'refs/remotes/origin/remove/fidelity-logo' was rewritten

$ git log --oneline | wc -l
     762

이렇게 하면 locale 디렉터리와 관련된 커밋을 처음부터 찾아서 재작성을 해준다. 커밋갯수가 762개로 줄어든 것을 볼 수 있다.

├── de
├── en
├── es
├── it
├── ja
├── ko
├── uk
└── zh-cn

디렉터리 구조를 보면 locale안의 내용만 프로젝트 루트로 남은 것을 볼 수 있다. 이렇게 분리해서 새로운 원격저장소에 푸시하면 된다.

원하는 파일만으로 분리하기

위처럼 프로젝트가 하나의 디렉터리로만 이뤄진 경우는 간단하지만 보통 프로젝트는 그렇지 않다. 공통 파일 등이 여기저기 흩어져 있는 때도 있고 테스트 코드나 프로젝트 설정 파일처럼 소스와 다른 파일이 여러 디렉터리에 흩어져 있는 경우가 더 흔할 것이다. 이때는 --index-filter를 사용해야 한다.

--index-filter
This is the filter for rewriting the index. It is similar to the tree filter but does not check out the tree, which makes it much faster. Frequently used with git rm --cached --ignore-unmatch ..., see EXAMPLES below.


index를 재작성한다고 나와 있기는 한데 나도 사실 이 필터의 기능을 제대로 알지는 못한다.(Git 명령어는 고급을 위해 조합되기 시작하면 너무 복잡해 져서... ㅠ) 원하는 디렉터리와 파일만 이용해서 히스토리를 재구성하는 방법을 Stackoverflow에서 찾아서 사용했을 뿐이다. ```bash $ git filter-branch --index-filter \ > 'git rm --cached -qr --ignore-unmatch -- . && \ > git reset -q $GIT_COMMIT -- \ > locale/ \ > README.md \ > scripts/event-geo.js \ > tests/build.smoketest.js' \ > --prune-empty -- --all Rewrite d86dcffa9c5b5ab7c89dce8620a1a4cedc05abc8 (1531/1549) (70 seconds passed, remaining 0 predicted) Ref 'refs/heads/master' was rewritten Ref 'refs/remotes/origin/master' was rewritten Ref 'refs/remotes/origin/add/mailchimp-form-partial' was rewritten WARNING: Ref 'refs/remotes/origin/master' is unchanged Ref 'refs/remotes/origin/remove/fidelity-logo' was rewritten $ git log --oneline | wc -l 1065

--index-filter에 많은 git 명령어가 들어가긴 했지만(다 이해하고 싶었는데 복잡해서 잘 모르겠다.) 4번부터 7번 라인 사이에 원하는 파일이나 디렉터리를 나열해 주면 이 파일만 모아서 히스토리를 새로 구성해 준다. 최종 커밋 개수도 1,065개로 줄어든 것으로 불 수 있다.

├── README.md
├── locale
│   ├── de
│   ├── en
│   ├── es
│   ├── it
│   ├── ja
│   ├── ko
│   ├── uk
│   └── zh-cn
├── scripts
│   └── event-geo.js
└── tests
    └── build.smoketest.js

디렉터리 구조를 보면 앞에서 지정한 파일만 남은 것을 볼 수 있다.

2016/10/29 22:32 2016/10/29 22:32