使用 TortoiseSVN 處理版本的分支與合併(一)

4/01/2010
在介紹 Subversion 分支與合併的觀念時,常見以 trunk 代表主線版本、以 branch 代表支線版本的方式來組織檔案庫的目錄結構。例如,資料夾 MyProject\trunk 底下用來存放專案的主線版本,而 MyProject\branches 底下則存放支線的版本。

主線只一條,支線可能很多,當然,這些路線可以「分進合擊」。這篇講分(分支),下一篇講合(合併)。

一個例子

有時候,我們可能不會一開始就建立 trunk 目錄--也許是因為在建立檔案庫時,覺得專案沒那麼複雜,不需要這麼做;也可能是因為其他特殊原因,例如:團隊制定的規範。這裡用一個 ASP.NET Web 專案為例,說明可能碰到的問題與應變方法。

假設這個 ASP.NET Web 專案的目錄結構類似這樣:


這裡我們把文件、網站應用程式的原始碼、商業邏輯層的類別庫原始碼都加入檔案庫,並分別放在 Document、Web、和 BusinessLogicLib 資料夾底下。其中 Web 資料夾除了固定的 Bin 目錄,還依子系統分出庫存(Inventory)、會計(Account)、人事(HR)等子目錄。

現在假設碰到一種狀況:人事系統的使用者不斷提出新需求,導致該系統的程式需要大改。問題是,這些修改需要好幾月的時間才能完成,期間勢必還有一些更要緊的 bug 修正或細部的程式修改必須同時進行,怎麼辦?

一個直覺的辦法,是讓開發人員自行管理兩個版本,也就是說,把整個 HR 資料夾複製成另一份,目錄名稱也許叫做「HR_NewFeature」,代表這是為了開發新功能而特別建立的目錄。如此一來,開發人員就可以分頭修改 HR 和 HR_NewFeature 底下的檔案,等到需要交付時,才把兩邊的程式碼整合起來。如果你曾這麼做,應該能體會它有多麻煩--程式碼裡面到處得加上註解,標示哪一段是為何修改、哪一段必須等到所有新版功能完成時才能交付……諸如此類的,以便在整合兩邊的程式碼時不會亂掉。但事實上,這種動作既麻煩,又容易出錯,到最後可能連程式設計師自己都分不清楚哪個部分要交付,哪個部分不要。

既然如此,何不試試用工具來減輕自己的負擔?

廉價複製

與其用手動複製檔案、剪貼程式碼的方式管理版本,我們可以用 Subversion 的廉價複製功能來建立那些支線的版本(如上例中的 HR_NewFeature),並視需要進行版本之間的合併。

這裡的廉價複製,指的是 svn copy 指令。但為什麼說「廉價複製」呢?因為當你利用 svn copy 來複製資料夾的時候,雖然在本機的工作目錄中確實可以看到兩個資料夾和複製的檔案,但檔案庫裡面並沒有真正複製所有的檔案內容,而只是類似建立連結的動作,或者只儲存有變動的部分而已。這種複製動作的負載輕、成本低,故曰廉價。
可以這麼說:Subversion 檔案庫裡面其實沒有「分支」這種東西,所有的變動都只是之前版本的另一個廉價複製版本。我們只是利用廉價複製的功能,產生一個資料夾的複本,並將它視為分支,如此而已。

決定分支的存放位置

延續前面的案例,現在我們要開始為人事系統(HR)建立支線版本。首先碰到的問題,是支線的目錄要放哪?以 Web 專案來說,分支的目錄顯然不能放在 Web 資料夾底下,因為如此一來,同一個 Web 專案將會因為建立分支(廉價複製)的緣故而產生類別名稱衝突的情形。

我們可以這樣安排分支的目錄:



也就是在專案(檔案庫)的根目錄下建立 branches 資料夾,以便將各類原始碼的支線納入此目錄下管理。這種方式的一個優點,是在主線與支線的版本之間來回切換與合併時,從檔案的 URL 前面幾個字元就能分辨是主線還是支線,不容易搞混。當然這只是一種選擇,不代表最好的方式。如果你一開始就想到將來可能需要建立分支,在建立檔案庫時,就可以在根目錄下各建一個 trunk 和 branches 來存放主線與支線的檔案。

或許有點多餘,但還是註明一下:分支的目錄不可跨檔案庫(即不能將分支建立在別的檔案庫中)。
建立分支

首先把作為分支的上層目錄建立妥當,也就是 branches\Web,並確認這些目錄及主線的工作複本(working copy)都已送交至檔案庫。

接著在想要產生分支的資料夾(HR)上點右鍵 > TortoiseSVN > Branch/tag...。參考下圖:


你會看到如下圖的對話窗:


注意:若 HR 分支的上層目錄 branches\Web 沒有事先建立且送交至檔案庫,在建立 HR 分支時會失敗(路徑不存在)。

對話窗左下角的「Switch working copy to new branch/tag」核取方塊若有勾選,表示分支建立完成後,順便把目前的工作目錄切換至支線版本--通常會這麼做。當然你也可以選擇先不要切換到支線版本,等你開始要在工作目錄下開發支線版本的程式時,再利用 TortoiseSVN 的 switch 功能來切換。

Tip:第一次建立分支,可能會有點疑慮,擔心哪個步驟做錯了,或者不清楚發生了什麼事。這裡有個小建議,分支建好之後,不妨對 branches 目錄做 SVN Update,你會看到 branches\Web 底下多出一份 HR 目錄的拷貝(啊!原來分支就是 copy 嘛)。當然,你不用直接修改 branches 底下的檔案,而應當把主線的工作目錄(MyProject\Web\HR)切換到支線,再對其修改。原因很簡單:這樣你做的修改才會被編譯、執行。

在撰寫程式時,都是針對本機的同一個工作目錄,以此例而言,就是 MyProject\Web\HR。當你要修改主線的版本時,就用 switch 功能切換到主線;同樣的,要修改支線版本時,就切到支線。你所要牢記的,就是每次修改程式之前,先確認目前的工作目錄是對應到主線還是支線版本(否則你還是可能會亂掉)。

Q & A

問:我們公司有一台測試用的 app server,有時候,使用者或測試人員想要測試目前正式環境的版本,有時候又要切換到新開發的版本,這種情況也能用分支的方式解決嗎?

答:一樣適用。看他們想測哪個版本,就在測試 server 上用 SVN 的 switch 功能切換主線或支線版本。
================================================
問:這樣來回切換版本,如果支線版本很多,我的腦袋可能還是會亂掉,造成版本錯亂,這樣.....用工具來管理真的有比較省力?

答:如果真的碰到支線版本眾多,又常得來回切換的情形,不用工具管理只會更糟(想像你要記住的版本有多少種、將來如何合併)。若情況嚴重到連工具都幫不了忙,或許該停下腳步,回頭想想為什麼那些需求會如此反覆無常。
================================================
問:我怎麼知道目前工作目錄下的版本是主線還是支線?

答:TortoiseSVN 的 relocate 或 switch 功能都會顯示目前所對應的版本的 URL。


下一篇: 使用 TortoiseSVN 處理版本的分支與合併(二)

程式過客

2/24/2010
這是一件很多年前的往事,不知為什麼印象很深,最近又突然想起。

那年,我在一間不算大的公司裡寫程式,用的工具是 Borland Delphi,偶爾也寫點 C++。記得全公司好像就兩個人用 Delphi,一個是我,另一個就是老闆(我一直很佩服他)。有一次,幾個同事在辦公室閒聊,說到自己在對軟體開發這條路的想法,頗有「盍各言爾志」的 味道。某位仁兄的計畫是「打算先寫兩年程式,然後轉作 SA,磨練個兩三年的經驗,再升 PM。」我從小胸無大志,不知何謂長遠規劃,初次聽到這樣的「生涯發展路徑」,頓時覺得自己是否也該想想將來的路該怎麼走。

可是,當時我對未來的能見度大概就幾公尺遠(現在恐怕也差不多),只覺得自己可能會一直矇著頭寫程式,暗無天日,直到隧道盡頭冒出一點光,再循著光線摸索前進。

後來,我離開了那家公司,到別的地方繼續寫程式,除了參與一些新的軟體開發案,當然也少不了要維護前人留下來的 legacy code--寫程式很有趣,但修改別人的程式 bug 則是另一回事。我沒有再見到當初那位同事,也不知道他後來怎麼樣了,但是,這些年來,我似乎偶爾會在別人身上看到他的影子。我開始擔心,我參與的專案,PM 和 SA 是不是也都循著那樣的生涯路徑一路「升」上來的,我會不會碰到「說得一口好程式」的傢伙、會不會要維護他們寫的程式碼?(註:這裡的「說得一口好程式」,意思是程式撰寫經驗很有限、技術背景或 sense 不夠,卻常指點別人程式應怎麼寫;這樣的人總是認為「那些功能應該都很容易做到」,因而經常低估需求變動的成本、任意承諾需求,導致專案範圍不斷擴大、成本不斷增加,甚至懷疑開發人員的可行性評估老是在誆他。)

無論 PM、SA、SD、還是 PG(programmer),都各有其專業,團隊不應把工作流程的順序理所當然地視為職務的高低(否則測試人員不就更等而下之了?絕非如此!),然後想當然耳地任意指揮「下層」的團隊成員工作--這樣恐怕只會創造弱勢族群,令位於「下層」的人無心做好手邊的工作,老是想著「往上爬」。我懷疑「寫兩年程式就要轉做 SA」的念頭多少因此而起。而且,如此循環下去,推而廣之,恐怕很難造就專業的程式設計師,而是產生一堆「程式過客」。

都說軟體開發團隊裡不需要救火英雄了,但為什麼軟體專案老是有一堆爛攤子需要有人挺身而出力挽狂瀾?

時勢造英雄,我想多半是這樣的。

大而化之:淺談 maximize 的翻譯

1/26/2010
將 maximize 直接翻譯成「最大化」似乎已經是很普遍,一般人也接受的譯法。但我想有些場合,可以試試以「充分....」或「將....儘量提升」的方式來表現,閱讀起來會覺得比較口語化接近口語。

審稿碎碎念:幾個專有名詞修訂

1/26/2010
隨手記下幾個審稿時碰到的術語:

Subversion 目錄存取權限控管

1/21/2010
簡單記錄一下 Subversion 目錄存取權限控管的相關設定(即控制哪些人可以讀取/寫入哪些資料夾)。

Subversion 版本:CollabNet Subversion Server v1.6.5.*

Apache 的 httpd.conf 範例(不重要的部分已省略):
#
# Dynamic Shared Object (DSO) Support
# 注意以下幾個 .so 模組的載入先後順序,不可任意調換!
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule dav_module modules/mod_dav.so
LoadModule dav_svn_module    modules/mod_dav_svn.so
LoadModule authz_svn_module modules/mod_authz_svn.so

<Location /svn>
    DAV svn
    SVNParentPath d:/svn
    AuthType Basic
    AuthName "Subversion 檔案庫"

    #存取檔案庫時需要驗證帳號,拿掉這行就不會進行驗證 
    Require valid-user

    #驗證帳號時的帳號、密碼資料
    AuthUserFile "d:/svn/SvnUsers.txt"
    
    #帳號對此檔案庫的檔案目錄存取權限
    AuthzSVNAccessFile "d:/svn/FolderAccessControl.ini"
</Location>
這裡的身份驗證方式是採用 Basic Apache 驗證,其中的 AuthUserFile 所指向的檔案,就是使用者的帳號密碼檔,我是用一個批次檔來建立這些帳號密碼資料。批次檔的內容大概像這樣:
REM John
htpasswd -c -b SvnUsers.txt john john123

REM Vivid
htpasswd -b SvnUsers.txt vivid vivid123

REM Adams
htpasswd -b SvnUsers.txt adams adams123

httpd.conf 中的 <Location> 元素的 AuthzSVNAccessFile 屬性代表目錄存取授權的控制檔。在上面的範例中,檔名是 FolderAccessControl.ini。以下是這個檔案內容的範例:
[groups]
tester = john, vivid
dev = adams, michael

[MyProject:/]
@dev = rw
john = rw
* = r

[MyProject:/TestDoc]
@tester = rw
* = 
其中 [groups] 是用來定義使用者的群組(角色),這裡定義了兩個群組,分別是 dev 和 tester。之後的 [MyProject:/] 和 [MyProject:/TestDoc] 分別代表檔案庫 MyProject 的跟目錄和 TestDoc 子目錄的存取控制。要注意的是,由於我在 httpd.conf 中使用了 SVNParentPath,所以在目錄存取權限控制檔案中明確指定了檔案庫名稱(MyProject)。雖然也可以直接寫路徑名稱,例如 [/] 代表根目錄,[/TestDoc] 代表 TestDoc 子目錄,但這樣一來,如果別的檔案庫中也有相同路徑,就會套用到相同的存取權限,因而導致權限亂掉。

在定義資料夾的存取權限時,可指定特定使用者或群組的讀寫權限。群組名稱是以 '@ ' 字元開頭,以便和使用者名稱區別,例如 @tester = rw。等號右邊就是讀寫權限,r 代表可讀取,w 代表可寫入。如果寫 "* = r",即代表所有人都可以讀取,若等號右邊是空的,則表示既沒有讀取也沒有寫入權限,例如:"* = "(此為預設值,即如果你的權限控制檔內容是空的,任何人都無法讀/寫檔案庫的內容)。

目錄存取權限控制檔的內容如果有修改,會即時生效,不用重啟 Apache。

當使用者欲 commit 檔案時,如果她沒有寫入權限,在 commit 時就會出現錯誤:

error Commit failed (details follow):
error Server sent unexpected return value (403 Forbidden) in response to MKACTIVITY
error request for '/svn/MyProject/!svn/act/adeca4da-d94c-8438-6773709123ec'

還有另一種情況是,明明確定某個使用者有寫入權限,commit 時卻仍然出現上面的錯誤訊息,這很可能是因為當初 checkout 時,URL 裡面的路徑名稱的英文字母大小寫不正確。此時只要 relocate URL,將大小寫修正之後應該就能順利 commit 了。

關於書摘

1/18/2010
近日讀完米蘭昆德拉的《生命中不能承受之輕》,覺得這部小說寫的真好,基於「好東西與好朋友分享」的心理,忍不住做了點書摘,寄給幾位朋友,表示推薦之意。

好用的 .NET 反組譯工具:Reflector.FileDisassembler

1/10/2010
Reflector.FileDisassemblerReflector 的附加元件(add-in),有了它,就可以把整個 DLL 組件的所有類別一次反組譯成各個對應的類別檔,還挺好用的。

使用方法:
  1. Reflector.FileDisassembler 壓縮檔解開。我是把解開後的檔案放在 Reflector 程式所在目錄下的 Addins 資料夾。
  2. 開啟 Reflector,點 View > Add-ins > Add,然後選擇 Reflector.FileDisassembler.dll。選好之後關閉 Add-ins 視窗。
  3. 將欲反組譯的 DLL 加入 Reflector,並選取該 DLL。
  4. 點 Tools > Fils Disassembler,接著選擇原始碼要輸出到哪個資料夾,然後按 Generate 鈕,就可以產生 DLL 檔案中所有類別的原始碼。
參考畫面:



反組譯過程中,如果出現訊息視窗,告訴你找不到某些參考的 DLL,可按 Skip 忽略之。反組譯完成之後,開啟 Visual Stduio,建立一個新的 Class Library 專案,再將反組譯出來的原始碼加入專案,就可以進行編譯了。

不過,反組譯產生的原始碼不見得能順利通過編譯。我在反組譯成 C# 程式碼時,至少碰到下列問題:
  1. 有些變數名稱會帶 $ 符號,以至於無法編譯。
  2. 區域變數名稱重複宣告。
  3. 函式的引數型別為列舉常數,可是呼叫時卻直接傳入整數。
還好這些問題並不嚴重,稍微改一下程式碼就能解決。

另一款類似的工具 FileGenerator For Reflector 雖然連專案檔都可以幫你產生,但我試的結果是無法順利產生原始碼,每次都卡在找不到其他參考的 DLL 檔案。

ps. 對於經過混淆器「攪拌」過的 DLL,就不用費神反組譯了,除非裡面真有什麼非取得不可的寶物,值得花上幾天甚至幾星期的時間去鑽研。

《地下室手記》:宅男囈語

1/09/2010

書名:地下室手記
作者:杜斯妥也夫斯基
譯者:孟祥森
出版社:印刻
出版日期:2003 年 1 月

此譯本係依據 Constance Garnett 的英譯本,近幾年並沒有出比較新的譯本,有些用詞讀起來不太習慣。例如,當我看到「謝謝高特」時,還以為是某個姓高的人,再看到後面說「全能的高特」,才意會過來,這姓高的,便是 God。

這本書一個有趣的地方,就是神經質的獨白和假想的對話,以下摘錄部分內容。

書摘:

各位先生,我請你們找個時間去聽聽十九世紀有教養的人牙痛時發出的呻吟,聽聽在牙痛的第二天開始發出的呻吟。意即是說,不像第一天那樣,不僅是因為牙痛而發出......他的呻吟骯髒可厭的帶著惡意,並且日日夜夜繼續下去。他自己當然知道這種呻吟對自己毫無益處,他比任何人都清楚他是在毫無道理的折磨別人和他自己。他的家人,完全帶著一種厭惡在聽他,他們一點也不相信他真正需要這種呻吟,他們心裡都知道他可以用另一種不同的方式,更為單純的,不要尖叫,不要揮拳踢腿;他們都知道他現在這種呻吟僅僅是為了取樂自己,是出於情緒不良,是出於惡意。(p.19)

註:尊貴的上流社會人士,牙痛時也是一樣的哀嚎,甚至唉得更難聽。有些已經不是單純因為疼痛而哀嚎,其中帶有刻意(惡意?)的成分。

  「哈,哈,但是你知道實際上根本沒有甚麼叫做選擇的東西,不管你怎麼說」你會咯格笑著打斷我。「科學對於人的分析已經到達如此的程度,以致我們老早知道所謂選擇和自由意志僅不過是--」

  住口!先生,這個話讓我自己說。 (p.36)

註:叫自己想像的人住口 :)

我寫它的目的是什麼?如果我不是為了給別人看,我何必把這些偶然的事情寫在紙上,而不僅僅是心裡想想就算了?

  完全對;但是,寫在紙上比較神氣。寫在紙上有一個重要的意義:我可以評論一下自己,我可以改善我的文體。此外,由於把它寫出來,我或許實際上可以使自己輕鬆一下。(p.56)

由於我無限制的虛榮心,由於我為自己所訂下的過高的標準,我時常以極不滿意的眼光看待自己,幾乎已經到達厭惡的程度,於是內心裡我認為每個人對我都有這種感覺。 (p.60)

一個有教養、莊重的人不可能不給自己定下可怕的崇高標準,有些時候不可能不鄙視自己甚至憎恨自己。然而不論我鄙視他們或覺得他們比我強,每次我遇到任何人,總是把眼睛低下來。我甚至做過好多次實驗,看看我能不能面對面直視著別人,但我總是第一個把眼睛低下來。這幾乎使我煩惱得神經錯亂。......我根本是個病態敏感的人--像我們這個時代的人必然會成為這個樣子。(p.60-61)

註:宅男必練基本功:兩眼直視對方,看誰先躲開視線。

絕大部分時間我都留在家裡,看書。我試圖用外來的力量窒息一切不斷在我心中滋擾的東西。而我所具有的唯一方法就是讀書。當然,讀書給我很大的幫助--使我激動、快樂或痛苦。但有些時候它使我感到可怕的厭倦。那時候我甚麼都不想,只想活動活動,於是,我投入黑暗的,地下室的,最可厭最卑下的罪惡之中。我那不幸的熱情是銳利的、惱人的,是由我不斷的、病態的易怒性格產生出來的。......除了讀書之外我沒有任何消遣;這是說,我週遭的一切沒有一件東西使我尊敬,沒有一件東西吸引我。同樣,我被沮喪壓得透不過氣來;我神經質的渴望衝突和矛盾,因此我喜歡罪惡。我說這話並不是要證明自己有道理......不,我在說謊......我就是要證明自己有道理。我說這話完全是為了對自己有好處,先生,我不要說謊。我發誓不說謊。

  於是,到了晚上,我偷偷摸摸的,膽卻的,孤獨的縱情在那種骯髒的罪惡中,心裡從未離開過羞恥感;這種羞恥感即使在最可憎的時刻都不曾離開我,以致使我對它咒罵。在那個時候我心裡就早有了地下室,我極端懼怕被人闖見,被人認出來。 (p.66)

註:每個人心中都有一座斷背山地下室?

有一個軍官推我......我可以原諒別人用拳頭揍我,卻絕不能原諒把我推開而竟然沒有注意到我。....請你不要以為我躲避那個軍官是出於懦弱:我內心裡永遠不曾懦弱過,雖然我的行為總是使我成為一個懦夫。你別忙著笑--我可以保證能夠向你解釋。

  ......我躲開他並不是由於懦弱,而是由於無限的虛榮心。......我害怕的是在場的每個人......當我開始反抗並用斯文的語言爭辯的時候,他們都要開始嘲笑我,不瞭解我。

註:寧願被揍也不要被人忽視,欲找人尋仇把面子討回來,卻又怕被其他人嘲笑。下文有點長,故未摘錄,大意是他還是老想著討回面子,於是跟蹤那軍官好幾次,要查他的底細和幹過的齷齪事,最後還寫了封非常漂亮的決鬥信,向那名軍官下戰帖。結果這封信並未寄出,而他也因為沒有寄出這封信而感謝上帝,因為,每當他想到把信送給軍官的後果時,他就感到「有一股寒流通過背脊」,同時宣稱「我用最簡單的方式為自己復了仇,這完全是天才手法!」呵,還真天才。

有些假日,我經常沿著涅夫斯基大道有陽光的一邊散步......實際上不能說是散步,因為總有這麼多不愉快的屈辱與怨恨發生;但無疑地,這正是我想要的。我總是穿著最不像樣的服裝沿路蠕動,像一條鱔魚,隨時給將軍,騎兵軍官或女士們讓路,在這時候我心裡常常感到絞痛。......在全世界的眼睛裡我只是一隻蒼蠅,一隻骯髒的、可厭的蒼蠅。--當然,我比所有這些人都更聰明,心靈有更高度的發展,情感更為敏銳......為什麼我要去涅夫斯基?我不知道。我只覺得只要有機會我就被吸到那裡去。

註:精神被虐狂?自認比別人更聰明、高尚,又覺得別人都瞧不起自己,可是卻又不由自主的老是往人群裡擠,讓自己感受那精神上的屈辱與折磨。另外,這用鱔魚來形容一個人猥瑣的行走模樣,在《高老頭》裡面也有:「胖子西爾維立即上來報告女主人,說有個漂亮得不像良家婦女的姑娘,裝扮得神仙似的,穿著一雙毫無灰土的薄底呢靴,像鰻魚一樣從街上一直溜進廚房,問高里奧先生的房間在哪兒。」真是生動有趣的比喻。

技術提供:Blogger.
回頂端⬆️