Git repository 一分為二的做法

活著 (active) 的專案隨著時間過去只會愈長愈大,豬養肥了就是要殺? 不管理由如何,總是有重整的機會。一般的情況下,目錄結構都是有組織的,也就是同性質的 resource 會擺放在一起,所以大部份的 case 都是拆單一目錄出來為獨立的 git repository。

依目錄拆 repository,換句話說就是把該目錄的 commit 全部保留,其餘的不要,當你建出這一個分支時,剩下的就只是合併剩下的成果而已,只要掌握這個概念的話,下面的操作就不難理解。

一開始我的土炮做法,以 laravel/framework 1 為例子,把 illuminate/database 拆出來。

首先做不做備份取決於你的信心,因為 remote 那有一份了,所以我自已都沒有做。
你可以另外 clone 一份 repository 下來,也可以另外開 branch 出來操作。

$ git clone git@github.com:laravel/framework.git

開一個 database branch 來操作

$ cd framework
framework (master) $ git checkout -b database
framework (database) $ 

利用 git filter-branch 來改寫歷史

framework (database) $ git filter-branch --subdirectory-filter src/Illuminate/Database/ database
Rewrite adbb29172a4a70972c15e9564649dd4b520fcf66 (1059/1059)
Ref 'refs/heads/database' was rewritten

結束後可以看到 src/Illuminate/Database 目錄下的檔案都攤在根目錄了

framework (database) $ ls -l
total 136
drwxrwxr-x 10 gasolwu gasolwu  4096  6月 17 01:35 ./
drwxrwxr-x 10 gasolwu gasolwu  4096  6月 17 01:28 ../
drwxrwxr-x  2 gasolwu gasolwu  4096  6月 17 01:35 Capsule/
-rwxrwxr-x  1 gasolwu gasolwu   889  6月 17 01:35 composer.json*
-rwxrwxr-x  1 gasolwu gasolwu  2895  6月 17 01:35 ConnectionInterface.php*
-rwxrwxr-x  1 gasolwu gasolwu 20643  6月 17 01:35 Connection.php*
-rwxrwxr-x  1 gasolwu gasolwu   502  6月 17 01:35 ConnectionResolverInterface.php*
-rwxrwxr-x  1 gasolwu gasolwu  1615  6月 17 01:35 ConnectionResolver.php*
drwxrwxr-x  2 gasolwu gasolwu  4096  6月 17 01:35 Connectors/
drwxrwxr-x  3 gasolwu gasolwu  4096  6月 17 01:35 Console/
-rwxrwxr-x  1 gasolwu gasolwu  6141  6月 17 01:35 DatabaseManager.php*
-rwxrwxr-x  1 gasolwu gasolwu  1267  6月 17 01:35 DatabaseServiceProvider.php*
drwxrwxr-x  3 gasolwu gasolwu  4096  6月 17 01:35 Eloquent/
drwxrwxr-x  8 gasolwu gasolwu  4096  6月 17 01:35 .git/
-rwxrwxr-x  1 gasolwu gasolwu  3611  6月 17 01:35 Grammar.php*
drwxrwxr-x  3 gasolwu gasolwu  4096  6月 17 01:35 Migrations/
-rwxrwxr-x  1 gasolwu gasolwu  5537  6月 17 01:35 MigrationServiceProvider.php*
-rwxrwxr-x  1 gasolwu gasolwu  1444  6月 17 01:35 MySqlConnection.php*
-rwxrwxr-x  1 gasolwu gasolwu  1194  6月 17 01:35 PostgresConnection.php*
drwxrwxr-x  4 gasolwu gasolwu  4096  6月 17 01:35 Query/
-rw-rw-r--  1 gasolwu gasolwu  1340  6月 17 01:35 QueryException.php
-rwxrwxr-x  1 gasolwu gasolwu  2198  6月 17 01:35 README.md*
drwxrwxr-x  3 gasolwu gasolwu  4096  6月 17 01:35 Schema/
-rwxrwxr-x  1 gasolwu gasolwu  1661  6月 17 01:35 Seeder.php*
-rwxrwxr-x  1 gasolwu gasolwu   975  6月 17 01:35 SeedServiceProvider.php*
-rwxrwxr-x  1 gasolwu gasolwu  1133  6月 17 01:35 SQLiteConnection.php*
-rwxrwxr-x  1 gasolwu gasolwu  2122  6月 17 01:35 SqlServerConnection.php*

這時後也可以用 tiggit log 看一下 history 長什麼樣子。

framework (database) $ git log -m -stat -n 1
commit 291c2e9139086aefefb3db32e56675c67d92c1ec (from ec71e594c245eeb5c05d6de7a770399e045341a4)
Merge: ec71e59 74b8089
Author: Taylor Otwell <taylorotwell@gmail.com>
Date:   Fri Jun 13 15:12:36 2014 -0500

    Merge branch '4.2'

 Console/Migrations/RefreshCommand.php | 10 ++++++++--
 Schema/Blueprint.php                  | 10 ++++++++++
 2 files changed, 18 insertions(+), 2 deletions(-)

commit 291c2e9139086aefefb3db32e56675c67d92c1ec (from 74b8089b6ff5aed2129cbd7e7559144b68d3f660)
Merge: ec71e59 74b8089
Author: Taylor Otwell <taylorotwell@gmail.com>
Date:   Fri Jun 13 15:12:36 2014 -0500

    Merge branch '4.2'

 Query/Builder.php | 46 ++++++++++++++++++++++++++++++++++++++++++++--
 composer.json     | 23 +++++++++++------------
 2 files changed, 55 insertions(+), 14 deletions(-)
 
commit 74b8089b6ff5aed2129cbd7e7559144b68d3f660
Author: Taylor Otwell <taylorotwell@gmail.com>
Date:   Fri Jun 13 14:28:23 2014 -0500

    Working on documentation block.

 Schema/Blueprint.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

如果不想攤在根目錄,而是放在 src 目錄的話,要再多做一步 filter-branch,--tree-filter 後面接的是 command 會被 shell 執行 (透過 eval),所以這邊我們建立 src/ 目錄 (replay 當下可能不存在),然後判斷根目錄的每個檔案如果不是一開始建立的 src/ 目錄,就把它搬到 src/ 裡去

framework (database) $ git filter-branch -f --tree-filter 'mkdir -p src; for d in *; do if [ $d != "src" ]; then mv $d src/; fi; done;' database
Rewrite 291c2e9139086aefefb3db32e56675c67d92c1ec (1059/1059)
Ref 'refs/heads/database' was rewritten

結果就變這樣

framework (database) $ tree -d
.
└── src
    ├── Capsule
    ├── Connectors
    ├── Console
    │   └── Migrations
    ├── Eloquent
    │   └── Relations
    ├── Migrations
    │   └── stubs
    ├── Query
    │   ├── Grammars
    │   └── Processors
    └── Schema
        └── Grammars

14 directories

最後,因為歷史被改寫的關係,masterdatabase branch 除了 commit 訊息相似之外,已經是兩條平行的 tree 了,只要簡單 init 一個 git repository,並把創造出來的 database branch fetch 回來,然後 merge 就結束了 (等同於 git pull 的操作)。

$ cd /path/to/new_repo
new_repo $ git init
Initialized empty Git repository in /path/to/new_repo/.git/
new_repo (master) $ git fetch /path/to/framework database
remote: Counting objects: 5889, done.
remote: Compressing objects: 100% (1821/1821), done.
remote: Total 5889 (delta 3026), reused 3762 (delta 3007)
Receiving objects: 100% (5889/5889), 1.54 MiB | 0 bytes/s, done.
Resolving deltas: 100% (3026/3026), done.
From ../framework
 * branch            database   -> FETCH_HEAD
 
new_repo (master) $ git merge FETCH_HEAD

想像一下,補上 composer.json 和 autoload PSR-4 後,是不是就像是一個獨立的 composer package,建議可以用這個方式拆 module,module 一多就可以利用 composer 的 replace 來組 full stack 的 package,有點像是 Debian dummy package 或 FreeBSD meta port 的概念,拆細的彈性就在這裡。


另外補充一下其他作法,--subdirectory-filter 那邊可以用 git subtree,上面兩段 filter-branch 可以用下面一行來完成...

framework (database) $ git filter-branch --subdirectory-filter src/Illuminate/Database/ --index-filter 'git ls-files -s | sed "s|\t\"*|&src/|" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE' database

指令有點長,不過這一段有寫在 manpage 的範例裡,所以不用刻意去背它。