供方分支

當開發軟體時有這樣一個情況,你版本控制的資料可能關聯於或者是依賴於其他人的資料,通常來講,你的項目的需要會要求你自己的項目對外部實體提供的資料保持盡可能最新的版本,同時不會犧牲穩定性,這種情況總是會出現—只要某個小組的訊息對另一個小組的訊息有直接的影響。

舉個例子,軟體開發者會工作在一個使用第三方函式庫的應用,Subversion恰好是和Apache的Portable Runtime library(見「Apache 可移植運行庫」一節)有這樣一個關係。Subversion原始碼依賴於APR庫來實現可移植需求。在Subversion的早期開發階段,項目緊密地追蹤APR的API修改,經常在庫代碼的「流血的邊緣」粘住,現在APR和Subversion都已經成熟了,Subversion只嘗試同步APR的經過良好測試的,穩定的API庫。

現在,如果你的項目依賴於其他人的訊息,有許多方法可以用來嘗試同步你的訊息,最痛苦的,你可以為項目所有的貢獻者發佈口頭或書寫的指導,告訴他們確信他們擁有你們的項目需要的特定版本的第三方訊息。如果第三方訊息是用Subversion版本庫維護,你可以使用Subversion的外部定義來有效的「強制」特定的版本的訊息在你的工作副本的的位置(見「外部定義」一節)。

但是有時候,你希望在你自己的版本控制系統維護一個針對第三方資料的自行定義修改,回到軟體開發的例子,程式設計師為了他們自己的目的會需要修改第三方函式庫,這些修改會包括新的功能和bug修正,在成為第三方工具官方發佈之前,只是內部維護。或者這些修改永遠不會傳給庫的維護者,只是作為滿足軟體開發需要的單獨的自行定義修改存在。

現在你會面對一個有趣的情形,你的項目可以用某種脫節的樣式保持它關於第三方資料自己的修改,如使用補綴(Patch)文件或者是完全的可選版本的文件和目錄。但是這很快會成為維護的頭痛的事情,需要一種機制來應用你對第三方資料的自行定義修改,並且迫使在第三方資料的後續版本重建這些修改。

這個問題的解決方案是使用供方分支,一個供方分支是一個目錄樹保存了第三方實體或供應方的訊息,每一個供應方資料的版本吸收到你的項目叫做供方drop

供方分支提供了兩個關鍵的益處,第一,通過在我們的版本控制系統保存現在支持的供方drop,你項目的成員不需要指導他們是否有了正確版本的供方資料,他們只需要作為不同工作副本更新的一部份,簡單的接受正確的版本就可以了。第二,因為資料存在於你自己的Subversion版本庫,你可以在恰當的位置保存你的自行定義修改—你不需要一個自動的(或者是更壞,手工的)方法來交換你的自行定義行為。

常規的供方分支管理過程

管理供方分支通常會像這個樣子,你建立一個最上層的目錄(如/vendor)來保存供方分支,然後你匯入第三方的代碼到你的子目錄。然後你將拷貝這個子目錄到主要的開發分支(例如/trunk)的適當位置。你一直在你的主要開發分支上做本地修改,當你的追蹤的代碼有了新版本,你會把帶到供方分支並且把它合併到你的/trunk,解決任何你的本地修改和他們的修改的衝突。

也許一個例子有助於我們闡述這個算法,我們會使用這樣一個場景,我們的開發團隊正在開發一個計算器程序,與一個第三方的複雜數字運算庫libcomplex關聯。我們從供方分支的初始建立開始,並且匯入供方drop,我們會把每株分支目錄叫做libcomplex,我們的代碼drop會進入到供方分支的子目錄current,並且因為svn import建立所有的需要的中間父目錄,我們可以使用一個命令完成這一步。

$ svn import /path/to/libcomplex-1.0 \
             http://svn.example.com/repos/vendor/libcomplex/current \
             -m 'importing initial 1.0 vendor drop'
…

我們現在在/vendor/libcomplex/current有了libcomplex當前版本的代碼,現在我們為那個版本作標籤(見「標籤」一節),然後拷貝它到主要開發分支,我們的拷貝會在calc項目目錄建立一個新的目錄libcomplex,它是這個我們將要進行自行定義的供方資料的拷貝版本。

$ svn copy http://svn.example.com/repos/vendor/libcomplex/current  \
           http://svn.example.com/repos/vendor/libcomplex/1.0      \
           -m 'tagging libcomplex-1.0'
…
$ svn copy http://svn.example.com/repos/vendor/libcomplex/1.0  \
           http://svn.example.com/repos/calc/libcomplex        \
           -m 'bringing libcomplex-1.0 into the main branch'
…

我們取出我們項目的主分支—現在包括了第一個供方釋放的拷貝—我們開始自行定義libcomplex的代碼,在我們知道之前,我們的libcomplex修改版本是已經與我們的計算器程序完全集成了。 [23]

幾周之後,libcomplex得開發者發佈了一個新的版本—版本1.1—包括了我們很需要的一些特性和功能。我們很希望升級到這個版本,但不希望失去在當前版本所作的修改。我們本質上會希望把我們當前基線版本是的libcomplex1.0的拷貝替換為libcomplex 1.1,然後把前面自行定義的修改應用到新的版本。但是實際上我們通過一個相反的方向解決這個問題,應用libcomplex從版本1.0到1.1的修改到我們修改的拷貝。

為了執行這個升級,我們取出一個我們供方分支的拷貝,替換current目錄為新的libcomplex 1.1的代碼,我們只是拷貝新文件到存在的文件上,或者是解壓縮libcomplex 1.1的打包文件到我們存在的文件和目錄。此時的目標是讓我們的current目錄只保留libcomplex 1.1的代碼,並且保證所有的代碼在版本控制之下,哦,我們希望在最小的版本控制歷史擾動下完成這件事。

完成了這個從1.0到1.1的代碼替換,svn status會顯示文件的本地修改,或許也包括了一些未版本化或者丟失的文件,如果我們做了我們應該做的事情,未版本化的文件應該都是libcomplex在1.1新引入的文件—我們運行svn add來將它們加入到版本控制。丟失的文件是存在於1.1但是不是在1.1,在這些路徑我們運行svn delete。最終一旦我們的current工作副本只是包括了libcomplex1.1的代碼,我們可以提交這些改變目錄和文件的修改。

我們的current分支現在保存了新的供方drop,我們為這個新的版本建立一個新的標籤(就像我們為1.0版本drop所作的),然後合併這從個標籤前一個版本的區別到主要開發分支。

$ cd working-copies/calc
$ svn merge http://svn.example.com/repos/vendor/libcomplex/1.0      \
            http://svn.example.com/repos/vendor/libcomplex/current  \
            libcomplex
… # resolve all the conflicts between their changes and our changes
$ svn commit -m 'merging libcomplex-1.1 into the main branch'
…

在這個瑣碎的用例裡,第三方工具的新版本會從一個文件和目錄的角度來看,就像前一個版本。沒有任何libcomplex原始文件會被刪除、被改名或是移動到別的位置—新的版本只會保存針對上一個版本的文字修改。在完美世界,我們對呢修改會乾淨得應用到庫的新版本,不會產生任何並發和衝突。

但是事情總不是這樣簡單,實際上原始文件在不同的版本間的移動是很常見的,這種過程複雜性可以確保我們的修改會一直對新的版本代碼有效,可以很快使形勢退化到我們需要在新版本手工的重新建立我們的自行定義修改。一旦Subversion知道了給定文件的歷史—包括了所有以前的位置—合併到新版本的進程就會很簡單,但是我們需要負責告訴Subversion供方drop之間原始文件部署的改變。

svn_load_dirs.pl

不僅僅包含一些刪除、新增和移動的供方drops使得升級第三方資料後續版本的過程變得複雜,所以Subversion提供了一個svn_load_dirs.pl腳本來輔助這個過程,這個腳本自動進行我們前面提到的常規供方分支管理過程的匯入步驟,從而使得錯誤最小化。你仍要負責使用合併命令合併第三方的新 版本資料合併到主要開發分支,但是svn_load_dirs.pl幫助你快速到達這一步驟。

一句話,svn_load_dirs.pl是一個增強的svn import,具備了許多重要的特性:

  • 它可以在任何有一個存在的版本庫目錄與一個外部的目錄匹配時執行,會執行所有必要的新增和刪除並且可以選則執行移動。

  • 它可以用來操作一系列複雜的操作,如那些需要一個中間媒介的提交—如在操作之前重命名一個文件或者目錄兩次。

  • 它可以隨意的為新匯入目錄打上標籤。

  • 它可以隨意為符合正則表達式的文件和目錄新增任意的屬性。

svn_load_dirs.pl利用三個強制的參數,第一個參數是Subversion工作的基本目錄URL,第二個參數在URL之後—相對於第一個參數—指向當前的供方分支將會匯入的目錄,最後,第三個參數是一個需要匯入的本地目錄,使用前面的例子,一個典型的svn_load_dirs.pl調用看起來如下:

$ svn_load_dirs.pl http://svn.example.com/repos/vendor/libcomplex \
                   current                                        \
                   /path/to/libcomplex-1.1
…

你可以說明你會希望svn_load_dirs.pl同時打上標籤,這使用-t指令列選項,需要指定一個標籤名,這個標籤是第一個參數的一個相對URL。

$ svn_load_dirs.pl -t libcomplex-1.1                              \
                   http://svn.example.com/repos/vendor/libcomplex \
                   current                                        \
                   /path/to/libcomplex-1.1
…

當你運行svn_load_dirs.pl,它會檢驗你的存在的「current」供方drop,並且與提議的新供方drop比較,在這個瑣碎的例子裡,沒有文件只出現在一個版本裡,腳本執行新的匯入而不會發生意外。然而如果版本之間有了文件部署的區別,svn_load_dirs.pl會詢問你如何解決這個區別,例如你會有機會告訴腳本libcomplex版本1.0的math.c文件在1.1已經重命名為arithmetic.c,任何沒有解釋為移動的差異都會被看作是常規的新增和刪除。

這個腳本也接受單獨設定文件用來為新增到版本庫的文件和目錄設定匹配正則表達式的屬性。設定文件通過svn_load_dirs.pl-p指令列選項指定,這個設定文件的每一行都是一個空白分割的兩列或者四列值:一個Perl樣式的正則表達式來匹配新增的路徑、一個控制關鍵字(break或者是cont)和可選的屬性名和值。

\.png$              break   svn:mime-type   image/png
\.jpe?g$            break   svn:mime-type   image/jpeg
\.m3u$              cont    svn:mime-type   audio/x-mpegurl
\.m3u$              break   svn:eol-style   LF
.*                  break   svn:eol-style   native

對每一個新增的路徑,會按照順序為匹配正則表達式的文件設定屬性,除非控制標誌是break(意味著不需要更多的路徑匹配應用到這個路徑)。如果控制說明是contcontinue的縮寫—然後匹配工作會繼續到設定文件的下一行。

任何正則表達式,屬性名或者屬性值的空格必須使用單引號或者雙引號環繞,你可以使用反斜槓(\)換碼符來迴避引號,反斜槓只會在解析設定文件時迴避引號,所以不能保護必要正則表達式字元之外的的其它字元。



[23] 而且完全沒有bug,當然!