因為 Subversion 目前沒有 multiple repository 的功能,無法跨 repositories 作 copy/move/switch/diff/merge 等動作,在跨 project 整合時,會有些麻煩。因此,我寫了一個叫 svn-sync-external.sh 的 shell script,希望能去除這些麻煩。

這些是什麼麻煩呢?舉我現在碰到的需求為例,我現在要將 Lady BBS 從古老的 SOB 改成用 pttbbs。剛好 pttbbs 有利用 Subversion 提供 Ptt Source,按照 SVNBook 的說法,利用 vendor branches 理論上可以讓我隨時更新到最新版的 pttbbs。

然而,事情其實並不是這麼簡單地。最大的癥結點,在於我也會改動到 BBS 的 source,即使是使用了 vendor branches 技法,因為是使用 import 將 Ptt Source 拉回來,所以實際上是已經與 Ptt Source 拖勾了,仍然是必須要進行許多手動的操作,以便把 Ptt Source 版本之間的差異,施行到我自己的 source tree 裡。

若只是偶爾在大改版的時候作一遍,這也還好,但若想要緊緊追著 vendor branch,那就麻煩至極了。而且換個角度來說,若僅在大改版時,更新 vendor branch,難保不會因為更動太大,使自己做的那些改變,無法相容於新版 vendor branch。若能常常追著 vendor branch 跑,或是能夠以「步進」的方式,將一次的大幅度版本更新,改以很多次小幅度的版本更新進行,就可以把衝突降低到最小。但無論如何,要這麼做,就必須先把「手動 merge」的麻煩,降到最低,最好是能夠就像 svn:externals 一樣,下一個指令,就做完所有事情。

svn-sync-external.sh 這隻 shell script 的基本原理是,用 svn export 把 Ptt Source 從遠端拉過來,然後 svn add 進自己的 repository 裡,並加上 sync:urlsync:revision 這兩個 properties 紀錄來源。之後,要更新 Ptt Source 時,就先用 svn diffsync:url 把新的 revision 與原來的 revision (記錄在 sync:revision) 之間差異拉回來存成 universal diff format,再用 patch 將版本之間的差異,施行進自己的 working directory 裡,然後,該 svn add 的加一加,該 svn rm 的刪一刪,就作成了新的 change set,人工確認過之後,便可以 svn commit 進去。

有別於 svn:externals,我把透過 svn-sync-external.sh 拉進來的遠端 source,稱作 sync-external。建立新的 sync-external 的程序,大致如下 (使用以 + 開頭的行展示內部的運作):

SHELL> svn-sync-external.sh import \                            http://opensvn.csie.org/pttbbs/trunk/pttbbs \                            pttbbs-trunk+ rev=`svn-maxver.sh http://opensvn.csie.org/pttbbs/trunk/pttbbs`+ svn export --revision $rev http://opensvn.csie.org/pttbbs/trunk/pttbbs pttbbs-trunk+ svn add pttbbs-trunk+ svn propset sync:url http://opensvn.csie.org/pttbbs/trunk/pttbbs+ svn propset sync:revision $rev pttbbs-trunk

而建立好 sync-external 之後,就可以隨時如下面的程序,將 sync-external 更新到最新 (或指定) 的版本:

SHELL> svn-sync-external.sh synchronize pttbbs-trunk+ url=`svn propget sync:url`+ oldrev=`svn propget sync:url`+ newrev=`svn-maxver.sh $url`+ patch_file=`mktemp /tmp/foo`+ patch_suffix=`basename $patch_file`+ svn diff --revision $oldrev:$newrev $url >> $patch_file+ cat $patch_file | patch --unified --quiet --suffix $patch_suffix --directory=pttbbs-trunk+ svn propset sync:revision $newrev+ svn status | grep ^\? | sed -e s/^\?// | xargs svn add;+ svn status | grep ^! | sed -e s/^\!// | xargs svn rm;+ rm -f $patch_file+ find $path -type f -name "*$patch_suffix" | xargs rm -f

SHELL> svn commit pttbbs-trunk...

有了 sync-external 之後,我們就可以隨時用 svn 的 copy/move/switch/diff/merge 操作來自 Ptt Source 的程式碼,因為那已經是在自己的 repository 裡了。而當需要更新 Ptt Source 的版本時,就再執行一次 svn-sync-external.sh sync pttbbs-trunk,就可以更新了。又,因為內部是以 svn diff 加上 patch 在運作的,所以搭配 --revision 參數,可以任意指定要 sychronize 到哪一個版本,換句話說,要 down-grade 也是可以做到的。

目前的 svn-sync-external.sh 已經實作了 import 與 sychronize 兩個 sub commands,我計畫再實作 merge、status 和 switch 等 sub commands,這樣應該就蠻完整的了。

在 SVNBook 的 vendor branches 章節後面有提到 svn_load_dirs.pl 這隻程式,也可以做到類似的效果。不過因為其行為有些不太符合我的需求,因此我就沒有直接用,而是自己寫一個 script 來用。一來似乎不能 down-grade,二來 silence commit 也蠻可怕,更重要的是,我寫好了才發現有這東西可以用。不過無論如何,這畢竟只是簡單地將一些原本可以手動完成的工作自動化而已,如要獲得真正的 multiple repository 的能力,還是要依靠如 SVK 之類的 svn-addon 系統,才有辦法。