Version Control with Subversion

Draft Revision 1411M

Ben Collins-Sussman

Brian W. Fitzpatrick

C. Michael Pilato

(TBA)


Table of Contents

前言
目標讀者
本書結構
排版慣例
本書是自由的
致謝
1. 簡介
什麼是 Subversion?
Subversion 的歷史
Subversion 的功能
安裝 Subversion
Subversion 的元件
用戶端元件 (供使用者使用)
伺服器元件 (供管理員使用)
2. 基本概念
檔案庫
各種版本控制的模型
檔案分享的問題
鎖定-修改-解鎖的解決方案
複製-修改-合併的解決方案
Subversion 實務
工作複本
修訂版本
工作複本如何追蹤檔案庫
混合修訂版的限制
摘要
3. 導覽
幫幫我!
匯入
修訂版: 數字, 關鍵字, 與日期. 我的天啊!
修訂版號
修訂版關鍵字
修訂版日期
最初的取出動作
基本工作流程
更新工作複本
對工作複本產生更動
檢視你的更動
svn status
svn diff
svn revert
解決衝突 (合併他人的更動)
手動合併衝突
將檔案複製並蓋過你的工作檔
棄踢: 使用 svn revert
送交更動
檢視歷史紀錄
svn log
svn diff
檢視本地端更動
比較檔案庫與本地複本
檔案庫與檔案庫之間的比較
svn cat
svn list
對歷史紀錄的最後叮嚀
其它有用的命令
svn cleanup
svn import
摘要
4. 分支與合併
何謂分支?
使用分支
建立一個分支
與分支共事
事情的內涵
在分支之間複製更動
複製特定的更動
重覆合併問題
合併整個分支
從檔案庫移除一個更動
切換工作複本
標記
建立一個簡單的標記
建立一個複雜的標記
分支維護
檔案庫配置
資料生命週期
摘要
5. Repository 管理
檔案庫的基本知識
了解異動與修訂版
無版本控制的性質
檔案庫的建立與設定
Hook scripts
Berkeley DB 設定
檔案庫維護
管理員的工具箱
svnlook
svnadmin
svnshell.py
Berkeley DB 工具
檔案庫善後
檔案庫回復
匯入檔案庫
檔案庫備份
網路檔案庫
httpd, Apache HTTP 伺服器
你需要什麼, 才能設定基於 HTTP 的檔案庫存取
基本 Apache 設定
權限, 認證, 以及授權
伺服器名稱與 COPY 要求
瀏覽檔案庫的 HEAD 修訂版
雜項的 Apache 功能
svnserve, 自訂的 Subversion 伺服器
設定匿名 TCP/IP 存取
設定使用 SSH 存取
使用哪一個伺服器?
檔案庫權限
新增專案
選擇一種檔案庫配置
建立配置, 匯入起始資料
摘要
6. 進階主題
執行時期的設定區域
設定區域配置
設定與 Windows 登錄檔
設定選項
Servers
Config
性質
為什麼要用性質?
使用性質
特殊性質
svn:executable
svn:mime-type
svn:ignore
svn:keywords
svn:eol-style
svn:externals
外部定義
供應商分支
通用供應商分支管理程序
svn-load-dirs.pl
7. Developer Information
Layered Library Design
Repository Layer
Repository Access Layer
RA-DAV (Repository Access Using HTTP/DAV)
RA-SVN (Proprietary Protocol Repository Access)
RA-Local (Direct Repository Access)
Your RA Library Here
Client Layer
Using the APIs
The Apache Portable Runtime Library
URL and Path Requirements
Using Languages Other than C and C++
Inside the Working Copy Administration Area
The Entries File
Pristine Copies and Property Files
WebDAV
Programming with Memory Pools
Contributing to Subversion
Join the Community
Get the Source Code
Become Familiar with Community Policies
Make and Test Your Changes
Donate Your Changes
8. 完整 Subversion 參考手冊
Subversion 命令列用戶端: svn
svn 選項
svn 子命令
svn add
svn cat
svn checkout
svn cleanup
svn commit
svn copy
svn delete
svn diff
svn export
svn help
svn import
svn info
svn list
svn log
svn merge
svn mkdir
svn move
svn propdel
svn propedit
svn propget
svn proplist
svn propset
svn resolved
svn revert
svn status
svn switch
svn update
svnadmin
svnadmin 選項
svnadmin 子命令
svnadmin list-unused-dblogs
svnadmin create
svnadmin dump
svnadmin help
svnadmin load
svnadmin lstxns
svnadmin recover
svnadmin rmtxns
svnadmin setlog
svnlook
svnlook 選項
svnlook author
svnlook cat
svnlook changed
svnlook date
svnlook diff
svnlook dirs-changed
svnlook help
svnlook history
svnlook info
svnlook log
svnlook proplist
svnlook tree
svnlook youngest
A. 給 CVS 使用者的 Subversion 指引
不同的修訂版號
目錄版本
更多不需網路的動作
區分狀態與更新
分支與標記
中介資料性質
衝突排解
二進制檔案與轉換
Versioned Modules
B. 匯入 CVS 檔案庫
需求
執行 cvs2svn.py
C. 故障排除
常見問題
使用 Subversion 的問題
每當我想要存取檔案庫時, 我的 Subversion 用戶端會停在那裡.
當我想要執行 svn 時, 它就說我的工作複本被鎖定了.
尋找或開啟檔案庫時有錯誤發生, 但是我確定我的檔案庫 URL 是正確的.
我要如何在 file:// URL 中指定 Windows 的磁碟機代號?
我沒有辦法經由網路寫入資料至 Subversion 檔案庫.
在 Windows XP 中, Subversion 伺服器有時會送出損壞的資料.
要在 Subversion 用戶端與伺服器進行網路傳輸的檢查, 最好的方法是什麼?
編譯 Subversion 的問題
我把執行檔編輯好了, 但是當我想要取出 Subversion 時, 我得到 Unrecognized URL scheme. 的錯誤.
當我執行 configure, 我得到像 subs-1.sed line 38: Unterminated `s' command 的錯誤.
我無法在 Windows 以 MSVC++ 6.0 來編譯 Subversion.
D. WebDAV 與自動版本
基本 WebDAV 概念
簡易 WebDAV
DeltaV 擴充
Subversion 與 DeltaV
將 Subversion 對映至 DeltaV
自動版本支援
mod_dav_lock 的替代品
自動版本互通性
Win32 網路資料夾
Mac OS X
Unix: Nautilus 2
Linux davfs2
E. 其它 Subversion 用戶端
Out of One, Many
F. 協力廠商工具
ViewCVS
SubWiki
Glossary

List of Figures

2.1. 典型的主從式系統
2.2. 應避免的問題
2.3. 鎖定-修改-解鎖的解決方案
2.4. 複製-修改-合併的解決方案
2.5. …複製-修改-合併的解決方案 (續)
2.6. 檔案庫的檔案系統
2.7. 檔案庫
4.1. 發展的分支
4.2. 起始檔案庫的配置
4.3. 有新複本的檔案庫
4.4. 檔案歷史的分支
5.1. 一種建議的檔案庫配置.
5.2. 另一種建議的檔案庫配置.
7.1. Subversion's "Big Picture"
7.2. Files and Directories in Two Dimensions
7.3. Revisioning Time—the Third Dimension!

List of Tables

2.1. 檔案庫存取的 URL
7.1. A Brief Inventory of the Subversion Libraries
E.1. Subversion 的圖形用戶端

List of Examples

5.1. 利用 svnshell, 在檔案庫之中巡行
5.2. txn-info.sh (回報未處理異動)
5.3. 使用漸進式檔案庫傾印
6.1. 登錄項目 (.REG) 檔案的範例.
7.1. Using the Repository Layer
7.2. Using the Repository Layer with Python
7.3. A Simple Script to Check Out a Working Copy.
7.4. Contents of a Typical .svn/entries File
7.5. Effective Pool Usage

前言

如果 C 給了你夠多的繩子來吊死自己, 那麼 Subversion 可視為是一種收納繩子的器具. ”—Brian Fitzpatrick

在開放原碼軟體的世界中, Concurrent Versions System (CVS)長久以來, 一直都是版本控制的不二選擇. CVS本身是自由軟體, 而且它是 “非鎖定式” 的系統 —這讓分布廣闊的程式設計人員能夠分享彼此的工作— 完全符合開放原碼世界的合作模式. CVS, 以及它那半混亂式的發展模式, 已經成為開放原碼文化的基石.

但是就像許多的工具, CVS 已經開始顯露疲態. 比較起來, Subversion 是一個新的工具, 是設計來成為 CVS 的後繼者. 設計者要以兩個方法來贏得 CVS 使用者的心: 產生一個設計 (還有 "外觀與感覺") 類似 CVS 的開放原碼系統, 以及試著修正 CVS 中最廣為人知的缺點. 雖然結果不見得會是版本控制設計的下一個偉大革命, 但是 Subversion 絕對 會是個強力, 可用性高, 而且深具彈性的工具.

目標讀者

本書是寫給那些想要以 Subversion 來管理資料的電腦使用者. 雖然 Subversion 可以在許多不同的作業系統上執行, 不過主要的使用界面還是命令列. 由於這樣的原因, 本書的例子都假設使用者使用的是類似 Unix 的作業系統, 而且熟悉 Unix 與其命令列的界面.

大多數的使用者可能是程式設計師或系統管理員, 需要追蹤原始碼的的變更; 這是 Subversion 最常見的用法, 因此也是本書例子的情景. 但是請記住, Subversion 可以用來管理任何類似的資訊: 圖形, 音樂, 資料庫, 文件等等. 對 Subversion 而言, 所有的資料就只是資料而已.

雖然本書撰寫時, 我們假設讀者都沒有使用過版本控制軟體, 但是我們也試著讓 CVS 的使用者能夠很快地上手. 一些 sidebar 會隨時出現討論 CVS, 而且也有一章附錄用來概述 CVS 與 Subversion 之間的不同.

本書結構

本書的前三章對 Subversion 有概括性的介紹. 我們一開始先介紹 Subversion 的功能, 討論它的設計與使用者模型, 然後進行一個導引. 不管是否有相關的經驗, 所有的讀者都應該讀這幾章的內容. 它們是本書其它章節的基礎.

第四, 五, 六章討論比較複雜的議題, 像是分支, 管理檔案庫 (repository), 以及進階的功能, 像是性質, 外部定義, 以及存取控制. 系統管理員與進階使用者絕對會希望讀這幾章的.

第七章是特別寫給那些想要在他們的軟體裡使用 Subversion 的 API, 或者是想要修改 Subversion 的程式設計師.

本書最後以參考資料結束: 第八章是所有 Subversion 命令的參考指南, 而附錄則涵蓋了許多有用的主題. 這幾章大概是你在讀完本書之後, 最常使用的部份.

排版慣例

### O'Reilly almost certainly needs to fill this in, depending on how they typeset the book.

請注意這裡使用的程式碼例子, 就只是—單純的例子. 在適當的編譯器設定之下, 它們都能夠正常被編譯, 但是只是用來示範手邊的問題而已, 而不是用來作為程式設計的範例.

本書是自由的

本書起初只是 Subversion 計畫的發展人員所寫的文件而已, 後來就成為獨立的工作, 並且重新改寫. 因為如此, 這個文件也和 Subversion 一樣, 使用的是自由的開放原碼授權. 事實上, 本書是在公眾面前寫成的, 是 Subversion 的一部份. 這意味著兩件事:

  • 你可以在 Subversion 的源碼樹中, 找到本書的最新版本.

  • 你可以任意散佈、修改本書 — 它用的是自由授權. 當然了, 除了散佈自己私有的版本, 我們比較希望你能夠將回應與修正送回給 Subversion 開發者社群. 請參見 the section called “Contributing to Subversion”, 以了解如何加入本社群.

You can send publishing comments and questions to O'Reilly here: ###insert boilerplate.

http://svnbook.red-bean.com 可以找到還滿新的線上版本.

致謝

### Huge list of thanks to the many svn developers who sent patches/feedback on this book.

### Also, individual-author acknowledgements to specific friends and family.

Chapter 1. 簡介

版本控制是管理資訊變化的技術. 對於程式設計者來說, 它已經成為不可或缺的工具, 他們常常將花時間修改軟體, 產生部份的變更, 然後第二天再取消所作的變更. 想像有一群程式設計人員同時工作的情況, 你就能夠理解, 為什麼需要一個良好的系統來管理可能的混亂.

什麼是 Subversion?

Subversion 是一個自由/開放源碼的版本控制系統, 也就是說 Subversion 管理著隨時間改變的檔案. 這些檔案放置在一個中央 檔案庫 (repository) 中. 這個檔案庫 很像一個尋常的檔案伺服器, 不過它會記住每一次檔案的變動. 這樣你就可以把檔案回復到舊的版本, 或是瀏覽檔案的變動歷程. 許多人會把版本控制系統想像成某種 “時光機器”.

某些版本控制系統也是 software configuration management (SCM) 系統. 這些系統是特別設計來管理大量程式碼的, 而且具有許多功能, 專門用在軟體發展之用 — 像是可完全了解程式語言, 或是提供編譯軟體的工作. 不過 Subversion 並不是這樣的系統; 它是一個泛用系統, 可用來管理任何 類型的檔案, 其中包括了程式源碼.

Subversion 的歷史

在 1995 年時, Karl Fogel 與 Jiim Blandy 成立了 Cyclic Software, 提供 Concurrent Versions System (CVS) 的商業支援, 並著手改良它. Cyclic 作出了第一個具網路功能的 CVS 公開版本 (由 Cygnus 軟體公司捐贈). 在 1999 年, Karl Fogel 出版了一本書, 講的是 CVS, 以及它所促成的開放源碼發展模式. Karl 與 Jim 很早前就提過, 要製作一個 CVS 的取代軟體的概想; Jim 甚至還起草了一個新的, 理論性的 檔案庫設計, 而且還想到了一個不錯的計劃名稱. 最後, 在 2000 年二月, CollabNet (http://www.collab.net) 的 Brian Behlendorf 提供 Karl 全職的工作, 專職發展 CVS 的替代程式. Karl 集合了一個團隊, 於五月開始發展. 由於 Subversion 是以自由授權撰寫的, 它很快就吸引了一堆發展人員.

Subversion 的原始設計團隊定下了幾個簡單的目標. 他們決定它必須在功能上可取代 CVS. 也就是說, 所有 CVS 可達成的事, 它都要能夠作到. 在修正最顯而易見的瑕疵的同時, 還要保留相同的發展模式. 還有, Subversion 應該要和 CVS 很相像, 任何 CVS 使用者只要花費少許的力氣, 就可以很快地上手.

經過十四個月的撰寫之後, Subversion 於 2001 年 8 月 31 號開始 “自行管理”. 也就是說, 發展人員不再使用 CVS 來管理 Subversion 的程式碼, 而以 Subversion 自己來管理.

雖然起始這個計畫, 與提供大部份成果的資金都歸功於 CollabNet (它付出幾位全職 Subversion 開發人員的薪水), 這還是個開放源碼計畫, 由一般開放源碼界所公認的規則所支配. CollabNet 擁有程式碼的版權, 不過程式碼是以 Apache/BSD 風格的版權發行, 完全符合 Debian Free Software Guidelines. 換句話說, 每個人都可以隨意地自由下載、修改、以及重新散播 Subversion; 完全不需要經過 CollabNet, 或是任何人的允許.

Subversion 的功能

Subversion 哪裡比 CVS 的設計更好? 這裡是個簡短的列表, 以滿足你的好奇心. 如果你不熟悉 CVS 的話, 可能不了解這些特色在哪裡. 別害怕: 第二章會提供你版本控制的簡單介紹.

目錄版本控制

CVS 只能追蹤單獨檔案的歷史, 不過 Subversion 實作了一個 “虛擬” 的版本控管檔案系統, 能夠依時間追蹤整個目錄的更動. 目錄檔案都被納入版本控管. 最後, 用戶端有真正可用的 move (移動) 與 copy 指令.

不可分割的送交

一個送交動作, 不是導致所有更動都送入檔案庫, 就是完全不會送入. 這讓發展人員以邏輯區段建立更動, 並送交更動.

納入版本控管的描述資料 (Meta-data)

每一個檔案與目錄都附有一組隱形 “性質 (property)”. 你可以自己發明, 並儲存任何你想要的鍵值對. 性質是隨著時間來作版本控管的, 就像檔案內容一樣.

選擇不同的網路層

Subversion 有抽象的檔案庫存取概念, 可以讓人很容易地實作新的網路機制. Subversion “先進” 的網路伺服器, 是 Apache 網頁伺服器的一個模組, 它以稱為 WebDAV/DeltaV 的 HTTP 變體協定與外界溝通. 這對 Subversion 的穩定性與互通性有很大的幫助, 而且額外提供了許多重要功能: 舉例來說, 有身份認證, 授權, 線上壓縮, 以及檔案庫瀏覽. 另外也有小而獨立的 Subversion 伺服器程式, 使用的是自訂的通訊協定, 可以很容易地透過 ssh 以 tunnel 方式使用.

一致的資料處理方式

Subversion 使用二進制差異運算法, 來表示檔案的差異, 它對文字 (人類可理解的) 與二進制檔案 (人類無法理解) 兩類的檔案都一視同仁. 這兩類的檔案都同樣地以壓縮形態儲存在檔案庫中, 而且檔案差異是以兩個方向在網路上傳送的.

更有效率的分支 (branch) 與標記 (tag)

分支與標記的花費並不必一定要與計畫大小成正比. Subversion 建立分支與標記的方法, 就只是複製該計畫, 使用的方法就像 hard-link 一樣. 所以這些動作只會花費很小, 而且是固定的時間.

Hackability

Subversion 沒有任何的歷史包袱; 它主要是一群共用的 C 程式庫, 具有定義完善的 API. 這使得 Subversion 便於維護, 並且可被其它應用程式與程式語言使用.

安裝 Subversion

Subversion 建立在一個可移殖的 layer, 稱為 APR (Apache Portable Runtime 程式庫) 上. 這表示 Subversion 應該可以在任何可以執行 Apache 的 httpd 伺服器的作業系統上: Windows, Linux, 所有的 BSD 分支, Mac OS X, Netware, ... 等等.

取得 Subversion 最簡單的方式, 就是下載為你的作業系統所編譯的二進制套件. Subversion 的網站 (http://subversion.tigris.org) 常常有自願者提供的, 可供下載的可執行檔. 網站上, 也常有供微軟作業系統使用者使用的圖形介面安裝套件. 如果你使用的是 Unix 系的作業系統, 你可以使用系統內定的套件發行系統 (rpm, deb, ports), 來取得 Subversion.

另外, 你也可以直接從源碼建立 Subversion. 你可以從網站上, 下載最新的源碼發行檔. 解開之後, 請遵循 INSTALL 檔案裡的說明, 把它建立起來. 請注意發行的源碼套件中, 包含了所有你需要建立命令列用戶端, 可與遠端檔案庫溝通的套件 (尤其是 apr, apr-util, 以及 neon 程式庫). 但是 Subversion 還有許多其它相依套件, 像是 Berkeley DB, 另外 Apapche httpd 也是個可能性. 如果你想要建立一個 “完整的” 的編譯, 請確定你具備了所有記載在 INSTALL 裡的套件. 如果你計劃要與 Subversion 工作, 你可以使用你的用戶端程式, 抓取最新的, 流血前線的源碼. 這記載在 the section called “Get the Source Code” 內.

Subversion 的元件

安裝好之後, Subversion 會有數個不同的部份. 以下是你取得的程式的快速綜覽.

用戶端元件 (供使用者使用)

svn

命令列用戶端程式. 這是用來管理資料的主要工具, 在第 2, 3, 4, 以及第 6 章有詳細說明.

svnversion

用來回報工作複本的混合版本狀態. (請參考 Chapter 2, 基本概念, 以了解混合版本的工作複本.)

伺服器元件 (供管理員使用)

這些會在 Chapter 5, Repository 管理 中討論.

svnlook

用來檢閱 Subversion 的檔案庫的工具.

svnadmin

用來調整與修整 Subversion 的檔案庫的工具.

mod_dav_svn

給 Apache-2.X 網頁伺服器使用的外掛模組; 可以用來將你的檔案庫透過網路對外開放, 以供他人進行存取。

svnserve

一個獨立的伺服器程式, 可以作為伺服器行程執行, 或是被 SSH 啟動; 另一個讓你的檔案庫在網路上可供其他人存取的方法.

假設你已經正確地安裝 Subversion, 你應該可以開始使用了. 接下來的兩章, 我們會帶領你涵蓋 Subversion 的命令列用戶端程式 svn 的使用.

Chapter 2. 基本概念

本章對 Subversion 有簡短與非正式的描述. 如果你是版本控制的新手, 本章相當適合你. 我們一開始會討論一般性的版本控制, 慢慢導向 Subversion 背後的概念, 並且利用 Subversion 舉出簡單的例子.

即使本章都以人們共同一群程式源碼來作為例子, 但是請記住 Subversion 可用來管理任何種類的檔案 — 並非僅僅侷限在幫助程式設計師而已.

檔案庫

Subversion 是一個用以分享資訊的中央系統, 核心為 檔案庫 (repository), 作為儲存資料的集散地. 檔案庫儲存資料的形式是 檔案系統樹 (filesystem tree) — 也就是典型的目錄與檔案的架構. 許多的 用戶端 會先連上檔案庫, 然後對這些檔案作讀取或寫入的動作. 藉由寫入資料, 一個用戶端能夠讓資訊為他人所用; 藉由讀取資料, 該用戶端能夠擷取他人的資訊.

Figure 2.1. 典型的主從式系統

典型的主從式系統

為什麼這樣會很有趣? 到目前為止, 這些聽起來就像一個典型的檔案伺服器. 事實上, 檔案庫 就是 一種檔案伺服器, 但是與你所見的不太相同. 讓 Subversion 檔案庫如此不同的原因, 在於 它會記住所有的更動: 每個檔案的每一個更動, 甚至是每一個目錄所作的更動, 像是目錄與檔案的新增, 刪除, 以及重新編排.

當一個用戶端自檔案庫讀取資料時, 它通常只會看到最新版本的檔案系統樹. 但是用戶端也可以看到早先的檔案系統. 舉例來說, 用戶端可以詢問歷史性的問題, 像是 "上個星期三, 這個目錄裡有什麼東西?", 或 "誰是最後一個更動這個檔案的人, 而且作了哪些更動?" 這就是任何 版本控制系統 的核心問題: 記錄並追蹤隨著時間對資料所作的更動.

各種版本控制的模型

檔案分享的問題

所有的版本控制系統都必須解決同樣的基本問題: 如何讓使用者分享資料, 但是不讓他們不小心成為彼此的阻礙? 使用者要不小心覆寫掉彼此在檔案庫裡的更動, 實在是太容易了.

考慮一下以下的情景: 假設我們有兩個協同工作人員, Harry 與 Sally. 他們決定同時編輯同一個儲存在檔案庫的檔案. 如果 Harry 先存入檔案庫, (幾個月之後) Sally 很有可能以她自己的新版檔案覆寫過去. 雖然 Harry 的版本並不會就此消失 (因為系統記得每一次的更動), 但是任何 Harry 所作的更動不會出現在 Sally 的新版檔案中, 因為她打從一開始就沒看過 Harry 的更動. 總地來說, Harry 的心血就這麼消失了—至少從該檔案的最新版就遺漏掉了—. 這絕對是我們想要避免的情況!

Figure 2.2. 應避免的問題

應避免的問題

鎖定-修改-解鎖的解決方案

許多版本控制系統以鎖定-修改-解鎖的方式 來解決這個問題, 這是個很簡單的解決方案. 在這樣的系統中, 檔案庫在同一時間只允許一個人修改一個檔案. 首先 Harry 必須在他開始更動之前, 先 "鎖定" 該檔案. 鎖定檔案就像從圖書館借書; 如果 Harry 已鎖定一個檔案, 那麼 Sally 就無法對其進行任何更動. 如果她想要鎖定該檔案, 檔案庫會拒絕她的要求. 她所能作的, 就只是讀取這個檔案, 然後等著 Harry 完成他的更動, 解除他設下的鎖定. 在 Harry 解除該檔案的鎖定之後, 他的回合就結束了, 現在輪到 Sally 可以對其進行鎖定並編輯內容.

Figure 2.3. 鎖定-修改-解鎖的解決方案

鎖定-修改-解鎖的解決方案

鎖定-修改-解鎖模型的問題, 在於它的限制多了點, 而且經常會成為使用者的絆腳石:

  • 鎖定可能會造成管理上的問題. 有的時候 Harry 在鎖定一個檔案之後, 然後就忘了這件事. 在此同時, 由於 Sally 還在等著編輯這個檔案, 她什麼事也不能作, 然後 Harry 就跑去休假了. 現在 Sally 必須找個管理員, 才能解除 Harry 的鎖定. 這樣狀況, 最後導致了許多不需要的延遲與時間的浪費.

  • 鎖定可能造成不必要的工作瓶頸 如果 Harry 編輯的是文字檔的開頭部份, 而 Sally 只是要修改同一檔案的結尾部份呢? 這樣的更動完全不會重疊在一起. 他們可以同時修改同一個檔案, 而不會造成任何的傷害, 只要這些更動適當地合併在一起. 像這樣的情況, 他們並不需要輪流進行才能完成.

  • 鎖定可能會造成安全的假象 假設 Harry 鎖定並編輯檔案 A, 同時 Sally 鎖定並編輯檔案 B. 但是假設 A 與 B 彼此相依, 而對各檔案所作的修改是完全不相容的語意. 突然之間 A 與 B 彼此無法執行. 鎖定系統完全無法避免這樣的狀況 —但是它在某種程度上提供了一種安全的假象. 它很容易讓 Harry 與 Sally 認為藉由鎖定檔案, 每個人都有個好的開始, 工作互不干涉, 如此讓他們不會在早期就討論他們互不相容的更動

複製-修改-合併的解決方案

Subversion, CVS, 還有其它的版本控制系統使用一種 複製-修改-合併的模型, 作為鎖定的取代方法. 在這種模型下, 每一個使用者用戶端會讀取檔案庫, 然後建立檔案或計畫的 工作複本. 然後使用者進行各自的工作, 修改他們自己的私有複本. 最後, 私有複本會合併在一起, 以產生一個新的, 最後的版本. 版本控制系統通常會協助合併的動作, 但是最後人類還是負有讓它能夠產生正確結果的責任.

這裡舉個例子. 假設 Harry 與 Sally 各自從檔案庫 建立同一個專案的工作複本. 他們同時工作, 並對同一個複本中的檔案 "A" 作了修改. Sally 先將她的更動送回檔案庫. 當 Harry 試著要存回他的更動時, 檔案庫會通知他, 他的檔案 A 已經 過時. 換句話說, 檔案庫裡的檔案 A 在他上次產生複本後已經更動過了. 所以 Harry 要求他的用戶端程式, 將檔案庫裡新的更動與他的檔案 A 工作複本 合併 起來. 通常 Sally 的更動與他的更動不會重疊; 所以只要讓兩組更動整合在一起, 他就可以將他的工作複本回存至檔案庫.

Figure 2.4. 複製-修改-合併的解決方案

複製-修改-合併的解決方案

Figure 2.5. …複製-修改-合併的解決方案 (續)

…複製-修改-合併的解決方案 (續)

要是 Sally 的更動真的與 Harry 的更動重疊的話呢? 這該怎麼辦呢? 這種情況稱為 衝突 (conflict), 通常也不會是多大的問題. 當 Harry 要求用戶端程式將最新的檔案庫更動, 合併到他自己的工作複本時, 他自己的檔案 A 會被標示為衝突的狀態: 他可以看到兩邊互相衝突的更動, 然後手動在這兩者之間作選擇. 請注意, 軟體不會自動地解決衝突; 只有人類才有理解能力, 進而作出明智的決定. 當 Harry 手動地解決重疊的更動之後 (也許就是和 Sally 討論這個衝突!), 他就可以安全地將手動合併的檔案存回檔案庫.

複製-修改-合併模型聽起來有點混亂, 但是實務上跑起來可是相當地平順. 使用者可以同時各自工作, 不需等待他人. 當他們同時處理同一個檔案時, 大多數的情況下, 這些同時產生的更動都不會互相重疊; 衝突是相當少見的. 而且解決衝突所需要的時間, 也遠低於鎖定系統所損失的時間.

最後, 這些通通都回歸到一個最重要的因素: 使用者溝通. 當使用者彼此溝通不良時, 語法與語意衝突都會增加. 沒有任何系統能夠強迫使用者完美地溝通, 而且也沒有任何系統能夠偵測出語意衝突. 所以並沒有任何論點, 能夠導出鎖定系統可減少衝突發生的虛假承諾; 實務上, 鎖定系統似乎比其它系統對生產力有更大的傷害.

Subversion 實務

工作複本

你已經讀過有關工作複本的部份; 現在我們要示範 Subversion 如何建立並使用它.

Subversion 工作複本就只一個普通的目錄樹, 位於你的本地系統中, 其中包含了一堆檔案. 你可以依你喜好, 自由地編輯這些檔案. 而且如果這些是源碼檔, 你可以依一般的方法編譯它們. 你的工作複本就是你自己的私有工作空間: Subversion 不會將其它人的更動合併進來, 也不會讓你的更動讓他人取得, 除非你明確地要求要這樣作.

你在自己的工作複本檔案中作了一些更動, 並且確認它們都能正常地工作, 此時 Subversion 提供你許多命令, 讓你將這些更動 "發表" 給同一計畫的其他人使用 (藉由寫至檔案庫). 如果別人發表了他們的更動, Subversion 提供你許多命令, 可將這些更動合併至你的工作目錄中 (藉由讀取檔案庫).

工作複本也包含了其它額外的檔案, Subversion 建立並維護這些檔案, 讓它自己可以執行這些命令. 特別一提, 工作複本中的每個目錄都會有一個名為 .svn 的子目錄, 也就是工作複本的 管理目錄. 每個工作目錄中的檔案, 都能夠幫助 Subversion 了解哪些檔案有未出版的更動, 哪些與其他人的工作相比是過時的檔案.

一個典型的 Subversion 檔案庫通常會包含數個專案使用的檔案 (或源碼檔); 一般來講, 每一個專案是檔案庫檔案樹中的子目錄. 在這樣的安排中, 一個使用者的工作複本, 通常對應到檔案庫裡的某一特定的子目錄.

舉例來說, 假設你有一個包含兩個軟體專案的檔案庫.

Figure 2.6. 檔案庫的檔案系統

檔案庫的檔案系統

換句話說, 檔案庫的根目錄有兩個子目錄: paintcalc.

要取得一個工作複本, 你必須 取出 (check out) 某個檔案庫裡的子目錄. ("check out" 聽起來有點像是要鎖定或預約某項資源, 但是它不是; 它就只是為你建立一個計畫的私有複本.) 舉個例子, 如果你取出 /calc, 你會得到一個像這樣的工作複本:

$ svn checkout http://svn.example.com/repos/calc
A  calc
A  calc/Makefile
A  calc/integer.c
A  calc/button.c

$ ls -a calc
Makefile  integer.c  button.c  .svn/

這一串字母 A, 表示 Subversion 已經加入幾個物件到你的工作複本之中. 現在你有檔案庫的 /calc 目錄的個人複本, 外加一些額外的東西—.svn— 這裡面有 Subversion 所需的額外資訊, 早先有提到過.

假設你更動了 button.c. 由於 .svn 目錄會記得檔案的修改日期與原始的內容, Subversion 能夠知道你改了這個檔案. 但是 Subversion 不會讓你的更動公諸於世, 除非你明確地表明要這麼作. 發表你的更動的行為, 通常稱為 送交 (或 存入 (check in)) 更動至檔案庫.

要發表你的更動讓其他人知道, 可以使用 Subversion 的 commit 命令:

$ svn commit button.c
Sending button.c
Transmitting file data..
Committed revision 57.

現在對 button.c 所作的更動, 已經送交至檔案庫了; 如果其他使用者取出 /calc 的工作複本, 他們會在最新版的檔案中看到你的更動.

假設你有個合作伙伴 Sally, 他和你同一時間取出 /calc 的工作複本. 當你送交你對 button.c 的更動, Felix 的工作複本並沒有改變; Subversion 只會依使用者要求來變更工作複本.

要讓她的專案也能跟上變動, Sally 可以藉由使用 Subversion 的 update 命令, 要求 Subversion 更新 他的工作複本. 這會讓你的更動合併到她的工作複本中, 外加他人自上次取出檔案以後所送交的更動.

$ pwd
/home/sally/calc

$ ls -a 
.svn/ Makefile integer.c button.c

$ svn update
U button.c

以上取自svn update命令的輸出, 表示 Subversion 更新了 button.c 的內容. 請注意 Felix 不必指定要更新哪個檔案; Subversion 使用 .svn 目錄與檔案庫裡的額外資訊, 來決定哪些檔案必須更新到最新版.

修訂版本

svn commit的動作, 可以將不限數目的檔案與目錄的更動, 視為單一不可分割的異動以進行發表. 在你的工作複本中, 你可以修改檔案的內容, 建立, 刪除, 更名, 以及複製檔案與目錄, 然後將所有的更動視為一個單位, 一口氣送交回去.

在檔案庫中, 每一次的送交都被視為是一個不可分割的異動: 不是所有送交的更動都成功, 就是全部都不成功. Subversion 會試著維持這樣的不可分割特性, 不管是遭遇程式失敗, 系統當機, 網路有問題, 還是其它使用者進行的動作.

每一次檔案庫接受一個送交的更動, 就會讓檔案樹進入一個新的狀態, 稱之為 修訂版本. 每一個修訂版本都會被賦與一個唯一的, 比前一個修訂版號大一的自然數. 一個新建立的檔案庫的修訂版號為零, 其中除了空的根目錄外, 什麼都沒有.

有一個把檔案庫具象化的好方法, 就是將之視為一系列的樹. 想像有一系列的修訂版號, 自左至右, 由 0 開始. 每一個修訂版號下都有一個檔案系統的樹狀結構, 每一個檔案樹都是每一次送交之後的檔案庫 “快照”.

Figure 2.7. 檔案庫

檔案庫

請特別注意, 工作複本並不見得一定會符合檔案庫某一特定的修訂版; 每個檔案可能會對應到不同的修訂版本. 舉個例子, 假設你的工作目錄, 是從檔案庫登出了 4 號修訂版:

calc/Makefile:4
     integer.c:4
     button.c:4

此時工作目錄對應的是檔案庫的 4 號修訂版. 不過要是你修改了 button.c, 送交這個更動. 假設此時其它人都沒有送交任何更動, 你所送交的更動就會變成檔案庫的第 5 號修訂版, 然後你的工作複本就會變成像這樣:

calc/Makefile:4
     integer.c:4
     button.c:5

假設在這個時候, Sally 送交了 integer.c 的更動, 產生了 6 號修訂版. 如果你以 svn update 更新你的工作目錄, 它看起來就會像這樣:

calc/Makefile:6
     integer.c:6
     button.c:6

Sally 對 integer.c 的更動會出現在你的工作複本中, 而你的更動還是在 button.c 之中. 在這個例子中, 版本 4, 5, 6 的 Makefile 內容都是一樣的, 不過 Subversion 會將工作複本中的 Makefile 標示為版本 6, 表示它還是最新版本. 所以, 在你對工作複本作了一次完整更新之後, 它基本上就是完全對應到檔案庫的某一個版本.

工作複本如何追蹤檔案庫

對每個工作目錄中的檔案, Subversion 會在管理區域 .svn/ 中, 記錄兩個重要的資訊:

  • 工作檔案的基本修訂版本 (亦稱為檔案的 工作版本), 以及

  • 時間戳記, 記錄本地複本最近一次被檔案庫更新的時間.

有了這些資訊, 再藉由諮詢檔案庫, Subversion 就可以決定某個工作檔案是處於下列四個狀態何者之一:

未更動, 現行版本

本檔案在工作目錄中未被更動, 而且自工作版本之後, 也沒有任何該檔案的更動被送交回去. 對它執行 svn commit 不會發生任何事, 執行 svn update 也不會發生任何事.

本地修改, 現行版本

這個檔案在工作目錄中被修改過, 而自其基礎修訂版號後, 也沒有任何更動送交回檔案庫. 由於有尚未送交回去的本地端修改, 所以對它的 svn commit 會成功地發表你的更動, 而 svn update 則不會作任何事.

未更動, 過時版本

這個檔案在工作目錄中並未更動, 但是檔案庫已被更動. 本檔案應該要更新, 以符合公開修訂版. 對它的 svn commit 不會發生任何事, 而 svn update 會讓工作目錄中的檔案更新至最新版本.

本地修改, 過時版本

這個檔案在工作目錄與檔案庫都受到了更動. 對它執行 svn commit 會產生 "out-of-date" 錯誤. 這個檔案應該要先被更新; svn update 會試著將已發表的更動, 與本地的更動合併在一起. 如果 Subversion 無法自動無誤地完成它, 那麼就會留給使用者, 讓他來解決這個衝突.

聽起來好像要注意很多東西, 但是 svn status 的命令會顯示任何在工作複本裡的項目的狀態. 欲取得該命令更詳細的資訊, 請參見 the section called “svn status”.

混合修訂版的限制

Subversion 的一個基本原則, 就是要盡量地保有彈性. 有一種特別的彈性, 就是工作複本可包含混合修訂版的能力.

一開始可能無法理解, 為什麼這樣的彈性會被視為一項特色, 而不是責任. 在完成至檔案庫的送交動作後, 剛送交的檔案與目錄的修訂版, 會比工作複本其它部份的修訂版還要新. 這看起來有點混亂. 就像我們早先示範過的, 我們永遠都可藉由 svn update, 將工作複本帶回至單一的工作修訂版. 為什麼會有人要 故意 混合不同的工作修訂版呢?

假設你的專案夠複雜, 你發現有時強制將工作複本的某些部份帶回到 “舊版本” 反而更好; 在第 3 章, 你會學到怎麼達成. 也許你想要對早先版本的子模組進行測試, 也許你想要在最新的檔案樹中, 檢視某個檔案幾個過去的版本.

但是, 當你在工作複本中使用混合修訂版時, 這項彈性有幾個限制.

首先, 如果檔案或目錄不全都是最新版本時, 對它們的刪除動作是無法送交的. 如果一個項目在檔案庫中, 存在著比目前更新的版本, 你想要送交的刪除動作會被拒絕, 以防止你不小心毀了還沒看過的更動.

第二, 你無法送交一個對目錄的描述資訊更動, 除非它完全是最新版本的. 你會在第 6 章學到如何將 “性質” 附加到項目. 一個目錄的工作修訂版, 會定義出特定的實體與性質的集合, 因此送交一個對過時目錄的性質更動, 很有可能會毀掉你還沒檢視過的性質.

摘要

本章涵蓋了幾個 Subversion 的基本概念:

  • 我們介紹了中央檔案庫, 用戶端工作複本, 以及檔案庫修訂版樹的陣列.

  • 本章示範幾個例子, 說明兩個協同工作者如何利用 '複製-修改-合併' 模式, 透過 Subversion 來發表並接收彼此所作的更動.

  • 我們也談了一些有關 Subversion 如何追蹤與管理工作複本的資料.

現在, 你應該對 Subversion 如何工作有個清楚的概念. 有了這樣的知識, 你現在已經準備好進行到下一章, 對 Subversion 的命令與功能來個詳細的巡禮.

Chapter 3. 導覽

現在我們將詳細講解如何使用 Subversion. 當你看完這一章後, 應該就能夠進行每日會用到的功能. 一開始會先取出程式碼, 更動程式, 然後檢視這些更動. 你也會看到如何將別人的更動, 取回至自己的工作複本. 檢視這些更動, 然後處理可能發生的衝突.

請注意本章並不打算列出所有 Subversion 的命令—更確切地說, 它只是個對話式的介紹, 講解最常遇到的 Subversion 工作. 本章假設你已經讀過並了解 Chapter 2, 基本概念, 也熟悉 Subversion 的基本模型. 想要取得所有命令列表, 請參照Chapter 8, 完整 Subversion 參考手冊.

幫幫我!

在繼續下去之前, 以下是你在使用 Subversion 時, 最重要、最常用到的命令: svn help. Subversion 的命令列用戶端提供自己的說明文件 —在任何時間, 只要打 svn help <子命令>, 就會有 子命令 的文件, 選項, 以及運作方式的說明.

匯入

你可使用 svn import 來匯入一個新的計畫至 Subversion 的檔案庫中. 雖然在設定你的 Subversion 伺服器時, 這可能是你第一件作的事, 但是它很少會使用到. 欲取得更詳細的匯入功能說明, 請參見 本章稍後的 the section called “svn import”.

修訂版: 數字, 關鍵字, 與日期. 我的天啊!

在我們繼續下去之前, 你應該要知道如何指定檔案庫中的特定修訂版. 如你在 the section called “修訂版本” 學到的, 修訂版是檔案庫在某一特定時間的 “快照”. 只要你會繼續送交更動, 讓檔案庫成長下去, 你就需要一個指定這些快照的機制.

使用 --revision (-r) 選項, 再加上你想要的修訂版號 (svn --revision REV), 就可以指定修訂版, 或是以分號隔開兩個修訂版號 (svn --revision REV1:REV2), 就可以指定一個修訂版範圍. Subversion 還可以讓你以數字, 關鍵字, 或是日期來指定這些修訂版號.

修訂版號

當你建立了一個新的 Subversion 檔案庫, 它就以修訂版號 0 開始它的生命. 其後的送交動作, 都會讓修訂版號加一. 在你的送交動作完成後, Subversion 用戶端會告知你新的修訂版號:

$ svn commit --message "Corrected number of cheese slices."
Sending        sandwich.txt
Transmitting file data .
Committed revision 3.
      

在未來的某一時間, 如果你想參考到這個版號時 (稍後在本章, 我們會看看如何, 以及為什麼我們會想這樣作), 你可以以 “3” 來指定它.

修訂版關鍵字

Subversion 用戶端懂得幾個 修訂版關鍵字. 這些關鍵字可在 --revision 選項中, 用來取代整數引數, 它們將會被 Subversion 轉換成特定的修訂版號:

Note

每一個工作複本的目錄內, 都有一個 .svn 管理區域. Subversion 會在管理區域中, 存放目錄裡每一個檔案的複本. 這個複本是你上次進行更新時, 所取得的修訂版 (稱為 “BASE” 修訂版) 內的無更動 (沒有關鍵字展開, 沒有列尾字元展開, 什麼都沒有) 檔案複本. 我們稱這個檔案為 “原始未更動 (prestine)” 複本.

HEAD

檔案庫內的最新版本.

BASE

一個物件在工作複本中的 “原始未更動” 修訂版.

COMMITTED

一個物件距 BASE 修訂版之前 (包含), 最近一次修改的修訂版.

PREV

最近一次修改的修訂版的 前一個 修訂版. (技術上來講, 就是 COMMITTED - 1.)

以下是幾個使用修訂版關鍵字的範例 (如果不懂這些命令, 沒有關係; 藉由本章的引導, 我們會逐步解釋這些命令):

$ svn diff --revision PREV:COMMITTED foo.c
# 顯示最近一次送交 foo.c 所產生的更動

$ svn log --revision HEAD
# 顯示最新的檔案庫送交的記錄訊息

$ svn diff --revision HEAD
# 將工作檔案 (含有本地的修改) 與檔案庫的最新版本作比較

$ svn diff --revision BASE:HEAD foo.c
# 將你的 “原始未更動” foo.c (未含本地修改)
# 與檔案庫裡的最新版本作比較

$ svn log --revision BASE:HEAD
# 顯示自你上次更新後的所有送交訊息

$ svn update --revision PREV foo.c
# 復原上一個 foo.c 的更動
# (foo.c 的工作修訂版本會被減少.)
      

這些關鍵字感覺並不是非常重要, 但是它們可以讓你進行一些常見 (而且有用) 的動作, 而不必先找出確切的修訂版號, 或是記住工作複本確實的修訂版本.

修訂版日期

任何你可以指定修訂版號或修訂關鍵字的地方, 你也可以在大括號 “{}” 內指定日期, 就可以指定修訂版日期. 你還可以一起使用日期與修訂版, 以存取檔案庫裡一個範圍的更動.

Subversion 可接受相當多的日期格式— 只要記得將有空白的日期以引號包起來就好. 這裡只是幾個 Subversion 接受的格式的例子而已:

$ svn checkout --revision {2002-02-17}
$ svn checkout --revision {2/17/02}
$ svn checkout --revision {"17 Feb"}
$ svn checkout --revision {"17 Feb 2002"}
$ svn checkout --revision {"17 Feb 2002 15:30"}
$ svn checkout --revision {"17 Feb 2002 15:30:12 GMT"}
$ svn checkout --revision {"10 days ago"} 
$ svn checkout --revision {"last week"} 
$ svn checkout --revision {"yesterday"} 
…
      

當你將日期指定為修訂版時, Subversion 會找出與該日期最接近的檔案庫的修訂版:

$ svn log --revision {11/28/2002}
------------------------------------------------------------------------
r12:  ira | 2002-11-27 12:31:51 -0600 (Wed, 27 Nov 2002) | 6 lines
…
      

你也可以使用一個範圍的日期. Subversion 會找出所有兩個日期之間的修訂版, 包含這兩個日期在內:

$ svn log --revision {2002-11-20}:{2002-11-29}
…
      

就像我們前面指出的, 你也可以混合日期與修訂版號:

$ svn log -r {11/20/02}:4040
      

最初的取出動作

大部份開始使用 Subversion 的檔案庫的動作, 就是對你的專案進行 取出 (checkout) 的動作. “取出” 檔案庫會在你的機器裡建立一份複本. 這個複本包含了你在命令列裡指定的檔案庫的 HEAD (也就是最新的) 版本:

$ svn checkout http://svn.collab.net/repos/svn/trunk
A  trunk/subversion.dsw
A  trunk/svn_check.dsp
A  trunk/COMMITTERS
A  trunk/configure.in
A  trunk/IDEAS
…
Checked out revision 2499.
    

雖然上面例子取出的是 trunk 目錄, 但是你可以取出任何檔案庫深層的目錄, 只要在取出的 URL 指定副目錄即可:

$ svn checkout http://svn.collab.net/repos/svn/trunk/doc/book/tools
A  tools/readme-dblite.html
A  tools/fo-stylesheet.xsl
A  tools/svnbook.el
A  tools/dtd
A  tools/dtd/dblite.dtd
…
Checked out revision 3678.
    

由於 Subversion 使用的是 “複製-修改-合併” 模式, 而不是 “鎖定-修改-解鎖” 模式 (參見 Chapter 2, 基本概念), 你現在已經可以修改已取出的檔案與目錄 (整個稱之為 工作複本.

換句話說, 現在你的 “工作複本” 就像其它在系統中的目錄與檔案的集合一樣 [1]. 你可以編輯或更動它們, 把它們移來移去, 甚至還可以刪掉整個工作複本, 然後完全把它拋諸腦後.

Note

雖然你的工作複本 “就只是跟其它在系統上的目錄與檔案的集合沒有兩樣”, 但是你要重新擺放工作複本裡的東西的話, 你還是得讓 Subversion 知道. 如果你要複製或移動工作複本中的某個項目, 你應該使用 svn copysvn move, 而非作業系統所提供的複製與移動的指令. 本章稍後會再討論到這些指令.

除非你準備要 送交 (commit) 新檔案或目錄, 或是對現有項目的更動, 否則你不需通知 Subversion 伺服器你所作的事情.

雖然你可以用檔案庫的 url 作為唯一的參數, 來取出工作複本, 你還是可以在檔案庫 url 之後指定一個目錄, 如此會將你的工作複本置於你命名的新目錄中. 舉個例子:

$ svn checkout http://svn.collab.net/repos/svn/trunk subv
A  subv/subversion.dsw
A  subv/svn_check.dsp
A  subv/COMMITTERS
A  subv/configure.in
A  subv/IDEAS
…
Checked out revision 2499.
    

這會將你的目錄置於 subv 中, 而不是我們之前的 trunk 目錄.

基本工作流程

Subversion 有許多特色, 選項, 以及其它有的沒有的功能, 不過對每天例行的工作來說, 你會用到的只有其中的一小部份而已. 在本節中, 我們會詳加介紹所有你會在每天的工作中, 最常會使用到的功能.

典型的工作流程, 看起來像這樣:

  • 更新工作複本

    • svn update

  • 產生更動

    • svn add

    • svn delete

    • svn copy

    • svn move

  • 檢視你的更動

    • svn status

    • svn diff

    • svn revert

  • 合併其它人的更動

    • svn merge

    • svn resolved

  • 送交更動

    • svn commit

更新工作複本

與一組團隊同時修改同一個專案時, 你會想先 更新 (update) 你的工作複本: 也就是說, 取回同一專案中, 其它發展人員所作的更動. 你可以使用 svn update, 將你的工作複本裡的版本同步至檔案庫的最新修訂版.

$ svn update
U  ./foo.c
U  ./bar.c
Updated to revision 2.
      

在這個例子中, 有人從你上次更新後, 登錄了 foo.cbar.c 所產生的更動, 而且 Subversion 已經更新了工作目錄, 以包含這些新的更動.

讓我們更詳細地檢視 svn update 的輸出. 當伺服器送出更動至你的工作目錄時, 在每一個項目的旁邊, 都有一個字母代碼, 讓你知道 Subversion 會執行什麼動作來更新你的工作目錄.

U foo

檔案 foo 會被更新 (Update) (自伺服器取得更動).

A foo

檔案或目錄 foo 會被新增 (Add) 到工作目錄中.

D foo

檔案或目錄 foo 會自工作目錄刪除 (Delete).

R foo

工作複本的檔案或目錄 foo 被取代 (Replace); 也就是說, foo 被刪除, 然後新增同一名稱的新項目. 雖然它們的名稱是相同的, 但是檔案庫會認為它們是不同的, 而且有著不同的歷史進程.

G foo

檔案 foo 自檔案庫取得新的更動, 但是本地複本的檔案含有你的更動. 不過這些更動並沒有重疊的部份, 所以 Subversion 可以毫無困難地合併 (merGe) 檔案庫的更動.

C foo

檔案 foo 自伺服器收到衝突的 (Conflict) 更動. 從伺服器來的更動, 與你對該檔案的更動有重疊的部份, 不過不必太驚慌失措. 衝突必須由人類 (也就是你) 來解決; 我們在本章稍後會討論這個狀況.

對工作複本產生更動

現在你可以開始工作, 修改工作複本了. 比較好的作法, 是產生特定的一個更動 (或是一組更動), 像是加個新功能, 修正臭蟲等等. 這裡會用到的 Subversion 指令, 有 svn add, svn delete, svn copy, 以及 svn move. 如果你只是要修改一個已在 Subversion 裡的檔案 (或是不只一個檔案), 可能在送交更動前都不會用到這些指令. 你能對工作複本所作的更動:

變更檔案

這是最簡單的更動了. 你不需要先和 Subversion 說你要修改檔案; 直接更動它即可. Subversion 有能力自動偵測哪些檔案被更動過.

目錄結構更動

你可以要求 Subversion 先 “標示” 預定要移除、 新增、複製、 或是移動的目錄或檔案. 雖然這些更動會馬上出現在工作複本中, 不過在你送交更動前, 檔案庫並不會跟著有所變動.

要產生檔案的更動, 請使用文字編輯程式, 文書處理程式, 圖形程式, 或是你平常使用的任何工具. Subversion 可以很容易地處理二進制檔案, 如同文字檔案一般 —而且一樣有效率.

以下是你最常用來更動目錄結構的四個常用的 Subversion 指令 (我們稍候會講到 svn importsvn mkdir).

svn add foo

預定將檔案 foo 新增至檔案庫中. 下一次送交時, foo 會成為其父目錄的子項目. 請注意如果 foo 是一個目錄的話, 所有在 foo 的東西都預定會被加入檔案庫. 如果你只想要 foo 本身而已, 請使用 --non-recursive (-N) 選項.

svn delete foo

預定將 foo 自檔案庫刪除. 如果 foo 是檔案, 那麼它會馬上自工作複本中刪除. 如果 foo 是目錄的話, 它不會被刪除, 但是 Subversion 會預定將其刪除. 當你送交你的更動時, foo 會自工作複本與檔案庫刪除. [2]

svn copy foo bar

建立一個新的項目 bar, 成為 foo 的複本. bar 會自動被預定新增至檔案庫中. 當 bar 在下一次被送交時, 它的複製歷程就會被紀錄下來 (也就是來自 foo).

svn move foo bar

這個命令就像執行 svn cp foo bar; svn delete foo 一樣. 也就是說, bar 預定被新增為 foo 的複本, 而 foo 則預定被刪除.

檢視你的更動

當你修改完畢後, 你必須將更動送交至檔案庫, 不過在這樣作之前, 先看看自己作了哪些更動, 是個不錯的主意. 在送交前先檢視更動的部份, 讓你可以寫出更清楚的記錄訊息. 你還可能會發現意外更動到的檔案, 這讓你在送交前還有回復這項錯誤的機會. 除此之外, 這也是在發表之前, 仔細檢視並檢查所作的更動的時機. 想要更清楚地了解所作的更動, 你可以藉由 svn status, svn diff, 以及 svn revert 這些命令, 來看看你所作的更動是什麼. 通常你會用前兩個命令, 來找你對工作複本所作的更動, 然後可能用第三個命令來復原這些更動.

Subversion 經過特別調校, 可在不與檔案庫溝通的情況下, 幫你進行以下的工作, 外加許多其它的工作. 尤其是你的工作複本裡, .svn 區域擁有每一個受版本控制的檔案的 “原始未更動” 複本. 也因為如此, Subversion 可以很快地告訴你檔案是如何被更動, 甚至讓你可以取消你所作的變更, 而不需使用到檔案庫.

svn status

你使用 svn status 的次數, 可能會遠大於其它的 Subversion 命令.

如果你對工作目錄不帶任何引數執行 svn status, 它會偵測出所有在檔案樹裡所產生的更動. 以下的例子, 是用來顯示所有 svn status 可傳回的狀態碼. 接在 # 後的文字, 並不是由 svn status 所產生的.

  L    ./abc.c               # svn 在 .svn 目錄中, 有 abc.c 的鎖定
M      ./bar.c               # bar.c 的內容, 有本地端的變更
 M     ./baz.c               # baz.c 已變更了性質, 但是沒有內容的更動
?      ./foo.o               # svn 並未管理 foo.o
!      ./some_dir            # svn 管理它, 但是不是不見了, 就是不完整
~      ./qux                 # 納入管理的是目錄, 但是這裡是檔案, 或是相反的情況
A  +   ./moved_dir           # 新增項目, 並以來源的歷史紀錄為其歷史紀錄
M  +   ./moved_dir/README    # 新增項目, 使用來源的歷史紀錄, 再加上本地更動
D      ./stuff/fish.c        # 本檔案已預定要被刪除
A      ./stuff/loot/bloo.h   # 本檔案已預定要被新增
C      ./stuff/loot/lump.c   # 這個檔案有因更新而產生的衝突
    S  ./stuff/squawk        # 這個檔案或目錄已切換到分支
        

在這個 svn status 的輸出格式中, 先印出了五欄字元, 後接幾個空白, 再接一個檔案或目錄的名稱. 第一欄表示檔案或目錄本身, 以及/或是其內容的狀態. 這裡顯示的代碼有:

A file_or_dir

目錄或檔案 file_or_dir 已預定要被新增至檔案庫.

M file

檔案 file 的內容已被修改.

D file_or_dir

目錄或檔案 file_or_dir 已預定要自檔案庫刪除.

X dir

目前 dir 未納入版本控制, 但是關聯到一個 Subversion 的外部定義. 欲了解更多外部定義, 請參考 the section called “外部定義”.

? file_or_dir

目錄或檔案 你可以藉由 svn status 命令的 --quite (-q) 選項, 或是對父目錄設定其 svn:ignore 性質, 就不會顯示問號代碼. 想知道有哪些檔案會被忽略, 請參照 the section called “svn:ignore”.

! file_or_dir

目錄或檔案 file_or_dir 已納入版本控制之中, 但是它不是消失, 就是不完整. 如果這個檔案或目錄被非 Subversion 的命令所刪除, 就會被判定為消失. 如果是目錄的話, 如果你在取出或是更新時中斷, 那麼它就會變成不完整. 只要執行 svn update, 就可以從檔案庫重新取得目錄或檔案, 或者以 svn revert file_or_dir, 回存消失的檔案.

~ file_or_dir

目錄或檔案 file_or_dir 在檔案庫裡是一種物件, 但是實際在工作複本中又是另一種. 舉個例子, Subversion 可能在檔案庫裡有一個檔案, 但是你刪除了這個檔案, 而以原名稱建立了一個目錄, 但是都沒有使用 svn deletesvn add 命令來處理.

C file

file_or_dir 處於衝突的狀態. 也就是說, 在更新時, 來自伺服器的更新與工作複本中的本地更動, 有重疊的部份. 在送交更動回檔案庫之前, 你必須先解決這個這個衝突.

第二欄指示的是目錄或檔案的性質 (請參見 the section called “性質”, 以了解更多性質的資訊). 如果第二欄出現的是 M, 那麼表示其性質曾變更過, 不然顯示的是空白字元.

第三欄只會顯示空白字元, 或是 L, 表示 Subversion 在 .svn 工作區中有物件的鎖定. 如果你處於正在執行 svn commit 的目錄中, 此時執行 svn status 的話, 你就會看到 L—大概那時正在編輯記錄訊息. 如果 Subversion 並沒有在執行的話, 那麼很有可能 Subversion 因故中斷. 這個鎖定可以利用 svn cleanup 來清除 (稍後本章會提到).

第四欄只會顯示空白字元, 或是 +, 表示這個檔案或目錄已預定要加入檔案庫, 或是以額外附加的歷史紀錄修改. 通常發生的時機, 是你對檔案或目錄執行 svn movesvn cp 命令. 如果你看到 A  +, 表示這個項目已預定被加入檔案庫, 並有歷史紀錄. 這可能是一個檔案, 也可能是目錄複製的根目錄. + 表示這是個預定被加入, 並有歷史紀錄的子檔案樹, 也就是其某個父目錄被複製, 它是連帶被複製的. M  + 表示這是個預定被加入, 並有歷史紀錄的子檔案樹, 而且 它有本地端的更動. 當你送交的時候, 其父物件會先以附加歷史紀錄的方式加入 (複製), 也就是該檔案會自動出現在複本中, 接著本地的修改也會跟著上傳至複本中.

第五欄只會顯示空白或是 S. 它表示這個檔案或目錄, 已經自該工作複本裡的路徑 (利用 svn switch) 切換到一個分支.

如果你指定路徑給 svn status, 它只會提供給你該物件的資訊:

$ svn status stuff/fish.c
D      stuff/fish.c
        

svn status 也有 --verbose (-v) 選項, 它會顯示 所有 在工作目錄中的物件資料, 就算還沒有修改過的亦同:

$ svn status --verbose
M               44        23    sally     ./README
                44        30    sally     ./INSTALL
M               44        20    harry     ./bar.c
                44        18    ira       ./stuff
                44        35    harry     ./stuff/trout.c
D               44        19    ira       ./stuff/fish.c
                44        21    sally     ./stuff/things
A                0         ?     ?        ./stuff/things/bloo.h
                44        36    harry     ./stuff/things/gloo.c
        

這是 svn status 的 “長格式” 輸出. 第一欄的輸出不變, 但是第二欄顯示的是物件的工作修訂版. 第三與第四欄顯示上次修改的修訂版, 還有誰修改了它.

以上的 svn status 的行為都不會存取到檔案庫, 他們只會比較工作複本與本地端的 .svn 目錄的描述資料. 最後要提的是 --show-updates (-u) 選項, 這就會存取檔案庫, 並且顯示已經 過時 的資訊:

$ svn status --show-updates --verbose
M      *        44        23    sally     ./README
M               44        20    harry     ./bar.c
       *        44        35    harry     ./stuff/trout.c
D               44        19    ira       ./stuff/fish.c
A                0         ?     ?        ./stuff/things/bloo.h
        

請注意那兩個星號: 如果你現在執行 svn update, 那麼你會取得 READMEtrout.c 的更新. 這告訴你一件很重要的事情— 你需要在你送交變更之前, 先作更新, 自伺服器取得 README 檔案的更新, 不然檔案庫 會因檔案過時而拒絕你的送交動作. (稍後會著墨這個課題.)

svn diff

另一個檢視更動的方法, 是使用 svn diff 命令. 你可以得知 切確 修改的地方, 只要不帶引數執行 svn diff 即可, 它會以統一差異格式 (unified diff) 格式顯示檔案的更動: [3]

$ svn diff
Index: ./bar.c
===================================================================
--- ./bar.c
+++ ./bar.c	Mon Jul 15 17:58:18 2002
@@ -1,7 +1,12 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <stdio.h>

 int main(void) {
-  printf("Sixty-four slices of American Cheese...\n");
+  printf("Sixty-five slices of American Cheese...\n");
 return 0;
 }

Index: ./README
===================================================================
--- ./README
+++ ./README	Mon Jul 15 17:58:18 2002
@@ -193,3 +193,4 @@ 
+Note to self:  pick up laundry.

Index: ./stuff/fish.c
===================================================================
--- ./stuff/fish.c
+++ ./stuff/fish.c  Mon Jul 15 17:58:18 2002
-Welcome to the file known as 'fish'.
-Information on fish will be here soon.

Index: ./stuff/things/bloo.h
===================================================================
--- ./stuff/things/bloo.h
+++ ./stuff/things/bloo.h  Mon Jul 15 17:58:18 2002
+Here is a new file to describe
+things about bloo.
        

svn diff 命令藉由比較你的工作複本, 以及 .svn 裡的 “原始未更動” 複本, 以產生這個輸出. 預定要加入的檔案, 會以全部新增的方式顯示, 而預定要刪除的檔案則以全部刪除的方式顯示.

輸出是以 統一差異格式 顯示的. 也就是說, 刪除的文字列是以 - 開頭的, 新增的文字列是以 + 開頭的. svn diff 也會顯示可供 patch 使用的檔名與偏移資訊, 所以你可以將 diff 輸出重導向至檔案, 以產生 “修補檔”:

$ svn diff > patchfile
        

舉例來說, 你可以在送交之前, 先把修補檔以電子郵件寄給另一個發展人員檢閱, 或是測試有沒有問題.

svn revert

假設你現在看了以上的 diff 輸出, 發現你對 README 所作的修改是不對的; 也許你不小心把那段文字打錯了檔案.

現在就是使用 svn revert 的絕佳機會.

$ svn revert README
Reverted ./README
        

Subversion 會使用 .svn 裡的 “原始未更動” 複本來蓋寫檔案, 將它回覆至修改之前的狀態. 不過 svn revert 也能取消 任何 預定的動作—舉個例子, 你可能會決定根本不必增加一個新檔案:

$ svn status foo
?      foo

$ svn add foo
A         foo

$ svn revert foo
Reverted foo

$ svn status foo
?      foo
        

Note

svn revert ITEM 的效果, 就跟自工作複本刪除 ITEM, 然後執行 svn update ITEM 是一樣的. 但是如果你要回復一個檔案, svn revert 有一個很重要的差異 — 它不需要與檔案庫溝通, 就能回復你的檔案.

或者你不該從版本控制之中刪除一個檔案:

$ svn status README 
       README

$ svn delete README 
D         README

$ svn revert README
Reverted README

$ svn status README
       README
        

解決衝突 (合併他人的更動)

我們之前已經看過 svn status -u 如何預測衝突. 假設你執行了 svn update, 然後有趣的事情發生了:

$ svn update
U  ./INSTALL
G  ./README
C  ./bar.c
      

代碼 UG 沒什麼好擔心的; 這幾個檔案都很順利地接受了來自檔案庫的更動. 標示有 U 的檔案, 表示它沒有本地端的更動, 但是更新了檔案庫的更動. G 代表的它已經合併更動, 也就是說有本地端的更動, 但是來自檔案庫的更動並沒有與它重疊.

但是 C 表示有衝突, 也就是說來自伺服器的更動與你的更動有重疊的地方, 而現在你必須手動在這兩者之間作選擇.

不管衝突於哪裡發生, 用戶端的 Subversion 會作三件事:

  • Subversion 在更新時, 會顯示 C, 並且記得這個檔案 “有衝突”.

  • Subversion 會將 衝突標記 置於檔案中, 明確地將重疊的部份標示出來.

  • 對每一個衝突的檔案而言, Subversion 會額外放置三個檔案在你的工作複本中:

    filename.mine

    這是你在更新工作複本前, 就在工作複本中的檔案—也就是說, 它沒有衝突標記. 這個檔案就只有你的最新更動, 沒有包含其它的東西.

    filename.r舊版號

    這是在你更新工作複本前, BASE 修訂版的檔案. 也就是在你開始進行修改之前所取出的檔案.

    filename.r新版號

    這是 Subversion 用戶端在你更新工作複本時, 剛從伺服器取得的檔案. 這個檔案對應的是檔案庫中的 HEAD 修訂版的檔案.

    這裡的 舊版號 是該檔案在 .svn 目錄中的修訂版號, 而 新版號 是檔案庫中的 HEAD 修訂版號.

舉例來說, Sally 對檔案庫中的 sandwich.txt 作了修改. Harry 則剛在他的工作複本中, 修改了這個檔案, 然後將它登錄進去. Sally 在將檔案登錄時, 先執行更新動作, 結果她得到了一個衝突狀況:

$ svn update
C  sandwich.txt
Updated to revision 2.
$ ls -1
sandwich.txt
sandwich.txt.mine
sandwich.txt.r1
sandwich.txt.r2
      

此時, Subversion 將 允許你再送交檔案 sandwich.txt, 除非這三個臨時的檔案都被刪除.

$ svn commit --message "Add a few more things"
svn: A conflict in the working copy obstructs the current operation
svn: Commit failed (details follow):
svn: Aborting commit: '/home/sally/svn-work/sandwich.txt' remains in conflict.
      

如果你遇到了衝突, 你必須進行以下三項之一:

  • 手動” 合併發生衝突的文字 (藉由檢視與編輯檔案裡的衝突標記).

  • 將某一個臨時檔案複製並蓋過你的工作檔.

  • 執行 svn revert <filename>, 將你的本地更動全部捨棄.

當你解決了衝突之後, 你必須執行 svn resolved, 讓 Subversion 知道. 如此會刪除這三個臨時檔案, Subversion 就不會認為這個檔案還是處於衝突的狀態. [4]

$ svn resolved sandwich.txt
Resolved conflicted state of sandwich.txt
      

手動合併衝突

第一次嘗試手動合併衝突可能會覺得很恐怖, 但是經過少許的練習後, 就會覺得和從腳踏車上摔下來一樣, 沒什麼大不了的.

以下是個範例. 假設因為你與協同工作人員 Sally 的溝通有誤, 兩個人都同時編輯了 sandwich.txt. Sally 送交了她的更動, 而你更新工作複本時就會得到一個衝突. 我們要編輯 sandwich.txt 以解決衝突. 首先, 讓我們先看看這個檔案:

$ cat sandwich.txt
Top piece of bread
Mayonnaise
Lettuce
Tomato
Provolone
<<<<<<< .mine
Salami
Mortadella
Prosciutto
=======
Sauerkraut
Grilled Chicken
>>>>>>> .r2
Creole Mustard
Bottom piece of bread
        

這些小於符號, 等於符號, 以及大於符號, 稱為 衝突標記. 夾在前兩種標記之間的文字, 就是你在衝突區域所作的變更:

<<<<<<< .mine
Salami
Mortadella
Prosciutto
=======
        

夾在後兩組衝突標記的文字, 來自於 Sally 的送交更動:

=======
Sauerkraut
Grilled Chickenn
>>>>>>> .r2
        

通常你不會直接刪掉衝突標記與 Sally 的更動—當她看到 sandwich, 發現與她預期的不同時, 可是會嚇個老大一跳. 所以現在請拿起你的電話, 或是走過辦公室, 跟她解釋在義大利食品店, 是買不到德國泡菜的. [5] 當你們都同意你將存入的變更, 請編輯你的檔案, 將衝突標記移除.

Top piece of bread
Mayonnaise
Lettuce
Tomato
Provolone
Salami
Mortadella
Prosciutto
Creole Mustard
Bottom piece of bread
        

現在執行 svn resolved, 你就可以送交你的更動.

$ svn resolved sandwich.txt
$ svn commit -m "Go ahead and use my sandwich, discarding Sally's edits."
        

請記住, 如果在編輯衝突檔案時, 搞不清楚該怎麼改, 你還可以參考 Subversion 在工作複本中建立的三個檔案— 還包括你在更新前, 己修改過的檔案.

將檔案複製並蓋過你的工作檔

如果你遇到了衝突, 但是決定要捨棄你所作的更動, 你可以直接將 Subversion 建立的暫存檔之一複製並蓋過你的工作檔:

$ svn update
C  sandwich.txt
Updated to revision 2.
$ ls sandwich.*
sandwich.txt  sandwich.txt.mine  sandwich.txt.r2  sandwich.txt.r1
$ cp sandwich.txt.r2 sandwich.txt
$ svn resolved sandwich.txt
$ svn commit -m "Go ahead and use Sally's sandwich, discarding my edits."
        

棄踢: 使用 svn revert

如果你遇到了衝突, 經檢視之後, 決定要丟棄你的更動, 再重新開始你的編輯工作, 只要回復所作的更動即可:

$ svn revert sandwich.txt
Reverted sandwich.txt
$ ls sandwich.*
sandwich.txt
        

請注意, 你在回復一個衝突的檔案時, 不必再執行 svn resolved.

現在你可以登錄你的更動了. 請注意 svn resolved 並不像本章其它的命令一樣, 它需要一個引數來執行. 不管在什麼情況下, 你必須非常小心, 只有當你非常確定已經解決了檔案中的衝突, 才執行 svn resolved — 當臨時檔案被移除後, 就算檔案中還有衝突標記, Subversion 還是會讓你送交檔案.

送交更動

終於到這裡了! 你已經完成了你的編輯, 從伺服器合併更動的部份, 並且準備要將你的更動送交至檔案庫.

svn commit 命令會將所有的更動送至檔案庫. 當你送交了一個更動, 你需要給它一個 記錄訊息, 說明一下你的更動. 你的記錄訊息會附在你建立的新修訂版上. 如果記錄訊息很簡短的話, 你可能會希望透過命令列的 --message (或 -m) 選項來指定:

$ svn commit --message "Corrected number of cheese slices."
Sending        sandwich.txt
Transmitting file data .
Committed revision 3.
      

但是如果你是邊工作邊寫記錄訊息的話, 你大概會希望直接告訴 Subversion, 從 --file 選項所指定的檔案來取得記錄訊息:

svn commit --file logmsg 
Sending        sandwich
Transmitting file data .
Committed revision 4.
      

如果你並沒有指定 --message--file, 那麼 Subversion 會自動叫用你偏好的編輯器 (定義於環境變數 $EDITOR 中) 來編輯記錄訊息.

Tip

如果你在編輯器中寫了送交訊息, 然後決定要取消這次的送交, 只要不儲存更動, 直接結束編輯器即可. 如果你已經儲存了送交訊息, 只要把文字都刪除, 然後再儲存一次即可.

$ svn commit
Waiting for Emacs...Done

Log message unchanged or not specified
a)bort, c)ontinue, e)dit
a
$
        

檔案庫並不會知道, 也不管你的更動是否有意義; 它只會檢查是否有人在你沒注意時, 也修改了你所更動的檔案. 如果有人 修改到了, 那麼整個送交就會失敗, 並以一則訊息提示你, 有一個或多個檔案已經過時了:

$ svn commit --message "Add another rule"
Sending        rules.txt
svn: Transaction is out of date
svn: Commit failed (details follow):
svn: out of date: `rules.txt' in txn `g'
$

此時, 你需要執行 svn update, 處理產生的合併或衝突, 然後再試著送交一次.

這樣就涵蓋了 Subversion 的基本工作流程. Subversion 還有許多功能, 可用來管理你的檔案庫與工作複本, 但是我們在本章所介紹的命令, 已足以讓你輕鬆地應付日常工作所需.

檢視歷史紀錄

我們早先曾說過, 檔案庫就像時光機器一樣, 它會記錄所有曾送交過的更動, 而且允許你檢視檔案與目錄, 以及伴隨的描述資料的過去修訂版, 以了解過往的歷史. 透過一個 Subversion 的命令, 你可以取出檔案庫 (或回復現有的工作複本), 讓它完全符合過去某一個修訂版號, 或是某一天的樣子. 不過呢, 有的時候你只是想 看看 過去, 而不是真的 回到 過去.

有幾個命令, 都能夠從檔案庫提供歷史資料:

svn log

給你比較廣的資訊: 附加在修訂版的記錄訊息, 以及每一個修訂版所更動的路徑.

svn diff

提供檔案隨著時間的確切變更內容.

svn cat

這是用來取得任何存於某一特定修訂版的檔案, 並將檔案內容顯示在螢幕上.

svn list

用來顯示任何指定修訂版的目錄內的檔案.

svn log

要找出某一檔案或目錄的歷史紀錄資訊, 請用 svn log 命令. svn log 可告訴你誰改了檔案或目錄, 該修訂版的日期與時間. 另外, 如果有提供送交時的記錄訊息, 它也會一併顯示出來.

$ svn log
------------------------------------------------------------------------
r3:  sally | Mon, 15 Jul 2002 18:03:46 -0500 | 1 line

Added include lines and corrected # of cheese slices.
------------------------------------------------------------------------
r2:  harry | Mon, 15 Jul 2002 17:47:57 -0500 | 1 line

Added main() methods.
------------------------------------------------------------------------
r1:  sally | Mon, 15 Jul 2002 17:40:08 -0500 | 2 lines

Initial import
------------------------------------------------------------------------
      

請注意預設記錄訊息是以 反向時間順序 顯示的. 如果你希望以特定順序顯示某個範圍的修訂版, 或是僅僅某一個修訂版而已, 請使用 --revision (-r) 選項:

$ svn log --revision 5:19    # 以時間順序, 顯示 5 到 19 的紀錄訊息

$ svn log -r 19:5            # 以反向時間順序, 顯示 5 到 19 的紀錄訊息

$ svn log -r 8               # 顯示修訂版 8 的紀錄訊息
      

你也可以檢視某一個檔案或目錄的訊息歷程. 舉個例子:

$ svn log foo.c
…
$ svn log http://foo.com/svn/trunk/code/foo.c
…
      

這些就 只會 顯示指定檔案 (或是 URL) 變動過的修訂版紀錄訊息.

如果你還想要更多有關於檔案或目錄的資訊, svn log 也有 --verbose (-v) 詳細訊息輸出選項. 由於 Subversion 允許你移動複製檔案或目錄, 追蹤檔案系統內的路徑更動是很重要的, 所以開啟詳細訊息輸出的話, svn log 會在修訂版的輸出中, 包含一串 變動路徑 的列表:

$ svn log -r 8 -v
------------------------------------------------------------------------
r8:  sally | 2002-07-14 08:15:29 -0500 | 1 line
Changed paths:
U /trunk/code/foo.c
U /trunk/code/bar.h
A /trunk/code/doc/README

Frozzled the sub-space winch.

------------------------------------------------------------------------
      

svn diff

我們在前面已經看過 svn diff— 它會以統一差異格式來顯示檔案的差異; 在我們送交至檔案庫之前, 可用來顯示我們對本地的工作複本所作的修改.

事實上, svn diff 總共有 三種 不同的用法:

  • 檢視本地端更動

  • 比較檔案庫與本地複本

  • 檔案庫與檔案庫之間的比較

檢視本地端更動

我們已經看過, 不帶引數執行 svn diff, 會比較 .svn 區域裡的 “原始未更動” 複本與工作複本之間的差異:

$ svn diff
Index: rules.txt
===================================================================
--- rules.txt	(revision 3)
+++ rules.txt	(working copy)
@@ -1,4 +1,5 @@
 Be kind to others
 Freedom = Responsibility
 Everything in moderation
-Chew with your mouth open
+Chew with your mouth closed
+Listen when others are speaking
$
        

比較檔案庫與本地複本

如果只有傳遞一個 --revision (-v) 的修訂版號, 那麼你的工作複本會與指定的檔案庫修訂版作比較.

$ svn diff --revision 3 rules.txt 
Index: rules.txt
===================================================================
--- rules.txt	(revision 3)
+++ rules.txt	(working copy)
@@ -1,4 +1,5 @@
 Be kind to others
 Freedom = Responsibility
 Everything in moderation
-Chew with your mouth open
+Chew with your mouth closed
+Listen when others are speaking
$
        

檔案庫與檔案庫之間的比較

如果透過 --revision (-r), 傳遞兩個以冒號隔開的修訂版號, 那麼這兩個修訂版號會直接拿來比較.

$ svn diff --revision 2:3 rules.txt 
Index: rules.txt
===================================================================
--- rules.txt	(revision 2)
+++ rules.txt	(revision 3)
@@ -1,4 +1,4 @@
 Be kind to others
-Freedom = Chocolate Ice Cream
+Freedom = Responsibility
 Everything in moderation
 Chew with your mouth closed 
$
        

svn diff 不但能拿來比較工作複本與檔案庫的檔案, 如果你的引數是 URL 的話, 就可以檢視檔案庫中的兩個物件, 甚至完全不需要先有工作複本. 如果你想看的更動, 是本地機器沒有工作複本的檔案, 這樣就非常有用了:

$ svn diff --revision 4:5 http://svn.red-bean.com/repos/example/trunk/text/rules.txt
…
$
        

svn cat

如果你想要檢視某一檔案早先的版本, 而不是兩個檔案之間的差異, 你可以使用 svn cat:

$ svn cat --revision 2 rules.txt 
Be kind to others
Freedom = Chocolate Ice Cream
Everything in moderation
Chew with your mouth closed
$
      

你可以將輸出直接重導到一個檔案中:

$ svn cat --revision 2 rules.txt > rules.txt.v2
$
      

你大概在想, 為何我們不使用 svn update --revision 將檔案更新至舊的修訂版. 有幾個理由, 讓我們會想用 svn cat.

首先, 你可能想要使用外部的 diff 程式, 來看一個檔案兩個不同版本之間的差異 (也許是圖形界面的, 也許你的檔案已經是這樣的格式, 輸出統一差異格式是完全沒有意思的). 在這種情況下, 你需要取得舊版本的複本, 重導向至一個檔案中, 然後將它與在你的工作目錄中的檔案一起傳給你的外部 diff 程式.

有的時候直接看整個先前版本的檔案, 要比只看它與其它修訂版的差異要方便得多.

svn list

svn list 命令可用來顯示檔案庫的目錄中有什麼檔案, 而不必實際將檔案下載至本地機器中:

$ svn list http://svn.collab.net/repos/svn
README
branches/
clients/
tags/
trunk/
      

如果你想要更詳細的列表, 加上 --verbose (-v) 選項, 就可以取得像這樣的輸出.

$ svn list --verbose http://svn.collab.net/repos/svn
   2755 harry          1331 Jul 28 02:07 README
   2773 sally               Jul 29 15:07 branches/
   2769 sally               Jul 29 12:07 clients/
   2698 harry               Jul 24 18:07 tags/
   2785 sally               Jul 29 19:07 trunk/
      

這些欄位告訴你, 這個檔案或目錄最後一次修改的修訂版, 修改它的使用者, 檔案則會有檔案大小, 上一次更新的日期, 以及該項目的名稱.

對歷史紀錄的最後叮嚀

除了以上的命令, 你還可以使用 svn updatesvn checkout, 加上 --revision 選項, 就可以將整個工作複本帶回到 “過去的時間[6]:

$ svn checkout --revision 1729 # 取出修訂版 1729 為新的工作複本
…
$ svn update --revision 1729 # 更新現有的工作複本為修訂版 1729
…
      

其它有用的命令

雖然不像本章前面討論過的命令那麼常用到, 這些命令還是有需要的時候.

svn cleanup

當 Subversion 修改你的工作複本時 (或是任何在 .svn 的資訊) 時, 它會試著儘量以安全的方式來進行. 在修改任何東西前, 會先將其意圖寫入一個紀錄檔, 執行紀錄檔裡的命令, 然後刪除紀錄檔 (這很像日誌檔案系統的設計). 如果一個 Subversion 的動作被中斷 (像是你按了 Control-C, 或是機器當掉了), 那麼紀錄檔會繼續存在磁碟之中. 藉由重新執行紀錄檔, Subversion 可以完成先前進行的動作, 而你的工作複本也能回復到一致的狀態.

而這就是 svn cleanup 所作的事: 它會搜尋你的工作複本, 執行任何遺留下來的紀錄檔, 移除動作進行時所使用的鎖定檔. 如果 Subversion 曾說過工作複本的某些地方被 “鎖定” 了, 那麼這就是你該執行的命令. 另外, svn status 也會在被鎖定的項目旁顯示 L:

$ svn status
  L    ./somedir
M      ./somedir/foo.c 

$ svn cleanup
$ svn status
M      ./somedir/foo.c
      

svn import

svn import 匯入命令, 是用來將未納入版本控制的檔案樹放進檔案庫的快速方法.

$ svnadmin create /usr/local/svn/newrepos
$ svn import mytree  file:///usr/local/svn/newrepos/fooproject
Adding  mytree/foo.c
Adding  mytree/bar.c
Adding  mytree/subdir
Adding  mytree/subdir/quux.h
Transmitting file data....
Committed revision 1.
      

以上的例子, 是將目錄 mytree 的內容, 直接放至檔案庫的 fooproject 目錄裡:

/fooproject/foo.c
/fooproject/bar.c
/fooproject/subdir
/fooproject/subdir/quux.h
      

摘要

現在我們已經涵蓋了大多數的 Subversion 用戶端命令. 顯而未提的部份, 是處理分支與合併 (參見 Chapter 4, 分支與合併), 以及處理性質 (參見 the section called “性質”) 的命令. 不過, 你可能會想花點時間略讀 Chapter 8, 完整 Subversion 參考手冊, 以了解 Subversion 還有哪些不同的命令— 還有你能如何使用它們, 讓你的工作能夠輕鬆點.



[1] 嗯, 除了一件事, 就是 “工作複本” 中的每個目錄都會有一個 .svn 子目錄. 不過現在講這個言之過早.

[2] 當然囉, 沒有任何東西會真正地自檔案庫中刪除— 它只是在檔案庫的 HEAD 版本之後被刪除了. 你還是可以取回被刪除的東西, 只要取出 (或更新工作複本) 到比你刪除的修訂版還早的版本即可.

[3] Subversion 使用它自己內部的差異引擎, 預設使用的是統一差異格式. 如果你想要使用不同的差異格式輸出, 請以 --diff-cmd 指定外部差異程式, 並以 --extensions 選項傳遞任何你想要使用的旗標. 舉個例子, 要以文脈輸出格式 (context output format) 來看 foo.c 的本地端差異, 但是要忽略空白字元的更動, 你就可以執行 “svn diff --diff-cmd /usr/bin/diff --extensions '-bc' foo.c”.

[4] 你絕對可以自行移除暫存檔, 不過 Subversion 可以幫你作好的話, 你還會想要自己來嗎? 我們不這麼認為.

[5] And if you ask them for it, they may very well ride you out of town on a rail.

[6] 看到了吧? 我們跟你講過, Subversion 是一個時光機器.

Chapter 4. 分支與合併

分支, 標記, 以及合併, 幾乎都是所有版本控制系統的共通概念. 如果你對這些概念不了解, 我們在這一章提供了一個不錯的介紹. 如果你熟悉這些概念的話, 那麼我們希望你能對 Subversion 如何實作這些概念的作法感到有興趣.

分支是版本控制的基本部份. 如果你要讓 Subversion 來管理你的資料, 那麼你一定會依賴這個功能. 本章假設你已經了解 Subversion 的基本概念. (Chapter 2, 基本概念).

何謂分支?

假設你的工作是負責管理公司部門的文件, 某種使用手冊什麼的. 有一天, 另一個部門跟你要同一份使用手冊, 不過他們行事不同, 要的內容也有 '些微' 的不一樣.

在這種情況下, 你該怎麼辦? 你會使用直接了當的方法, 直接產生文件的第二份複本, 然後開始分別維護這兩份文件. 隨著各個部份要求你作些小修改, 你會將這些修改整入某一份, 或是另一份文件.

對這兩份文件, 你常會想要作同樣的修改. 舉個例子, 如果你在第一份複本裡看到一個錯字, 很有可能第二份複本裡也會有. 畢竟這兩份文件幾乎都一樣; 它們只有在特定的一小部份不一樣而已.

這就是 分支 的基本概念 — 換句話說, 有一條發展途徑與另一條是獨立的, 但是如果回溯的時間夠久, 你會發現它們都有共同的歷史紀錄. 一個分支都是以某樣東西的複本開始其生命週期, 然後就自行發展下去, 有著自己的歷史紀錄發生.

Figure 4.1. 發展的分支

發展的分支

Subversion 有一些命令, 可以幫助你維持檔案與目錄的平行分支. 它讓你藉由複製資料來產生分支, 並且會記住這些複本是彼此相關的. 它也會幫你從一個分支中, 重製更動到另一個分支去. 最後, 它還能夠讓工作複本的不同部份反映出不同的分支, 這樣你就能在你每日的工作中, "混合搭配" 不同的發展路徑.

使用分支

此時, 你應該了解, 每一次的送交都會在檔案庫建立一個全新的檔案系統樹 (稱為 "修訂版"). 如果不是的話, 請回去 the section called “修訂版本” 閱讀有關修訂版的概念.

我們會使用第 2 章的範例, 作為本章的例子. 請記得你和你的協同工作者 Sally, 兩個人都共用同一個檔案庫, 其中包含 paintcalc 兩個計劃. 不過這一次有人在檔案庫中, 建立了兩個最頂層的目錄, 稱為 trunkbranches. 這兩個計劃本身是 trunk 的子目錄, 我們稍候會解釋.

Figure 4.2. 起始檔案庫的配置

起始檔案庫的配置

就像前面的一樣, 假設你與 Sally 兩個人都有 /trunk/calc 計劃的工作複本.

假設你被指定了一個工作, 要對該計劃進行全面性的重新整理. 這需要花費許多時間撰寫, 而且會影響計劃中全部的檔案. 問題是你並不想妨礙到 Sally 的工作, 此時她正在修正隨處可見的臭蟲. 她所依賴的, 就是計劃的每一份最新修訂版都是可用的. 如果你開始送交你所作的更動, 肯定會打斷 Sally 的工作.

有個方法, 就是與世隔絕: 在一兩個星期中, 你和 Sally 不分享資訊. 也就是說, 開始在工作複本中修改並重整所有的檔案, 但是在完成工作之前, 不進行任何送交或更新. 這樣會有一些問題. 首先, 它並不安全. 大多數的人喜歡時常將他們的工作存回至檔案庫中, 以防工作複本發生什麼致命的意外. 如果你在不同的電腦上工作 (也許你在兩台不同的電腦上, 都有 /trunk/calc 的工作複本), 你就需要在這兩台電腦之間手動複製你的修改, 不然就是只在一台電腦上工作. 最後, 當你完成了之後, 你可能會發現送交更動是相當困難的. Sally (或其它人) 也許會在檔案庫作一些更動, 而它們很難合併到你的工作複本中 — 尤其是全部一口氣來的時候.

較好的方式, 就是在檔案庫中建立你自己的分支, 或稱為發展支線. 這讓你能夠常常儲存進行到一半的工作, 又不會妨礙到其它人, 而你還可以選擇性地與其它協同工作者分享資訊. 稍候你就可以了解, 這是如何運作的.

建立一個分支

建立一個分支相當地容易 — 利用 svn copy 命令, 在檔案庫中建立計劃的複本. Subversion 不僅能夠複製單一檔案, 整個目錄也沒有問題. 既然如此, 你會想要產生一份 /trunk/calc 目錄的複本. 新的複本該放在哪裡? 哪裡都可以 — 這實際上是計劃的原則. 我們假設你所屬團隊的原則, 是將分支置於檔案庫裡的 /branches/calc 區域, 而你想將分支命名為 "my-calc-branch". 你想要建立一個新的目錄 /branches/calc/my-calc-branch, 它一開始為 /trunk/calc 的複本.

建立一份複本有兩種不同的方法. 我們先從麻煩的方法開始, 讓這個概念能夠更清楚. 一開始, 請先取出檔案庫根目錄 (/) 的工作複本:

$ svn checkout http://svn.example.com/repos bigwc
A  bigwc/branches/
A  bigwc/branches/calc
A  bigwc/branches/paint
A  bigwc/trunk/
A  bigwc/trunk/calc
A  bigwc/trunk/calc/Makefile
A  bigwc/trunk/calc/integer.c
A  bigwc/trunk/calc/button.c
A  bigwc/trunk/paint
A  bigwc/trunk/paint/Makefile
A  bigwc/trunk/paint/canvas.c
A  bigwc/trunk/paint/brush.c
Checked out revision 340.

現在, 建立複本就只是將兩個工作複本路徑傳給 svn copy 命令:

$ cd bigwc
$ svn copy trunk/calc branches/calc/my-calc-branch
$ svn status
A  +   branches/calc/my-calc-branch

在這裡, svn copy 命令會將 trunk/calc 工作目錄裡所有的東西複製到 branches/calc/my-calc-branch. 你可以從 svn status 命令看得出來, 新的目錄現在預計要被新增至檔案庫中. 不過請注意字母 A 旁邊的 + 號. 它表示這預計要新增的項目, 實際上是某項目的 複本, 而不是全新的東西. 當你送交你的更動時, Subversion 會在檔案庫複製 /trunk/calc 以建立 /branches/calc/my-calc-branch, 而不是將所有工作複本資料再經由網路重傳一次.

$ svn commit -m "Creating a private branch of /trunk/calc."
Adding      branches/calc/my-calc-branch
Committed revision 341.

現在介紹的是另一個建立分支較容易的方法, 我們該在一開始就告訴你: svn copy 可以處理兩個 URL.

$ svn copy http://svn.example.com/repos/trunk/calc \
           http://svn.example.com/repos/branches/calc/my-calc-branch \
      -m "Creating a private branch of /trunk/calc"

Committed revision 341.

這兩個方法實際上沒什麼差別. 這兩個方法都在修訂版 341 中建立一個新的目錄, 而這個新目錄是 /trunk/calc 的複本. 但是呢, 請注意第二個方法進行的是 立即 送交. [7] 這個方法比較容易的原因, 是它不需要你先取出檔案庫的東西. 事實上, 這個技巧甚至不要求你得先有工作複本.

Figure 4.3. 有新複本的檔案庫

有新複本的檔案庫

與分支共事

現在你已經建立了一個新的計劃分支, 你可以取出新的工作複本, 然後開始使用它:

$ svn checkout http://svn.example.com/repos/branches/calc/my-calc-branch
A  my-calc-branch/Makefile
A  my-calc-branch/integer.c
A  my-calc-branch/button.c
Checked out revision 341.

這個工作複本沒有什麼特殊的地方; 它只是映射出另一個檔案庫的位置. 但是當你送交更動時, Sally 在更新時也不會看到它們. 她的工作複本是在 /trunk/calc.

讓我們假裝一個星期已經過去了, 而我們有以下的送交更動:

  • 你修改了 /branches/calc/my-calc-branch/button.c, 產生修訂版 342.

  • 你修改了 /branches/calc/my-calc-branch/integer.c, 產生修訂版 343.

  • Sally 修改了 /trunk/calc/integer.c, 產生修訂版 344.

現在 integer.c 有兩條獨立的發展支線:

Figure 4.4. 檔案歷史的分支

檔案歷史的分支

如果你看看你自己的 integer.c 複本的歷程紀錄, 事情變得滿有趣的:

$ pwd
/home/user/my-calc-branch

$ svn log integer.c
------------------------------------------------------------------------
r343:  user | 2002-11-07 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines

* integer.c:  frozzled the wazjub.

------------------------------------------------------------------------
r303:  sally | 2002-10-29 21:14:35 -0600 (Tue, 29 Oct 2002) | 2 lines

* integer.c:  changed a docstring.

------------------------------------------------------------------------
r98:  sally | 2002-02-22 15:35:29 -0600 (Fri, 22 Feb 2002) | 2 lines

* integer.c:  adding this file to the project.

------------------------------------------------------------------------

請注意 Subversion 會依時間回溯 integer.c 的歷程紀錄, 直到它被複製之時. (請記得你的分支於修訂版 341 時建立.) 現在看看 Sally 對她自己的檔案複本執行相同命令時, 會發生什麼事:

$ pwd
/home/sally/calc

$ svn log integer.c
------------------------------------------------------------------------
r344:  sally | 2002-11-07 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines

* integer.c:  fix a bunch of spelling errors.

------------------------------------------------------------------------
r303:  sally | 2002-10-29 21:14:35 -0600 (Tue, 29 Oct 2002) | 2 lines

* integer.c:  changed a docstring.

------------------------------------------------------------------------
r98:  sally | 2002-02-22 15:35:29 -0600 (Fri, 22 Feb 2002) | 2 lines

* integer.c:  adding this file to the project.

------------------------------------------------------------------------

Sally 會看到她自己的修訂版 344 的更動, 但是不會看到你在修訂版 343 的更動. 就 Subversion 的認知, 這兩個送交更動影響的是檔案庫裡不同位置的不同檔案. 但是 Subversion 顯示這兩個檔案有共同的歷史. 在修訂版 341 建立分支複本之前, 它們兩個是同一個檔案. 這也是為什麼你和 Sally 兩個都會看到修訂版 303 與 98.

事情的內涵

這一節中, 有兩點課題是你應該要記住的.

  1. 不像其它的版本控制系統, Subversion 的分支是以 正常的檔案系統目錄 存在於檔案庫裡, 而不是其它獨立的維度.

  2. Subversion 內部並沒有 "分支" 的概念—只有複本而已. 如果你複製了一個目錄, 結果的目錄會是 "分支", 只是因為 賦與它這個意義. 你可以把它想成不同的意義, 或是以不同的方式處理它, 但是對 Subversion 而言, 它只是一個因複製而產生的普通目錄而己.

在分支之間複製更動

現在你與 Sally 在同一專案的不同平行分支上工作: 你的是私有分支, 而 Sally 則是 主幹 (trunk), 也就是主要的發展線.

對於有眾多貢獻人員的專案來說, 大多數的人都有主幹的工作複本是很常見的. 要是有哪個人需要進行可能會妨礙到主幹的長期變動, 標準的作法是建立一個私有的分支, 然後將更動都送交至該分支, 直到工作結束為止.

所以, 好消息就是你和 Sally 兩個不會互相影響, 壞消息是這兩個很容易就跑得 遠了. 請記得 "與世隔絕" 策略的一個問題, 就是在你結束分支的工作之後, 幾乎不可能將你所作的更動, 在不產生大量衝突的情況下, 把它合併回主分支去.

相反地, 你和 Sally 應該在你工作時, 也一直分享彼此的更動. 哪些更動應該被分享, 完全由你來決定; Subversion 提供你在各個分支之間 "複製" 選擇性更動的能力. 而當你已經處理完你的分支時, 整個分支的所有更動就可以複製回主幹去.

複製特定的更動

在前一節中, 我們提到你與 Sally 兩個在不同的分支中, 都修改了 integer.c. 如果你看看 Sally 在修訂版 334 的記錄訊息, 你可以看到她修改了幾個拼字錯誤. 毫無疑問的, 你這相同檔案的複本也還有同樣的拼字錯誤. 很有可能你未來對這個檔案的更動, 是針對有拼字錯誤的地方, 那麼某一天你要合併分支的時候, 也就很有可能會產生衝突. 所以最好是現在就取得 Sally 的更動, 在你開始在同一地方進行大改 之前.

現在該是使用 svn merge 命令的時候. 這個命令結果是很像 svn diff 命令 (這在第 3 章就介紹過了). 這兩個命令都可以比較兩個檔案庫裡物件, 並表示它們之間的差異. 舉個例子, 你可以要求 svn diff 為你顯示 Sally 在修訂版 344 時所作的更動:

$ svn diff -r 343:344 http://svn.example.com/repos/trunk/calc

Index: integer.c
===================================================================
--- integer.c	(revision 343)
+++ integer.c	(revision 344)
@@ -147,7 +147,7 @@
     case 6:  sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break;
     case 7:  sprintf(info->operating_system, "Macintosh"); break;
     case 8:  sprintf(info->operating_system, "Z-System"); break;
-    case 9:  sprintf(info->operating_system, "CPM"); break;
+    case 9:  sprintf(info->operating_system, "CP/M"); break;
     case 10:  sprintf(info->operating_system, "TOPS-20"); break;
     case 11:  sprintf(info->operating_system, "NTFS (Windows NT)"); break;
     case 12:  sprintf(info->operating_system, "QDOS"); break;
@@ -164,7 +164,7 @@
     low = (unsigned short) read_byte(gzfile);  /* read LSB */
     high = (unsigned short) read_byte(gzfile); /* read MSB */
     high = high << 8;  /* interpret MSB correctly */
-    total = low + high; /* add them togethe for correct total */
+    total = low + high; /* add them together for correct total */
 
     info->extra_header = (unsigned char *) my_malloc(total);
     fread(info->extra_header, total, 1, gzfile);
@@ -241,7 +241,7 @@
      Store the offset with ftell() ! */
 
   if ((info->data_offset = ftell(gzfile))== -1) {
-    printf("error: ftell() retturned -1.\n");
+    printf("error: ftell() returned -1.\n");
     exit(1);
   }
 
@@ -249,7 +249,7 @@
   printf("I believe start of compressed data is %u\n", info->data_offset);
   #endif
   
-  /* Set postion eight bytes from the end of the file. */
+  /* Set position eight bytes from the end of the file. */
 
   if (fseek(gzfile, -8, SEEK_END)) {
     printf("error: fseek() returned non-zero\n");

svn merge 幾乎是完全一樣的. 但是它不會在終端機上顯示差異, 而是直接當成 本地修改 套用到你的工作複本上:

$ svn merge -r 343:344 http://svn.example.com/repos/trunk/calc
U  integer.c

$ svn status
M  integer.c

svn merge 的輸出, 顯示了你的 integer.c 複本已經修補過了. 它現在包含了 Sally 的更動 — 這個更動從主幹 "複製" 到你的私有分支內, 並以本地修改的形式存在. 此時, 你可檢視本地的更動, 確認它沒有問題.

在另一個場景中, 事情可能就不會這麼順利, 使得 integer.c 進入了衝突的狀態. 你必須使用標準的步驟 (參見第 3 章) 來解決衝突, 如果你認為合併根本不是個好主意的話, 直接捨棄它, 以 svn revert 來回復本地更動.

不過假裝你已經檢視過合併的更動, 你可以一如往常地使用 svn commit 來送交更動. 此時, 這個更動就被合併到你的 檔案庫分支. 在版本控制的術語中, 這個在不同的分支之間複製更動的動作, 稱為 移植 更動.

有件事要提醒一下: 雖然 svn diffsvn merge 的概念很類似, 但是它們在許多情況下的語法並不相同. 請閱讀第 8 章, 或是使用 svn help 以了解細節. 舉個例子, svn merge 需要一個工作複本路徑作為目標, 也就是要套用檔案樹更動的地方. 如果目標沒有指定的話, 它會假設你要執行以下的動作之一:

  1. 你想要合併目錄更動至目前的工作目錄中.

  2. 你想要合併特定檔案的更動至目前工作目錄中的同名檔案中.

如果你要合併一個目錄, 但是沒有指定目標路徑的話, svn merge 會假設第一種情況, 試著將更動套用到目前的工作目錄中. 如果你要合併一個檔案, 而那個檔案 (或同名檔案) 存在於目錄的工作目錄中, 那麼 svn merge 會假設第二種情況, 並試著將更動套用到同名的本地端檔案.

如果你要讓更動套用到別的地方, 你要這樣下:

$ svn merge -r 343:344 http://svn.example.com/repos/trunk/calc my-calc-branch
U   my-calc-branch/integer.c

重覆合併問題

合併更動聽起來很簡單, 但是實務上, 它可能讓你很頭痛. 問題在於如果你將更動從一個分支重覆複製到另一個分支的話, 你很有可能不小心合併相同的更動 兩次. 如果發生的話, 有可能並不會出什麼問題. 當 Subversion 套用更動時, 一般來說, 它會幫你記住這個檔案是已經有這個更動, 如果有的話, 不會發生任何事. 不過這個已套用的更動已經被更動的話, 你就會得到衝突. 理想的狀況下, Subversion 會自動防止重覆套用更動.

這是個困擾許多版本控制軟體的問題, 包括 CVS 與 Subversion. 目前而言, 在 Subversion 中, 唯一防止這個問題的方法, 就是記錄哪些更動已經合併了, 哪些還沒. 當你建立一個分支目錄時, 你必須記錄它是從哪個修訂版產生出來的 — 自己建立在別的地方. 當你合併一個修訂版 (或是一個範圍的修訂版) 到工作複本時, 你也得把它們記錄下來. 如果你忘了任何一個這樣的資訊, 你可以利用 svn log -v 分支目錄 的輸出來重新找出這個資訊. 但是這裡的重點, 是每一個後續的合併必須小心翼翼地手動建立, 以確定之前合併過的修訂版不會再重新合併一遍.

當然囉, Subversion 預計在發行版 1.0 之後, 再來解決這個問題. 所有這樣的合併資訊可以在性質描述資料中找到 (參見 the section called “性質”), 這樣 Subversion 有一天就能夠自動地防止重覆的合併.

合併整個分支

要讓我們所舉的實例更完整, 讓我們把時間往後拉. 幾天過去了, 主幹與你的私人分支都有了許多更動. 假設你已經完成你的私有分支; 它該有的功能, 或是臭蟲修正終於完成了, 現在你想把分支裡所有的更動都合併回主幹, 讓別人也能享用.

所以, 這種情況我們該如何使用 svn merge 呢? 請記住這個命令會比較兩個檔案樹, 然後把更動套用到工作複本去. 所以要接收這些更動, 你需要有主幹的工作複本. 我們假設你手邊還有一份 (完全更新的), 或者你已取出一份新的 /trunk/calc 工作複本.

但是要比較哪兩個檔案樹? 隨便一想, 答案似乎很明顯: 比較最新的主幹樹與你最新的分支樹就好了. 但是注意 — 這個想法是 錯的, 而且重創不少初學者! 由於 svn merge 的行為與 svn diff 相似, 比較最新的主幹與分支樹並 不會 直接給你你對分支所產生的更動: 它不只顯示出你新加入的分支更動, 還會 移除 你從未在分支作過的主幹更動.

想要表達只在你的分支中產生的更動, 你必須比較分支的初始狀態, 以及其最終狀態. 對你的分支執行 svn log , 你可以發現分支是在修訂版 341 產生的. 而分支的最終狀態, 只要使用修訂版 HEAD 即可.

那麼, 這就是最後的合併手續:

$ cd trunk/calc

$ svn merge -r 341:HEAD http://svn.example.com/repos/branches/calc/my-calc-bran
ch
U   integer.c
U   button.c
U   Makefile

$ svn status
M   integer.c
M   button.c
M   Makefile

[examine the diffs, compile, test, etc.]

$ svn commit -m "Merged all my-calc-branch changes into the trunk."
…

從檔案庫移除一個更動

svn merge 一個常用的用法, 就是回復一個已經送交的更動. 假設你以前在修訂版 303 所作的更動, 修改了 integer.c, 這根本就是錯誤的, 它根本就不該被送交. 你可以在工作複本中使用 svn merge 來 "反悔" 這個更動, 然後再將這個本地更動送交回檔案庫. 你只要指定一個 反向 的差異即可:

$ svn merge -r 303:302 http://svn.example.com/trunk/calc
U  integer.c

$ svn status
M  integer.c

$ svn commit -m "Undoing change committed in r303."
Sending    integer.c
Transmitting file data .
Committed revision 350.

一種想像檔案庫修訂版的方法, 就是將它們視為一群更動 (某些版本控制系統稱這為 更動組). 藉由使用 -r 選項, 你可以要求 svn merge 將一個更動組, 或是一個範圍的更動組, 套用到工作複本中. 在我們反悔更動的情況中, 我們要求 svn merge 逆向 套到更動組 #303 到我們的工作複本中.

你要謹記在心, 像這樣回復一個更動, 就跟其它的 svn merge 動作沒什麼兩樣, 所以你應該要使用 svn statussvn diff 來確認你的所作的跟你想要的是相同的, 然後再使用 svn commit 將最終的版本送交回檔案庫. 在送交之後, 該特定的更動組就不會再出現在 HEAD 修訂版中.

當然囉, 你可能會想: 呃, 這根本就不是反悔先前的送交, 不是嗎? 這個更動還是存在於修訂版 303 中. 如果某人取出了修訂版 303 與 349 之間的 某個 calc 計劃的版本, 就會看到這個錯誤的更動, 對吧?

是的, 沒有錯. 當我們講到 "移除" 一個更動時, 我們真正講的是 "從 HEAD 移除". 原先的更動還是會存在於檔案庫的歷程中. 大多數的情況下, 這樣已經夠好了. 大多數的人只對追蹤一個計劃的 HEAD 版本有興趣而已. 不過還是可能存在著特殊的情況, 你會希望完全地消除所有該送交的證據 (也許某人不小心送交了一個機密文件). 但是它非常地不容易, 因為 Subversion 的設計, 就是不會丟棄任何資訊. 修訂版是不可變動的檔案樹, 它們一個個建立在另一個之上. 從歷史進程移除一個修訂版會產生骨牌效應, 會對所有後續的修訂版造成混亂, 還有可能讓所有的工作複本都無效. [9]

切換工作複本

svn switch 命令可將現有的工作複本切換到不同的分支去. 雖然這個命令與分支運作沒什麼很直接的關係, 但是對使用者來說, 是個滿不錯的捷徑. 我們早先的例子中, 在建立了自己的私有分支之後, 你要從新的檔案庫目錄取出一個全新的工作複本. 但是你也可以改為要求 Subversion, 將你的 /trunk/calc 的工作複本改為映射到新的分支位置去:

$ cd calc

$ svn info | grep URL
URL: http://svn.example.com/trunk/calc

$ svn switch http://svn.example.com/branches/calc/my-calc-branch
U   integer.c
U   button.c
U   Makefile
Updated to revision 341.

$ svn info | grep URL
URL: http://svn.example.com/branches/calc/my-calc-branch

在 "切換" 到指定的分支之後, 你的工作複本與取出全新的目錄沒有什麼差別. 而且使用這個命令是更有效率的, 因為分支通常都只有小部份不同而己. 伺服器只會送出最小的, 足以讓你的工作複本反映出分支目錄的更動集合.

svn switch 命令也接受 --revision (-r) 選項, 所以工作複本並不一定得是分支的 “尖端”.

當然囉, 大多數的專案要比你的 calc 範例要複雜得多了, 包括了許多子目錄. Subversion 的使用者在使用分支時, 通常都依循一個特定的演算法:

  1. 將整個專案 '主幹' 複製到新的分支目錄.

  2. 只切換 部份 的主幹工作複本, 以反映分支.

換句話說, 如果使用者知道分支的工作只需要在某個特定子目錄下發生的話, 他們可以用 svn switch, 將他們工作複本的子目錄轉移到某特定的分支去. (有的時候, 使用者可能只要將某個工作檔案切換到分支去!) 這樣, 他們大部份的工作複本還是可以繼續接受正常的 '主幹' 更新, 但是被切換的部份就會保持不動 (除非有對該分支的送交更動). 這個功能對 "混合工作複本" 的概念, 提供了全新的維度, 工作複本不只是可以混合不同的工作修訂版, 還可以混合不同的檔案庫位置.

如果你的工作複本包含了來自不同檔案庫位置的切換子檔案樹, 它們還是能夠正常地工作. 當你更動時, 你會從每個子檔案樹收到修補更新. 當你送交更動時, 你的本地修改還是會以單一的, 不可分割的更動送回檔案庫.

請注意一點, 雖然你的工作複本可以對映到混合的檔案庫位置, 但是它們必須全在 同一個檔案庫中. Subversion 的檔案庫還無法彼此溝通; 這個功能預計在 Subversion 1.0 之後加入. [10]

由於 svn switch 實際上是 svn update 的變形, 它們有相同的行為; 當有新的資料從檔案庫來的時候, 任何在工作複本裡的本地修改還是會保存下來. 這可以讓你進行各式各樣的妙技.

舉個例子, 假設你有一個 /trunk 的工作複本, 並且對它進行了一些修改, 然後你突然發現, 應該修改的是分支才對. 沒問題! 當你以 svn switch 來切換工作複本到分支時, 本地更動還是會存在. 此時你就可以進行測試, 然後將它們送交回分支去.

標記

另一個常見的版本控制概念, 就是 標記. 標記只是一個計劃在時間軸上的 "快照". 在 Subversion 中, 這個概念已經隨處都是了. 每一個檔案庫的修訂版, 就是檔案系統在每一次送交之後的快照.

但是呢, 人們通常會想要標記一個便於記憶的名稱, 像是 "release-1.0". 而且他們只想要的, 只是一部份檔案系統子目錄的快照而已. 要記住某一個軟體的 release-1.0 版, 對應的是修訂版 4822 裡的某個特定子目錄, 畢竟是不太容易的.

建立一個簡單的標記

再一次地, svn copy 又來解救眾生了. 如果你想要建立一個完全符合 HEAD 修訂版的 /trunk/calc 的快照, 那就建立一個它的複本:

$ svn copy http://svn.example.com/repos/trunk/calc \
           http://svn.example.com/repos/tags/calc/release-1.0 \
      -m "Tagging the 1.0 release of the 'calc' project."

Committed revision 351.

這個例子假設 /tags/calc 的目錄已經存在了. 在複製動作完成後, 新的 release-1.0 目錄就永遠是這個計劃的快照, 完全符合你建立複本時的 HEAD 修訂版的內容. 當然囉, 你可能想要更準確地指定你所複製的修訂版, 以免別人在你沒注意時, 又送交了幾個更動. 所以如果你知道 /trunk/calc 的 350 修訂版, 就是你想要的快照的話, 你可以藉由傳給 svn copy 命令的 -r 350 選項來指定它.

不過先等一下: 這個建立標記的步驟, 不是和我們用來建立分支的步驟是一樣的嗎? 沒錯, 事實上, 根本就是一樣的. 在 Subversion 中, 標記與分支是沒有任何分別的. 兩者都只是透過複製而建立的尋常目錄而已. 就像分支一樣, 被複製的目錄之所以為 "標記", 只是因為 人們 決定它是: 只要沒有人再送交更動到這個目錄中, 它就永遠是個快照而已. 如果有人開始送交更動進去, 它就會變成一個分支.

如果你管理一個檔案庫的話, 有兩種方法可以用來管理標記. 第一個方法是 "放任": 把它視為計劃的原則, 決定標記放置的位置, 確定所有的使用者都知道如何處理複製進去的目錄. (也就是說, 確定他們都知道別送交更動進去.) 第二個方法就很偏執: 你可以利用 Subversion 內附的存取控制的腳本檔, 讓別人只能在標記區域建立新的複本, 但是無法送交東西進去. (###cross-ref a section that demonstrates how to use these scripts?). 不過呢, 偏執的方法並不是那麼必要的. 如果一個使用者不小心送交更動到標記目錄, 你只要像前一節所講的, 把它回復回來即可. 畢竟, 這可是版本控制軟體.

建立一個複雜的標記

有的時候, 你想要你的 "快照" 要比單一修訂版的單一目錄要來得複雜一點.

舉例來說, 假設你的計劃比我們的 calc 例子要大得多: 也許它包含了幾個子目錄, 以及許多的檔案. 在你工作之間, 你可能決定要建立一個工作複本, 它有特定的功能, 以及特定的臭蟲修正. 想要達到這樣的目的, 你可以藉由選擇性地將檔案與目錄固定在某一個修訂版, (大方地使用 svn update -r), 或是將目錄與檔案切換到某一個分支 (使用 svn switch). 當你完成之後, 你的工作複本就會變成來自不同版本的不同檔案庫位置的大雜燴. 但是在測試過後, 你確定這就是你需要的資料組合.

現在是作快照的時候了. 這裡無法將一個 URL 複製到另一個去. 在這個情況中, 你想要建立一個你現在調整的工作複本的快照, 並將它存到檔案庫中. 很幸運的, svn copy 事實上有四種不同的使用方法 (你可以參考第 8 章), 包括將工作複本的檔案樹複製到檔案庫的功能:

$ ls
./    ../   my-working-copy/

$ svn copy my-working-copy http://svn.example.com/repos/tags/calc/mytag

Committed revision 352.

現在在檔案庫中, 有了一個新的目錄 /tags/calc/mytag, 它就是完全符合你的工作複本的快照 — 混合的修訂版, url, 所有的東西.

有別的使用者找出這個功能的有趣用法. 有的情況下, 你的工作複本可能有一群本地更動, 而你想讓你的協同工作者看看. 除了執行 svn diff, 將修補檔送過去以外 (這沒辦法捕捉到檔案樹的變化), 你可以改用 svn copy 來 "上載" 你的工作複本至某一個檔案庫的私有區域. 你的協同工作者就可以取出一個一模一樣的工作複本, 或是利用 svn merge 來接收你所作的更動.

分支維護

你大概注意到, Subversion 相當地有彈性. 因為它以相同的機制 (目錄複本) 來實作分支與標記, 也因為分支與標記是出現在正常的檔案系統空間中, 有許多人覺得 Subversion 真是太可怕了. 它可以說是 有彈性了. 在本節中, 我們提供了幾點建議, 作為你依時間來安排與管理資料的參考.

檔案庫配置

管理檔案庫有數種標準建議的方法. 大多數人會建立一個 trunk 的目錄來放置 "主要" 的發展途徑, 建立一個 branches 目錄來放置分支複本, 以及建立一個 tags 目錄來放置標記複本. 如果一個檔案庫只放置一個計畫, 那麼大多數的人會建立這樣的最頂層目錄:

/trunk
/branches
/tags

如果一個檔案庫包含了多個計劃, 那麼人們建立目錄配置的方法, 會以分支為準:

/trunk/paint
/trunk/calc
/branches/paint
/branches/calc
/tags/paint
/tags/calc

…或是依計劃:

/paint/trunk
/paint/branches
/paint/tags
/calc/trunk
/calc/branches
/calc/tags

當然囉, 你還是有忽略這些常用配置的自由. 你可以自己建立各種變化, 只要最能符合你與你的團隊需要即可. 請記住, 不管你作什麼樣的選擇, 都不表示就永遠都不可變更. 不管何時, 你都可以重新整理你的檔案庫. 由於分支與標記只是普通的目錄, 你可以利用 svn move 命令來移動或更名這些目錄. 從一種配置換到另一種, 只是對伺服端進行一連串的移動而己; 如果你不喜歡檔案庫現在的目錄結構, 直接動它們吧.

資料生命週期

另一個Subversion 模型的特色, 就是分支與標記可以是有限生命週期的, 就像其它受控管的項目一樣. 舉個例子, 假設你最後完成了個人 calc 計劃分支裡的工作. 在將你所有的更動送交回 /trunk/calc 後, 你的私有分支目錄就沒有必要再存在了:

$ svn delete http://svn.example.com/repos/branches/calc/my-calc-branch \
             -m "Removing obsolete branch of calc project."

Committed revision 375.

現在, 你的分支就消失了. 它當然不是真的不見了: 這個目錄只是從 HEAD 修訂版消失, 不會再引起別人的注意. 如果你檢視更早一點的修訂版 (使用 svn checkout -r, svn switch -r, 或是 svn list -r), 你還是可以看到以前的分支.

如果只是瀏覽被刪除的目錄還不夠, 你還是可以再讓它回來. Subversion 要恢復舊資料相當地容易. 如果你想把被刪除的目錄 (或檔案) 叫回 HEAD 修訂版, 只要使用 svn copy -r 將它從以前的修訂版複製回來就好了:

$ svn copy -r 374 http://svn.example.com/repos/branches/calc/my-calc-branch \
                  http://svn.example.com/repos/branches/calc/my-calc-branch

Committed revision 376.

在我們的例子中, 你的個人分支有個滿短的生命週期: 你可能只是把它建立起來, 用來修正臭蟲, 或是實作一個新的功能. 在工作完成後, 這個分支也沒有存在的必要了. 不過在軟體發展的過程中呢, 長時間同時有兩個 "主要" 的發展主線也是很常見的. 舉個例子, 假設現在該對外發佈一個穩定的 calc 的版本, 而你也知道大概要花好幾個月才能把臭蟲都清掉. 你不想讓別人對這個計劃加新功能, 但是你也不想叫所有的發展人員都停止工作, 所以你改為計劃建立一個不會有太大變更的 "stable" 分支:

$ svn copy http://svn.example.com/repos/trunk/calc \
         http://svn.example.com/branches/calc/stable-1.0
         -m "Creating stable branch of calc project."

Committed revision 377.

現在設計人員可以繼續自由地將最先進的 (或實驗性的) 功能加到 /trunk/calc 中, 你也可以宣佈 /branches/calc/stable-1.0 只接受臭蟲修正的計劃原則. 也就是說, 大家還是可以繼續對主體工作, 但是選擇性將臭蟲修正移植到穩定分支去. 就算在穩定分支已經送出去了, 你可能還是得繼續維護該分支很長一段時間 — 換句話說, 只要你繼續為客戶支援該發行版本, 就得繼續維護.

摘要

我們在本章中, 涵蓋了相當大的範圍. 我們討論了標記與分支的概念, 也示範了 Subversion 如何藉由透過 svn copy 命令複製目錄, 實作這樣的概念. 我們示範如何使用 svn merge 命令, 從一個分支複製更動到另一個分支, 或是回復錯誤的更動. 我們也展示了以 svn switch 來建立混合位置的工作複本. 我們還提到在檔案庫中, 如何管理分支的結構與其生命週期.

請注意 Subversion 的 mantra: 分支是廉價的. 所以請大方地使用它們吧!



[7] Subversion 不支援跨檔案庫的複製. 當 svn copysvn move 與 URL 一同使用時, 你只能複製同一個檔案庫裡的項目.

[8] 未來呢, Subversion 打算使用 (或發明) 可以描述檔案樹更動的擴充修補格式.

[9] 不過 Subversion 計劃還是有個計劃, 打算在某天實作 svnadmin obliterate 命令, 可以達到永久刪除資訊的任務.

[10] 要是伺服器的 URL 變更了, 而你又不想拋棄現有的工作複本, 你 可以svn switch--relocate 一同使用. 請參閱 Chapter 8, 完整 Subversion 參考手冊svn switch 一節, 以瞭解更多的資訊.

Chapter 5. Repository 管理

Subversion 的檔案庫是個中央倉儲, 用來存放任意數量專案的受版本控管資料. 因為如此, 它成為管理員集中注意的焦點. 檔案庫一般並不需要太多的照顧, 但是了解如何適當地設定它, 照顧它是很重要的, 如此才能避免一些潛在性的問題, 而實際的問題得以安全地解決.

在本章中, 我們會討論如何建立與設定 Subversion 的檔案庫, 以及如何開放檔案庫的網路存取. 我們也會提到檔案庫的維護, 包括 svnlooksvnadmin 工具的使用方法 (它們都包含 Subversion 中). 我們也會說明常見的問題與錯誤, 並提供幾個如何安排檔案庫資料的建議.

如果你存取 Subversion 的檔案庫的目的, 只是打算成為一個單純將資料納入版本控管的使用者的話 (也就是透過 Subversion 用戶端), 那你可以完全跳過本章. 但是如果你是, 或想要成為 Subversion 的檔案庫管理員, [11] 本章絕對是你要投注注意力的地方.

當然了, 一個人無法成為檔案庫的管理員, 除非他有檔案庫可管理.

檔案庫的基本知識

了解異動與修訂版

就概念來講, Subversion 的檔案庫是一連串的目錄樹. 每一個目錄樹, 就是檔案庫的目錄與檔案於不同時間點的快照. 這些快照是使用者進行作業的結果, 稱為修訂版.

每一個修訂版, 是以異動樹 (transaction tree) 開始其生命週期. 在送交發生時, Subversion 用戶端會建立一個異動, 其中反映了本地端的更動 (以及自用戶端開始進行送交的任何額外更動), 然後指示檔案庫將該樹儲存為下一個快照. 如果送交成功的話, 這個異動就會實際成為新的修訂版樹, 並被賦與新的修訂版號. 如果送交因為某些原因失敗的話, 這個異動會被捨棄, 用戶端會被通知該動作失敗.

更新的動作也類似這樣. 用戶端會建立一個暫時的異動樹, 反映工作複本的狀態. 接著, 檔案庫會將異動樹與指定的修訂版樹作比較 (通常是最新的, 或是 “最年輕的” 樹), 然後送回指示用戶端該如何進行何種更動的資訊, 以將工作複本轉換成該修訂版樹的樣子. 在更新完成後, 這個暫時的異動樹就會被刪除.

使用異動樹, 是對檔案庫的版本控制檔案系統產生永久更動的唯一方法. 但是, 了解異動的生命週期極富彈性是很重要的. 在更動的情況下, 異動只是馬上會被消滅的暫時檔案樹而已. 在送交的情況下, 異動會變成固定的修訂版 (如果失敗的情況下, 則是被移除). 如果有錯誤或是臭蟲的話, 異動很有可能不小心遺留在檔案庫之中 (雖然不會影響什麼東西, 但是會佔用空間).

理論上, 某一天整個流程能夠發展出對異動生命週期能夠有更細部的控制. 可以想像一下成一個系統, 在用戶端已經描述完它對檔案庫所產生的更動後, 每個無法成為修訂版的異動會先放到某個地方. 如此, 每個新的送交就可以被某個人檢閱, 也許是主管, 也許是工程品管小組, 他們可以決定是要接受這個異動成為修訂版, 還是捨棄它.

這些跟檔案庫管理有什麼關係呢? 答案很簡單: 如果你要管理一個 Subversion 的檔案庫, 除了監控檔案庫的情況外, 你還必須檢視修訂版與異動情況.

無版本控制的性質

Subversion 檔案庫的異動與修訂版也可以附加性質上去. 這些性質只是基本的鍵值對應, 可以用來儲存與對應檔案樹有關的資料. 這些鍵值與你的檔案樹資料一樣, 都是儲存在檔案庫的檔案系統之中.

對於儲存某些檔案樹的資料, 但是這些資料並不完全與這些目錄與檔案相關時, 修訂版與異動性質是很有用的 — 像是不被用戶端工作複本管理的性質. 例如, 當一個新的送交異動於檔案庫中建立時, Subversion 會對這個異動新增一個名為 svn:date 的性質 — 一個用來表示異動何時建立的時間戳記. 當送交程序結束後, 而該異動被提昇為一個固定的修訂版時, 這個檔案樹會再多出儲存修訂版作者的使用者名稱性質 (svn:author), 以及一個用來儲存該修訂版的紀錄訊息性質 (svn:log).

修訂版與異動性質都是 無版本控制的性質 (unversioned property) — 因為它們被修改後, 原先的值就永遠被捨棄了. 另外, 雖然修訂版樹本身是不會再變的, 附加在它們上的性質卻不是. 你可以在日後對修訂版性質作新增, 移除, 以及修改的動作. 如果你送交了一個新的修訂版, 但是日後發現紀錄訊息寫錯了, 或是有拼字錯誤的話, 你可以直接以正確的新訊息蓋過 svn:log 的值.

檔案庫的建立與設定

建立一個 Subversion 的檔案庫出乎意料地簡單. Subversion 所提供的 svnadmin 工具, 有個專門處理這件事的子命令. 要建立一個新的檔案庫, 只要執行:

$ svnadmin create path/to/repos

這會在目錄 path/to/repos 裡建立一個新的檔案庫. 這個新的檔案庫會以修訂版 0 開始其生命週期, 裡面除了最上層的根目錄 (/), 什麼都沒有. 剛開始, 修訂版 0 還有一個單一的修訂版性質 svn:date, 會設定在檔案庫初建立的時間.

你可能注意到 svnadmin 的路徑引數只是一個普通的檔案系統路徑, 而不是像 svn 用戶端程式用來表示檔案庫的 URL. svnadminsvnlook 都被視為是伺服器端的工具 — 它們是在檔案庫所在的機器上, 對檔案庫作檢視或修改之用. Subversion 新手常犯的錯誤, 就是試著將 URL (即使是 "本地端" 的 file: 路徑) 傳給這兩個程式.

所以, 在你執行 svnadmin create 命令之後, 這個目錄中就會有全新的 Subversion 檔案庫. 讓我們看一下在這個目錄裡產生了什麼東西.

$ ls repos
dav/  db/  format  hooks/  locks/  README.txt

除了 README.txtformat 檔以外, 檔案庫是由一群子目錄組成. 就像 Subversion 其它部份的設計一樣, 模組化是很重要的原則, 而且階層式組織要比雜亂無章好. 以下是新的檔案庫目錄中, 各個項目的簡單敘述:

dav

提供給 Apache 與 mod_dav_svn 使用的目錄, 讓它們儲存內部資料.

db

主要的 Berkeley DB 環境, 裡面都是儲存 Subversion 檔案系統 (就是你置於版本控制的全部資料所在) 的資料庫表格.

format

一個內容為一個整數的檔案, 表示檔案庫配置的版本號碼.

hooks

一個放置 hook 腳本檔範本的目錄 (如果你有安裝的話, 還有腳本檔本身的檔案).

locks

用來放置 Subversion 檔案庫鎖定資料的目錄, 用來追蹤存取檔案庫的用戶端.

README.txt

這個檔案只是用來告知使用者, 他們在看的是 Subversion 的檔案庫.

一般來說, 你不需要自已 “手動” 處理檔案庫. svnadmin 工具就足以用來處理對檔案庫的任何改變, 不然也可以使用協力廠商的工具 (像是 Berkeley DB 工具組) 來調整部份的檔案庫. 不過還是有些例外, 我們會在這裡提到.

Hook scripts

Hook scripts

掛勾 (hook) 是某些檔案庫事件所觸發的程式, 像是建立新的修訂版時, 修改未納入版本控制的性質時. 每一個掛勾都會得到足夠的資訊, 可以分辨出得到的是什麼事件, 針對哪個 (哪些) 目錄, 以及被誰觸發. 依掛勾輸出或傳回狀態的不同, 掛勾程式可以繼續, 停止, 或是暫停該動作.

hook 子目錄中, 預設是放置各個檔案庫掛勾的範本.

$ ls repos/hooks/
post-commit.tmpl          pre-revprop-change.tmpl
post-revprop-change.tmpl  start-commit.tmpl
pre-commit.tmpl           

每一個 Subversion 檔案庫實作出來的掛勾, 都各自有對應的範本, 只要檢視這些範本的內容, 你就知道每一個命令稿是被什麼觸發的, 什麼資料會傳給命令稿. 每個範本還包含包含一些範例, 示範如何與其它的 Subversion 隨附工具一同完成某些常見工作. 要安裝一個真正可用的掛勾程式, 只要把可執行檔案或命令稿置於 repos/hooks 目錄下, 並且讓它能以掛勾的名稱 (像是 start-commitpost-commit) 執行即可.

在 Unix 平台上, 這表示提供與掛勾完全同名的命令稿或是程式即可 (可以是 shell 命令稿, Python 程式, 編譯過的 C 二進制程式, 或是任何的可能). 當然囉, 範本檔案存在的目的, 並不只有提供資訊而已 — 在 Unix 平台上安裝掛勾最簡單的方法, 就是把適當的範本拷貝成去掉 .tmpl 副檔名的新檔案, 修改掛勾的內容, 然後確定命令稿是可執行的. 但是 Windows 是利用檔案副檔名來決定它是不是可執行檔, 所以你必須提供主檔名為掛勾名稱的程式, 再加上 Windows 認為是可執行檔的副檔名, 像是視為程式的 .exe.com, 以及批次檔的 .bat.

目前 Subversion 檔案庫實作的掛勾有五個:

start-commit

這個掛勾在送交異動還沒建立之前就會執行, 通常用來決定使用者是否有送交的權限. 檔案庫會傳遞兩個引數給這個程式: 檔案庫的路徑, 以及想要進行送交的使用者名稱. 如果程式傳回一個非零的結束值, 在送交異動還沒建立之前, 送交就會結束.

pre-commit

本掛勾執行的時間為異動完成之後, 送交之前. 一般來講, 這個掛勾是用來阻止因內容或位置而不被允許的送交 (舉個例子, 你的站台可能要求某個分支的送交都必須包含臭蟲追蹤的申請單號碼, 或是進來的記錄訊息不可為空白的). 檔案庫會傳遞兩個引數給這個程式: 檔案庫的路徑, 以及準備送交的異動名稱. 如果程式傳回一個非零的結束值, 送交會被中止, 而異動會被刪除.

Subversion 的發行檔案包含了幾個存取控制的命令稿 (位於 Subversion 源碼樹的 tools/hook-scripts 目錄中), 可在 pre-commit 中使用, 以進行更細微的存取控制. 在這個時候, 除了 httpd.conf 所提供的以外, 這是管理員唯一可以進行細微的存取控制的方法. 在未來的 Subversion 中, 我們計劃直接在檔案系統實作 ACL。

post-commit

本掛勾執行的時間是在異動送交, 新修訂版被建立之後. 大多數的人用這個掛勾來寄出關於本次送交的電子郵件, 或是建立檔案庫的備份. 檔案庫會傳遞兩個引數給這個程式: 檔案庫的路徑, 以及新建立的修訂版號. 本程式的結束碼會被忽略.

Subversion 的發行檔案包含了一個 commit-email.pl 命令稿 (位於 Subversion 源碼樹的 tools/hook-script 目錄中), 可以用來寄送包含描述指定送交的電子郵件. 這個郵件包含了更動路徑列表, 該送交所對應的記錄訊息, 使用者, 送交的日期,以及一個以 GNU diff 樣式表示的本次更動差異.

另一個 Subversion 隨附的有用工具是 hot-backup.py 命令稿 (位於 Subversion 源碼樹的 tools/backup/ 目錄內). 這個命令稿可以進行 Subversion 檔案庫的即時備份 (這是 Berkeley DB 資料庫後端支援的功能), 可以用來建立每一次送交的檔案庫快照, 作為歸檔紀錄, 或是緊急回復之用.

pre-revprop-change

由於 Subversion 的修訂版性質並未納入版本控制, 修改這樣的性質 (舉個例子, svn:log 送交訊息性質) 將會永遠地覆寫原先的數值. 由於資料有可能會消失, Subversion 提供了這個掛勾 (以及相對應的 post-revprop-change), 以便檔案庫管理員能夠依其意願, 利用其它外部機制來記錄曾作過的更動.

這個掛勾在檔案庫即將發生更動之前執行. 檔案庫會傳遞四個引數給這個掛勾: 檔案庫的路徑, 更動性質所在的修訂版, 產生更動的認證使用者名稱, 以及性質名稱本身.

post-revprop-change

如同我們早先提過的, 這個掛勾是 pre-revprop-change 的對應掛勾. 事實上, 為了偏執狂著想, 如果 pre-revprop-change 不存在的話, 這個命令稿不會執行. 這兩個掛勾都存在的話, post-revprop-change 掛勾只會在修訂版性質更動之後才會執行, 一般是用來寄送包含更動性質的新數值的電子郵件. 檔案庫會傳遞四個引數給這個掛勾: 檔案庫的路徑, 該性質所在的修訂版, 產生更動的認證使用者名稱, 以及性質名稱.

Subversion 的發行檔案包括了一個 propchange-email.pl 命令稿 (位於 Subversion 源碼樹的 tools/hook-scripts/ 目錄中), 可用來寄送包含修訂版性質更動細節的電子郵件 (或/且附加至記錄檔中). 這個郵件會包含更動性質所在的修訂版與名稱, 產生更動的使用者, 以及新的性質數值.

Subversion 會試著以正在存取 Subversion 檔案庫的使用者身份來執行掛勾程式. 在大多數的情況下, 檔案庫是透過 Apache HTTP 伺服器與 mod_dav_svn 存取的, 所以使用者會與 Apache 執行的使用者身份相同. 掛勾程式本身必須以作業系統層級的權限進行設定, 以便讓使用者可以執行. 另外, 這也表示任何會被掛勾程式直接或間接存取的檔案或程式 (包括 Subversion 檔案庫本身), 都會以同一個使用者的身份來進行存取. 換句話說, 請注意任何因檔案權限而可能造成的潛在問題, 以免掛勾無法進行你想進行的工作.

Berkeley DB 設定

Berkeley DB 環境有它自己預設的設定值, 像是任何時間可使用的鎖定數目, 或是 Berkeley 日誌記錄檔的截止大小等等. Subversion 檔案系統的程式會額外地為幾個 Berkeley DB 設定選項選擇其它的預設值. 不過, 你的檔案庫可能會有自己獨特的資料集合與存取型態, 也許就需要不同的設置選項數值.

Sleepycat (Berkeley DB 的製造廠商) 的人員瞭解不同的資料庫有不同的需求, 所以他們提供了一種執行時期的機制, 可以更動許多 Berkeley DB 環境的設定選項數值. Berkeley 會檢查每個環境目錄是否存在著一個名為 DB_CONFIG 的檔案, 然後剖析其中的為某個特定 Berkeley 環境所用的選項.

檔案庫的 Berkeley 設定檔位於 db 環境目錄中, 也就是 repos/db/DB_CONFIG. Subversion 在建立檔案庫時, 就會自行建立這個檔案. 這個檔案一開始包含了幾個預設值, 也包含了幾個 Berkeley DB 線上文件的參照, 這樣你可以自己去查這些選項所代表的意義. 當然囉, 你可以任意在 DB_CONFIG 檔案裡加入任何支援 Berkeley DB 選項. 不要請記得, 雖然 Subversion 不會試著去讀取或解讀這個檔案的內容, 或是使用任何裡面的選項, 你還是得避免某些設定選項, 讓 Berkeley DB 產生出 Subversion 不知如何處理的行為. 另外, 對 DB_CONFIG 的更動並不會馬上生效, 除非你回復了資料庫環境 (使用 svnadmin recover.)

檔案庫維護

管理員的工具箱

svnlook

svnlook 是 Subversion 提供的工具, 用來檢視檔案庫不同的修訂版與異動. 本程式完全不會試著去修改檔案庫 — 這是 “唯讀” 工具. svnlook 通常用在檔案庫掛勾程式中, 用來回報檔案庫即將送交的更動 (用在 pre-commit 掛勾時), 或剛送交的更動 (用在 post-commit 掛勾時). 檔案庫管理員也許會將這個工具用於診斷之用.

svnlook 的語法很直接:

$ svnlook help
general usage: svnlook SUBCOMMAND REPOS_PATH [ARGS & OPTIONS ...]
Note: any subcommand which takes the '--revision' and '--transaction'
      options will, if invoked without one of those options, act on
      the repository's youngest revision.
Type "svnlook help <subcommand>" for help on a specific subcommand.
…

幾乎每一個 svnlook 的子命令可以針對修訂版或異動樹運作, 顯示檔案樹的資訊, 或是它與檔案庫的前一個修訂版之間有什麼差異. 你可以使用 --revision--transaction 選項來指定要檢視的修訂版與異動. 請注意, 雖然修訂版號看起來像是自然數, 但是異動名稱是包含英文字母與數字的字串. 請記得檔案系統只允許瀏灠未送交的異動 (未變成新修訂版的異動). 大多數的檔案庫不會有這樣的異動, 因為異動不是已送交了 (這讓它們失去被檢視的資格), 就是被中止然後移除.

如果完全沒有 --revision--transaction 選項的話, svnlook 會檢視檔案庫中最年輕的 (或 “HEAD”) 修訂版. 所以這兩個命令作的事情都一樣, 如果 19 是 /path/to/repos 檔案庫的最年輕修訂版:

$ svnlook info /path/to/repos
$ svnlook info /path/to/repos --revision 19

這些子命令規則的唯一例外, 就是 svnlook youngest 子命令, 它不需要指定選項, 就只會印出 HEAD 修訂版號.

$ svnlook youngest /path/to/repos
19

svnlook 的輸出是設計為人類與機器都易讀的. 以 info 子命令的輸出作為例子:

$ svnlook info path/to/repos
sally
2002-11-04 09:29:13 -0600 (Mon, 04 Nov 2002)
27
Added the usual
Greek tree.

info 的輸出命令定義如下:

  1. 作者, 後接換行字元.

  2. 日期, 後接換行字元.

  3. 紀錄訊息的字元數目, 後接換行字元.

  4. 紀錄訊息, 後接換行字元.

這個輸出是人類可解讀的, 像是日期戳記等具有意義的項目, 皆以文字形式表示, 而不是用看不懂的方式 (像是某個可愛外星人出現至今的毫微秒計數). 但是這個輸出也是機器可讀 — 因為紀錄訊息可以有好幾行, 沒有長度的限制, 所以 svnlook 在訊息之前提供了訊息的長度. 這讓命令稿與其它包裝這個程式的命令稿, 可以對記錄訊息作出聰明的決定, 要是這段資料不是串流的最後一個部份時, 至少也知道要略過幾個位元組.

另一個 svnlook 常見的用法, 就是檢視某個修訂版或異動樹的內容. svnlook tree 會顯示要求檔案樹的目錄與檔案系統 (還可以選擇顯示每一個路徑的檔案系統節點修訂版編號), 藉由檢視其輸出, 對於管理員決定是否可以移除某個看起來無效的異動是很有用的, 對 Subversion 發展人員在診斷發生檔案系統相關的問題時也很有用.

$ svnlook tree path/to/repos --show-ids
/ <0.0.1>
 A/ <2.0.1>
  B/ <4.0.1>
   lambda <5.0.1>
   E/ <6.0.1>
    alpha <7.0.1>
    beta <8.0.1>
   F/ <9.0.1>
  mu <3.0.1>
  C/ <a.0.1>
  D/ <b.0.1>
   gamma <c.0.1>
   G/ <d.0.1>
    pi <e.0.1>
    rho <f.0.1>
    tau <g.0.1>
   H/ <h.0.1>
    chi <i.0.1>
    omega <k.0.1>
    psi <j.0.1>
 iota <1.0.1>

svnlook 還可以進行其它的查詢, 顯示我們早先提到過的資訊的一部份, 回報某個指定的修訂版或異動中更動的路徑, 顯示檔案與目錄產生的文字與性質的差異, 諸如此類的資訊. 以下是目前 svnlook 所支援的子命令, 簡單的描述, 以及他們輸出的東西:

author

顯示檔案樹的作者.

cat

顯示檔案樹裡的檔案內容.

changed

列出檔案樹中, 所有更動的檔案與目錄.

date

顯示檔案樹的日期戳記.

diff

顯示更動檔案的統一差異格式.

dirs-changed

顯示檔案樹裡本身更動的目錄, 或是其下子檔案有更動的目錄.

history

顯示某個納入版本控制路徑的歷程紀錄點 (更動或複製發生的地方).

info

顯示檔案樹的作者, 日期戳記, 紀錄訊息字元計數, 以及紀錄訊息.

log

顯示檔案樹的紀錄訊息.

propget

顯示設定於檔案樹路徑的性質內容.

proplist

顯示設定於檔案樹路徑的性質名稱與內容.

tree

顯示檔案樹列表, 還可選擇顯示關聯到每一個路徑的檔案系統節點修訂版編號.

uuid

顯示檔案樹的唯一使用者代號 (UUID).

youngest

顯示最年輕的修訂版號.

svnadmin

svnadmin 程式是檔案庫管理員最好的朋友. 除了可以建立 Subversion 檔案庫, 這個程式還可以讓你對檔案庫進行幾種維護動作. svnadmin 的語法與 svnlook 很類似:

$ svnadmin help
general usage: svnadmin SUBCOMMAND REPOS_PATH  [ARGS & OPTIONS ...]
Type "svnadmin help <subcommand>" for help on a specific subcommand.

Available subcommands:
   create
   dump
   help (?, h)
…

我們已經提過 svnadmincreate 子命令 (見 the section called “檔案庫的建立與設定”). 在本章中, 我們會仔細講解大多數其它的命令. 現在, 我們先簡單地看看每個可用的子命令提供什麼樣的功能.

create

建立一個新的 Subversion 檔案庫.

deltify

在指定的修訂版範圍中, 對其中變動的路徑作 deltification. 如果沒有指定修訂版的話, 則直接對 HEAD 修訂進行.

dump

以可移植傾印格式, 傾印指定範圍修訂版的檔案庫內容.

list-dblogs

列出關聯至檔案庫的 Berkeley DB 紀錄檔案的路徑. 這個列表包含了所有的紀錄檔 — Subversion 仍在使用的, 以及不再使用的.

hotcopy

產生檔案庫的即時備份. 你可以在任何時候執行這個命令以安全地產生檔案庫的複本, 毋須理會是否有其它的行程也同時在存取檔案庫.

list-unused-dblogs

列出所有關聯到檔案庫的, 但是已經不再使用的 Berkeley DB 紀錄檔. 你可以安全地自檔案庫的目錄結構中移除這些紀錄檔, 也可以將其備份下來, 日後要在遭遇災難事件後, 可於回復檔案庫時使用.

load

從一個使用可移植傾印格式、由 dump 子命令產生的資料串流中, 載入一組修訂版至檔案庫中.

lstxns

列出目前存在於檔案庫中, 尚未送交的 Subversion 異動.

recover

對一個有需要的檔案庫進行回復步驟, 可能因為曾發生重大的錯誤, 讓某個行程無法正常地中止與檔案庫的溝通.

rmtxns

安全地從檔案庫中移除 Subversion 異動 (可以直接使用 lstxns 子命令的輸出).

setlog

將檔案庫中指定修訂版的 svn:log (送交紀錄訊息) 性質的現值, 以指定的新值取代.

verify

驗證檔案庫的內容. 這包括了比較存放於檔案庫裡的版本控制資料的總和檢查碼.

svnshell.py

Subversion 源碼樹還包含了一個類似 shell, 與檔案庫溝通的界面. svnshell.py 這個 Python 命令稿 (位於源碼樹的 tools/examples/ 目錄中), 它使用 Subversion 的語言繫結 (因此你必須正確地編譯並安裝它們, 以便讓這個命令稿能正常執行), 以連接上檔案庫與檔案系統程式庫.

執行之後, 這個程式就像 shell 一樣, 讓你可以瀏灠檔案庫裡的目錄. 一開始, 你 “位於” 檔案庫的 HEAD 修訂版的根目錄, 並給你一個提示符號. 你可以在任何時候使用 help 命令, 它會顯示可用命令的列表, 以及它們功用為何.

$ svnshell.py /path/to/repos
<rev: 2 />$  help
Available commands:
  cat FILE     : dump the contents of FILE
  cd DIR       : change the current working directory to DIR
  exit         : exit the shell
  ls [PATH]    : list the contents of the current directory
  lstxns       : list the transactions available for browsing
  setrev REV   : set the current revision to browse
  settxn TXN   : set the current transaction to browse
  youngest     : list the youngest browsable revision number
<rev: 2 />$

在檔案庫的目錄結構之間移動, 就像在普通的 Unix 或 Windows shell 作法一樣 — 請用 cd 命令. 在任何時候, 命令指示符號都會顯示目前檢視的修訂版 (前置 rev:) 或異動 (前置 txn:), 以及該修訂版或異動的路徑位置. 你可以利用 setrevsettxn, 來變更目前你的修訂版或異動. 就像在 Unix shell, 你可以使用 ls 命令來顯示目前目錄的內容, 也可以使用 cat 命令來顯示檔案的內容.

Example 5.1. 利用 svnshell, 在檔案庫之中巡行

<rev: 2 />$ ls
   REV   AUTHOR  NODE-REV-ID     SIZE         DATE NAME
----------------------------------------------------------------------------
     1    sally <     2.0.1>          Nov 15 11:50 A/
     2    harry <     1.0.2>       56 Nov 19 08:19 iota
<rev: 2 />$ cd A
<rev: 2 /A>$ ls
   REV   AUTHOR  NODE-REV-ID     SIZE         DATE NAME
----------------------------------------------------------------------------
     1    sally <     4.0.1>          Nov 15 11:50 B/
     1    sally <     a.0.1>          Nov 15 11:50 C/
     1    sally <     b.0.1>          Nov 15 11:50 D/
     1    sally <     3.0.1>       23 Nov 15 11:50 mu
<rev: 2 /A>$ cd D/G 
<rev: 2 /A/D/G>$ ls
   REV   AUTHOR  NODE-REV-ID     SIZE         DATE NAME
----------------------------------------------------------------------------
     1    sally <     e.0.1>       23 Nov 15 11:50 pi
     1    sally <     f.0.1>       24 Nov 15 11:50 rho
     1    sally <     g.0.1>       24 Nov 15 11:50 tau
<rev: 2 /A>$ cd ../..
<rev: 2 />$ cat iota
This is the file 'iota'.
Added this text in revision 2.

<rev: 2 />$ setrev 1; cat iota
This is the file 'iota'.

<rev: 1 />$ exit
$

如你在前一個範例中看到的, 在一個命令指示中可以用分號隔開, 指定數個命令. 還有, shell 瞭解絕對路徑與相對路徑的表示法, 它可以正確地處理 "." 與 ".." 的特殊路徑部份.

youngest 命令會顯示最年輕的修訂版號, 它很適合用來決定你可用在 setrev 命令引數的有效修訂版範圍 — 你可以瀏灠包含 0 到最年輕的修訂版內容 (請回想一下, 修訂版號是以整數表示的). 決定可瀏灠的有效異動就不是那麼容易了, 請使用 lstxns 命令來列出你可瀏灠的異動. 可瀏灠的異動列表就跟 svnadmin lstxns 傳回的是一樣的, 它們也可以用在 svnlook--transaction 選項中.

當你完成了 shell 的工作, 你可以使用 exit 命令, 以便正確地結束. 另外, 你也可以輸入檔尾字元 — Control-D (不過某些 Win32 的 Python 發行版本改用 Windows 傳統的 Control-Z.)

Berkeley DB 工具

目前 Subversion 檔案庫只有一種資料庫後端 — Berkeley DB. 所有你的檔案系統結構與資料都存在於檔案庫的 db 子目錄的一組資料表格中. 這個子目錄是一般的 Berkeley DB 環境目錄, 因此可以跟任何 Berkeley 資料庫工具一起使用 (你可以在 SleepyCat 的網站看看這些工具的文件, http://www.sleepycat.com/). 日常使用 Subversion 是不需要這些工具的, 但是他們提供的一些重要功能, 是目前 Subversion 本身所沒有的.

舉個例子, 因為 Subversion 使用 Berkeley DB 的日誌功能, 資料庫本身會先寫下任何它打算進行的更動的描述, 然後再進行實際的更動. 這是為了確定在出錯的情況下, 資料庫系統還是可以回溯到前一個 檢查點 (checkpoint) — 一個已知沒有問題的紀錄檔位置 — 然後再重新進行異動, 直到資料回復到一個可用的狀態. 因為這個功能, 就是為什麼選擇 Berkeley DB 作為 Subversion 一開始的主要資料庫後端的主要原因之一.

這些紀錄檔案會隨著時間累積. 這實際上是資料庫系統的一項特色 — 你應該可以只藉由這些紀錄檔, 就可以重建整個資料庫, 所以這些檔案對於災後的資料庫重建是很重要的. 但是一般來說, 你會想要把 Berkeley DB 不再需要的紀錄檔案給歸檔, 然後把它們從磁碟刪去以節省空間. Berkeley DB 提供了一個 db_archive 工具, 其中之一的功能就是列出關聯到指定資料庫, 但是不再被使用的紀錄檔. 如此, 你就可以知道哪些資料可以歸檔, 然後將其移除. svnadmin 工具程式對這個 Berkeley DB 工具提供了一個方便的包裝:

$ svnadmin list-unused-dblogs /path/to/repos
/path/to/repos/log.0000000031
/path/to/repos/log.0000000032
/path/to/repos/log.0000000033

$ svnadmin list-unused-dblogs /path/to/repos | xargs rm
## 釋放磁碟空間!

Subversion 的檔案庫使用 post-commit 掛勾命令稿, 它在執行檔案庫的 “即時備份” 之後, 就會移除這些多餘的紀錄檔案. 在 Subversion 的源碼樹, tools/backup/hot-backup.py 命令稿示範了一個安全的方法, 如何在 Berkeley DB 資料庫環境仍被存取的情況下, 還能進行備份: 以遞迴方式複製整個檔案庫目錄, 然後重新複製 db_archive -l 列出的檔案.

一般來說, 只有真正的偏執狂才真的需要在每次送交時進行即時備份. 但是我們假設任何一個檔案庫都有某種程度的冗餘機制, 可到某一細微的程度 (例如每一次的送交), 檔案庫管理員仍有可能想要進行資料庫的即時備份, 作為系統層級的每日備份. 就大多數的檔案庫來說, 歸檔的送交電子郵件本身就足以成為回復的來源, 至少可用於最後幾個送交. 但是這是你的資料; 請以你希望的程度來保護它.

Berkeley DB 還包含了兩個用來轉換成 ASCII 文字檔, 以及自它轉換成資料庫的工具. db_dumpdb_load 這兩個程式, 各是用來寫入與讀取一個自訂的檔案格式, 可用來描述 Berkeley DB 資料庫裡的資料鍵與資料值. 由於 Berkeley 資料庫在不同的機器平台並不具移植性, 這個格式在不同的機器之間傳輸資料庫就很有用了, 完全不必煩惱機器架構與作業系統.

檔案庫善後

一般來講, 在你的 Subversion 檔案庫依你需要作好設定之後, 就不需要太多的注意. 但是有的時候還是會需要管理員的介入. svnadmin 工具提供了幾個有用的功能, 可幫助你進行以下的工作:

  • 修改送交紀錄訊息,

  • 移除無效的異動,

  • 修復 “卡住的” 檔案庫, 以及

  • 將檔案庫內容移至不同的檔案庫.

也許 svnadmin 最常使用的子命令是 setlog. 當一個異動送交到檔案庫, 並且提升成為修訂版之後, 關聯到新修訂版的描述紀錄訊息 (由使用者提供) 會被儲存為附加到修訂版的未納入版本控制的性質. 換句話說, 檔案庫只會記得該性質的最新值, 直接捨棄前一個值.

有的時候, 使用者的紀錄訊息會出錯 (也許是拼錯字, 或是寫錯資訊). 如果檔案庫設定為 (使用 pre-revprop-changepost-revprop-change 掛勾; 請參照 the section called “Hook scripts”) 送交之後還可以接受紀錄訊息的更動, 那麼使用者可以利用 svn 程式的 propset 命令 (請參照 Chapter 8, 完整 Subversion 參考手冊), 從遠端 “修正” 紀錄訊息. 但是呢, 因為有永遠失去資訊的可能, Subversion 檔案庫的預設設定是不允許更動未納入版本控制的性質 — 除非由管理員為之.

如果紀錄訊息必須由管理員來更正, 這可由 svnadmin setlog 來達成. 這個命令會修改檔案庫中, 指定修訂版的紀錄訊息 (svn:log 性質), 並且由提供的檔案中讀取新值.

$ echo "Here is the new, correct log message" > newlog.txt
$ svnadmin setlog myrepos newlog.txt -r 388

另一個 svnadmin 常用的用法, 就是查詢檔案庫中未處理的 — 有可能是已經不再使用了的 — Subversion 異動. 在送交注定失敗的時候, 異動一般都會被清除, 也就是說, 異動本身會自檔案庫移除, 任何與該異動有關 (也只有與其有關) 的資料會被移除. 但是有的時候, 發生的錯誤不會清除異動. 發生的原因有幾種: 也許用戶端的動作被使用者無預期中斷, 或是網路在運作之中突然中斷等等. 不管原因為何, 這些未處理的異動只會分散檔案庫, 只會佔用資源而已.

你可以使用 svnadminlstxns 命令, 列出目前未處理異動的名稱.

$ svnadmin lstxns myrepos
19
3a1
a45
$

產生的輸出中, 每一個項目都可以供 svnlook 使用 (以及它的 --transaction 選項), 看看是誰建立這個異動, 何時建立, 異動中有哪種類型的更動 — 換句話說, 就是這些異動是不是可以安全地被移除! 如果這樣的話, 異動的名稱可以傳給 svnadmin rmtxns, 用來清除異動. 事實上, rmtxns 子命令可以直接以 lstxns 的輸出作為輸入.

$ svnadmin rmtxns myrepos `svnadmin lstxns myrepos`
$

如果你像這樣使用這兩個子命令的話, 你應該考慮暫時不讓用戶端存取檔案庫. 如此, 在你開始進行清除之前, 沒有人可以進行有效的異動. 以下是簡單的 shell 命令稿, 可以快速地產生每一個檔案庫裡的未處理異動的資訊:

Example 5.2. txn-info.sh (回報未處理異動)

#!/bin/sh

### 產生 Subversion 檔案庫中, 未處理異動的相關訊息.

SVNADMIN=/usr/local/bin/svnadmin
SVNLOOK=/usr/local/bin/svnlook

REPOS=${1}
if [ x$REPOS = x ] ; then
  echo "usage: $0 REPOS_PATH"
  exit
fi

for TXN in `${SVNADMIN} lstxns ${REPOS}`; do 
  echo "---[ Transaction ${TXN} ]-------------------------------------------"
  ${SVNLOOK} info ${REPOS} --transaction ${TXN}
done

你可以以 /path/to/txn-info.sh /path/to/repos 來使用前面的命令檔. 這個輸出基本上是 svnlook info 輸出區塊合併起來而已 (請參考 the section called “svnlook”), 看起來就像這樣:

$ txn-info.sh myrepos
---[ Transaction 19 ]-------------------------------------------
sally
2001-09-04 11:57:19 -0500 (Tue, 04 Sep 2001)
0
---[ Transaction 3a1 ]-------------------------------------------
harry
2001-09-10 16:50:30 -0500 (Mon, 10 Sep 2001)
39
Trying to commit over a faulty network.
---[ Transaction a45 ]-------------------------------------------
sally
2001-09-12 11:09:28 -0500 (Wed, 12 Sep 2001)
0
$

一般來講, 如果你看到一個沒有關聯紀錄訊息的無效異動, 這是一個失敗的更新 (或類似更動) 作業的結果. 這些動作是偷偷使用 Subversion 異動來模仿工作複本的狀態. 由於它們並不打算送交, Subversion 並不要求這些異動要有紀錄訊息. 有紀錄訊息的異動幾乎可以肯定是某種失敗的送交. 還有, 異動的日期戳記可以提供一些有趣的資訊 — 舉個例子, 一個九個月前開始的作業, 仍為有效的可能性有多大?

簡單地說, 是否要清除異動的決定不可輕率為之. 不同的資料來源 — 包括 Apache 的錯誤與存取紀錄, 成功的 Subversion 送交紀錄, 諸如此類的 — 都可以用來幫助你作決定. 最後, 管理員常常只要簡單地跟疑似異動的擁有者溝通 (像是透過電子郵件), 就可以確定這個異動實際上是處於僵屍狀態.

檔案庫回復

為了要保護檔案庫中的資料, 資料庫後端使用了鎖定的機制. 這個機制保證不會同時有多個資料庫存取者同時修改資料庫的某一部份, 而資料從資料庫讀取出來時, 每一個行程都會看到資料是在正確的狀態. 當一個行程需要修改資料庫裡的東西, 首先會檢查目標資料是否有鎖定. 如果資料沒有被鎖定的話, 行程就會鎖定資料, 產生它想要的更動, 然後解除鎖定. 另一個行程會被迫等待, 直到鎖定解除之後, 它才可以繼續存取該部份的資料庫.

在使用 Subversion 檔案庫的過程中, 嚴重的錯誤 (像是磁碟空間, 或是可用的記憶體耗盡) 或中斷會導致行程沒有機會移除它在資料庫產生的鎖定, 結果就是後端資料庫會 “卡住”. 當這樣的情況發生時, 任何存取檔案庫的嚐試都會無限期停住 (因為每一個新的存取者都在等鎖定解除 — 而這件事不會發生).

如果你的檔案庫發生這樣的情況, 首先請勿慌張. Subversion 的檔案系統充份地利用了資料庫的異動, 檢查點, 以及事先寫入日誌的優點, 以確保只有最嚴重的災難事件 [12] 才會永久地毀壞資料庫環境. 一個夠偏執的檔案庫管理員會作出某種形式的 off-site 檔案庫備份, 但是還別忙著找你的系統管理員將磁帶回存回來.

第二, 使用以下的步驟, 試著 “解開” 卡住的檔案庫:

  1. 確定沒有別的行程在存取 (或是試著去存取) 檔案庫. 對網路檔案庫來說, 這表示也要把 Apache HTTP 伺服器給關掉.

  2. 成為擁有與管理檔案庫的使用者身份.

  3. 執行 svnadmin recover /path/to/repos 命令. 你會看到像這樣的輸出:

    Acquiring exclusive lock on repository db, and running recovery procedures.
    Please stand by...
    Recovery completed.
    The latest repos revision is 19.
    
  4. 重新起動 Subversion 伺服器.

這個程序幾乎能夠解決所有檔案庫鎖死的情況. 請確定你以擁有與管理資料庫的身份來執行這個命令, 而不是只以 root 執行. 修復程序的某些部份會從新建立數個資料庫檔案 (舉例來說, 共用的記憶體部份). 以 root 重建的話, 這些檔案將為 root 所擁有, 這表示就算你重新開放檔案庫供人存取, 一般的使用者也無法存取它.

如果前述的步驟因為某些原因而無法解開你的檔案庫, 你應該作兩件事. 首先, 將已損壞的資料庫移到別的地方, 然後回存最新的備份. 然後, 寄一封電子郵件到 Subversion 發展人員郵件論壇 (在 ), 詳細地描述你的問題. 對 Subversion 的發展人員來說, 資料完整性有著極高的優先權.

匯入檔案庫

Subversion 檔案系統將其資料散佈在數個資料庫表格之中, 通常只有 Subversion 管理員了解 (也只有他們想了解). 但是有的時候, 我們需要將所有的資料, 或是部份的資料, 集合在單一可移植的平面檔. Subversion 提供這樣的機制, 由兩個 svnadmin 子命令實作出來: dump 以及 load.

傾印與載入 Subversion 檔案庫的最常見原因, 就是 Subversion 本身的改變. 隨著 Subversion 日漸成熟, 有時會因後端資料庫 schema 的改變, 導致 Subversion 與前一版的檔案庫不相容. 當你升級遇到這樣的相容性問題時, 我們建議以下列簡單的步驟來進行:

  1. 使用你 現有 版本的 svnadmin, 將檔案庫傾印至傾印檔案.

  2. 升級至新版的 Subversion.

  3. 將舊的檔案庫移開, 在原處以 新版本svnadmin, 建立新的空檔案庫

  4. 再利用 新版本svnadmin, 將你的傾印檔載入至剛剛建立的檔案庫.

  5. 最後, 請記得將原來檔案庫的自訂部份複製到新的去, 包括 DB_CONFIG 與掛勾命令稿. 你應該要檢查一下新版本 Subversion 的發行備註, 看看從你上次更新後, 有沒有影響這些掛勾或設定選項的更動.

svnadmin dump 會以 Subversion 自訂的檔案系統傾印檔格式, 輸出一個範圍的檔案庫修訂版. 傾印檔格式會輸出到標準輸出串流, 而資訊訊息會送至標準錯誤串流. 這讓你可以將輸出串流轉向到一個檔案, 但是還能在終端機看到狀況輸出. 舉個例子:

$ svnlook youngest myrepos
26
$ svnadmin dump myrepos > dumpfile
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
…
* Dumped revision 25.
* Dumped revision 26.

作業結束之後, 你會有一個檔案 (在前述的例子中, 就是 dumpfile), 其中包含所有儲存於要求的檔案庫修訂版範圍的資料.

相對的另一個子命令是 svnadmin load, 它會將標準輸入串流以 Subversion 檔案庫傾印檔案進行剖析, 可以有效地將這些傾印的修訂版重播到目標的檔案庫. 它也會提供資訊訊息, 但是這次是送到標準輸出串流:

$ svnadmin load newrepos < dumpfile
<<< Started new txn, based on original revision 1
     * adding path : A ... done.
     * adding path : A/B ... done.
     …
------- Committed new rev 1 (loaded from original rev 1) >>>

<<< Started new txn, based on original revision 2
     * editing path : A/mu ... done.
     * editing path : A/D/G/rho ... done.

------- Committed new rev 2 (loaded from original rev 2) >>>

…

<<< Started new txn, based on original revision 25
     * editing path : A/D/gamma ... done.

------- Committed new rev 25 (loaded from original rev 25) >>>

<<< Started new txn, based on original revision 26
     * adding path : A/Z/zeta ... done.
     * editing path : A/mu ... done.

------- Committed new rev 26 (loaded from original rev 26) >>>

請注意, 因為 svnadmin 使用標準輸入與輸出串流, 作為傾印與載入作業, 膽子很大的人可以這樣試試 (可能還在管道兩旁使用不同版本的 svnadmin):

$ svnadmin create newrepos
$ svnadmin dump myrepos | svnadmin load newrepos

我們在前面說過, svnadmin dump 會輸出一個範圍的修訂版. 透過 --revision 選項, 可以指定傾印單一修訂版, 或是一個範圍的修訂版. 如果你省略這個選項, 所有檔案庫裡的現有修訂版都會被傾印出來.

$ svnadmin dump myrepos --revision 23 > rev-23.dumpfile
$ svnadmin dump myrepos --revision 100:200 > revs-100-200.dumpfile

雖然 Subversion 會傾印所有的修訂版, 它只會輸出夠用的資訊, 讓後來的載入程式可以依此來進行重建. 換句說, 對任何一個傾印檔裡的修訂版來說, 只有在該修訂版被更動的項目會出現在傾印檔中. 本規則的唯一例外, 就是目前 svnadmin dump 命令所輸出的第一個修訂版.

Subversion 預設並不會將第一個傾印的修訂版, 以可用於一個修訂版的差異輸出. 第一, 在傾印檔中並沒有前一個修訂版! 第二個, Subversion 無法得知後來載入傾印檔時 (如果真的有的話), 載入它的檔案庫的狀態為何. 要確定每一次 svnadmin dump 都能自己自給自足的話, 預設第一個傾印出來的修訂版會完整地表示出該檔案庫修訂版的目錄, 檔案, 以及性質.

但是, 你可以變更這樣的預設行為. 如果在傾印檔案庫時, 加上了 --incremental 選項, svnadmin 會將檔案庫的第一個傾印修訂版, 與檔案庫中前一個修訂版作比較, 就像其它被傾印的修訂版的處理方法一樣. 第一個修訂版的輸出, 就像傾印檔裡其它的修訂版輸出一樣 — 只會顯示該修訂版的更動部份而已. 這樣的好處, 是你可以建立幾個可連續載入的小傾印檔, 而不是一個很大的, 像這樣:

$ svnadmin dump myrepos --revision 0:1000 > dumpfile1
$ svnadmin dump myrepos --revision 1001:2000 --incremental > dumpfile2
$ svnadmin dump myrepos --revision 2001:3000 --incremental > dumpfile3

這些傾印檔可以透過下列一連串的命令, 載入到新的檔案庫中:

$ svnadmin load newrepos < dumpfile1
$ svnadmin load newrepos < dumpfile2
$ svnadmin load newrepos < dumpfile3

另一個可以透過 --incremental 選項的技巧, 就是你可以把新範圍的傾印修訂版附加至現有的傾印檔. 舉個例子, 你可能有一個 post-commit 掛勾, 它就會附加觸動它的修訂版的傾印內容. 或是你可以每晚執行像下面的命令稿, 將檔案庫中, 自上一次執行後新增的修訂版附加至傾印檔.

Example 5.3. 使用漸進式檔案庫傾印

#!/usr/bin/perl -w

use strict;

my $repos_path  = '/path/to/repos';
my $dumpfile    = '/usr/backup/svn-dumpfile';
my $last_dumped = '/var/log/svn-last-dumped';
 
# Figure out the starting revision. Use 0 if we cannot read the
# last-dumped file, else use the revision in that file incremented
# by 1.
my $new_start = 0;
if (open LASTDUMPED, $last_dumped)
  {
    my $line = <LASTDUMPED>;
    if (defined $line and $line =~ /^(\d+)/)
      {
        $new_start = $1 + 1;
      }
    close LASTDUMPED;
  }

# Query the youngest revision in the repos.
my $youngest = `svnlook youngest $repos_path`;
defined $youngest && $youngest =~ /^\d+$/
  or die "$0: 'svnlook youngest $repos_path' cannot get youngest revision.\n";
chomp $youngest;

# Do the backup.
system("svnadmin dump $repos_path --revision $new_start:$youngest --incremental >> $dumpfile.tmp") == 0
  or die "$0: svnadmin dump to '$dumpfile.tmp' failed.\n";

# Store a new last-dumped revision.
open LASTDUMPED, "> $last_dumped.tmp"
  or die "$0: cannot open '$last_dumped.tmp' for writing: $!\n";
print LASTDUMPED "$youngest\n";
close LASTDUMPED
  or die "$0: error in closing '$last_dumped.tmp' for writing: $!\n";

# Rename to final locations.
rename("$dumpfile.tmp", $dumpfile)
  or die "$0: cannot rename '$dumpfile.tmp' to '$dumpfile': $!\n";
rename("$last_dumped.tmp", $last_dumped)
  or die "$0: cannot rename '$last_dumped.tmp' to '$last_dumped': $!\n";

# All done!

以這樣子使用的話, svnadmindumpload 命令就成了相當有用的工具, 可進行檔案庫更動的備份, 以避免系統當機, 或是其它災難事件所帶來的損害.

最後, 另一個 Subversion 檔案庫傾印檔案格式的可能應用, 就是從不同的儲存機制, 或是不同的版本控制系統進行轉換. 因為傾印檔案格式絕大部份都是人類可懂的, [13]

檔案庫備份

從現代電腦誕生以來, 不過技術有多進步, 有件事情還是很不幸地一直不變 — 有的時候, 事情會有非常非常嚴重的問題. 電力中斷, 網路斷線, 爛掉的記憶體, 墜毀的硬碟, 無論管理員是如何努力作好他們的工作, 還是有可能會遇到這些暗黑命運的魔爪. 所以我們遇到了最重要的課題 — 如何備份你的檔案庫的資料.

基本上, Subversion 檔案庫管理員可以使用兩種備份 — 漸進式與完整式. 我們在本章稍早的章節曾討論過, 如果使用 svnadmin dump --incremental 來進行漸進式備份 (請參見 the section called “匯入檔案庫”). 基本上, 它的概念就是只備份自上次製作備份後, 一定時間內的檔案庫更動.

就跟字面上的意思一樣, 檔案庫的完整備份是整個檔案庫目錄 (這包括了 Berkeley 資料庫環境) 的複製. 現在, 除非你暫時關閉所有其它對檔案庫的存取, 不然直接進行遞迴式的目錄複製的話, 會有得到無效備份的風險存在, 這是因為別人仍有可能正在寫入資料庫.

很幸運地, Sleepcat 的 Berkeley DB 文件載明了以怎麼樣的步驟複製資料庫檔案, 就可以保證取得有效的備份複本. 更好的是你不必親自實作, 因為 Subversion 發展團隊已經作好了. 在 Subversion 源碼的 tools/backup/ 目錄中, 可以找到 hot-backup.py 命令稿. 只要指定一個檔案庫的路徑與備份的位置, hot-backup.py 就會進行必要的步驟, 進行檔案庫的即時備份 — 完全不需要中止所有的公共存取 — 然後會清除檔案庫中無用的 Berkeley 記錄檔.

就算你已經有了漸近式備份, 你還是會想要定期執行這個程式. 舉例來說, 你會想把 hot-backup.py 加進定時執行的程式裡 (像是 Unix 系統的 crond). 或者你偏好可微調的備份方案的話, 可以讓你的 post-commit 掛勾命令稿呼叫 hot-backup.py (請參見 the section called “Hook scripts”), 這樣在每次有新的修訂版出現時, 就會建立檔案庫的新備份. 只要將以下這一行加進你的檔案庫目錄的 hooks/post-commit 命令稿即可:

(cd /path/to/hook/scripts; ./hot-backup.py ${REPOS} /path/to/backups &)

產生出來的備份是完全可用的 Subversion 檔案庫, 在事情無法收拾時, 可以立即用來作為檔案庫的替代品.

這兩種備份方式都有其長處. 目前最簡單的是完整備份, 它一定能夠產生正確無誤的檔案庫複製. 這表示不管線上檔案庫發生多糟糕的事, 只要一個簡單的遞迴目錄複製指令, 就能從備份回復回來. 不過很不幸地, 如果你維護多個檔案庫的備份, 這些完整備份會佔用跟你線上檔案庫一樣多的磁碟空間.

在 Subversion 前後版本的資料庫綱要變動的時候, 使用檔案庫傾印格式的漸進式備份在手邊是很方便的. 由於將檔案庫升級到新版的資料庫綱要需要進行完整的檔案庫傾印與載入, 如果已經有一半的步驟 (傾印的部份) 已經完成的話, 事情就方便多了. 不過很不幸地, 漸進式備份的建立 — 以及回復 — 要花較長的時間, 因為實際上每一個送交都是重播至傾印檔或是檔案庫之中.

不管是哪一種備份方式, 檔案庫管理員必須知道未納入版本控制的性質更動如何影響它們的備份. 由於這些更動不會產生新的修訂版, 它們不會觸發 post-commit 掛勾, 甚至也不會觸發 pre-revprop-change 與 post-revprop-change 掛勾. [14] 由於你能夠不依時間順序更動修訂版性質 — 你可以在任何時間修改任何修訂版的性質 — 最新幾個修訂版的漸進式備份也許不會知道原先備份的性質更動.

通常最好的修訂版備份是包含多種方式的. 你可以妥善運用完整備份與漸進備分的組合, 再加上電子郵件的送交檔案. 舉個例子, Subversion 的發展人員在每一次新的修訂版建立後, 就會備份 Subversion 的源碼, 並且保留所有的送交與性質的電子郵件通知. 你的方案可能很類似, 但是應該配合你的需要, 在便利與偏執之間取得平衡. 雖然這些都無法從暗黑命運的魔拳 [15] 中解救你的硬體, 它應該能在她肆機而動的時候解救你.

網路檔案庫

一個 Subversion 檔案庫可能同時被其所在機器上的多個用戶端同時存取, 不過常見的 Subversion 組態, 牽涉到一個可被其它辦公室用戶端存取的伺服器 — 當然也有可能是被全世界存取.

本節描述如何讓你的 Subversion 檔案庫開放給遠端用戶使用. 我們會涵蓋所有可用的伺服器機制, 討論每一個的設定與用法. 讀完本章後, 你應該能夠描述哪一種網路設定適合你的需求, 並且瞭解如何在你的電腦上啟用這樣的設定.

httpd, Apache HTTP 伺服器

Subversion 的主要網路伺服器為 Apache HTTP 伺服器 (httpd), 可以 WebDAV/deltaV 通訊協定溝通. 這個通訊協定 (它是 HTTP 1.1 的擴充; 請參照 http://www.webdav.org/) 採用廣為使用的 HTTP 通訊協定, 這是全球資訊網的核心, 再加上寫入 — 更具體地說, 有版本控制的寫入 — 的能力. 其結果就是一個標準的, 強固的系統, 包裝成一個 Apache 2.0 軟體的一部份, 常用作業系統與協力廠商都支援, 又不需要網路管理員開啟另一個自訂的連接埠. [16]

以下的討論中, 提到許多 Apache 設定的指令. 雖然有些範例中提供了指令的用法, 但是完整的描述並不在本書的範圍中. Apache 團隊維護一份很不錯的文件, 可在他們的網站 http://httpd.apache.org 取得. 例如, 一般性的設定指令位於 http://httpd.apache.org/docs-2.0/mod/directives.html.

另外, 在你修改 Apache 的設定時, 很有可能會在過程中引入其它的錯誤. 就算你不熟悉 Apache 的紀錄子系統, 你也應該要知道有它的存在. 在你的 httpd.conf 檔中, 有著指定 Apache 產生的存取紀錄檔與錯誤紀錄檔應放置的位置 (各為 CustomLogErrorLog 指令). Subversion 的 mod_dav_svn 亦使用 Apache 的錯誤紀錄界面. 你都可以瀏灠這些檔案的內容, 它們可能會顯示出其它方法無法發生的錯誤來源.

你需要什麼, 才能設定基於 HTTP 的檔案庫存取

要讓你的檔案庫透過 HTTP 供他人存取, 基本上你需要四個元件, 可從兩個套件中取得. 你需要 Apache 的 httpd 2.0, 它也包含了 mod_dav 的 DAV 模組. Subversion, 以及包含的 mod_dav_svn 檔案系統供應模組. 當你有了這些元件後, 將檔案庫放到網路上的步驟就只是簡單的:

  • 讓 httpd 2.0 跑起來, 並與 mod_dav 模組一併執行,

  • 將 mod_dav_svn 安插模組安裝至 mod_dav, 它會透過 Subversion 程式庫來存取檔案庫, 以及,

  • 設定你的 httpd.conf 檔案, 匯出 (讓他人可存取) 檔案庫.

要完成前兩項, 你可以自源碼編譯 httpd 與 Subversion, 或是在系統上安裝事先編譯好的二進位套件. 欲得知如何將 Subversion 編譯成與 Apache HTTPD 伺服器一同使用, 以及如何編譯與設定 Apache 以達成這樣效果的最新資訊, 請看看 Subversion 源碼樹最上層的 INSTALL 檔案.

基本 Apache 設定

當你把所有需要的元件都安裝到系統上之後, 剩下的只就是透過 httpd.conf 檔案設定 Apache. 請使用 LoadModule 指令, 讓 Apache 載入 mod_dav_svn 模組, 這個指令必須出現在其它的 Subversion 相關指令之前. 如果你的 Apache 是以預設的目錄配置安裝的, 你的 mod_dav_svn 模組應該會安裝在 Apache 安裝位置 (通常是 /usr/local/apache2) 的 modules 子目錄內. LoadModule 指令的語法很簡單, 就是將一個具名模組對映到共用程式庫在磁碟上的位置:

LoadModule dav_svn_module     modules/mod_dav_svn.so
        

請注意, 如果 mod_dav 是編譯成共用 object (而不是直接以靜態連結到 httpd 可執行檔), 你也需要對它使用一個類似的 LoadModule 敘述.

稍後於設定檔中, 你需要告訴 Apache, Subversion 檔案庫 (或多個檔案庫) 的位置. Location 指令有類似 XML 的表示法, 以一個起始標籤開始, 以完結標籤結束, 中間包含了其它不同的指令. Location 指令的目的, 是告訴 Apache 在處理指向指定 URL 或其子項目之一的要求時, 對它進行特別的處理. 在 Subversion 的情況中, 你要讓 Apache 把指向具版本控制資源的要求, 直接交給 DAV 去處理. 你可以利用以下的 httpd.conf 語法, 指示 Apache 將所有對路徑部份 (就是 URL 中, 在伺服器名稱與可能出現的連接埠之後的部份) 以 /repos/ 開始的 URL 的處理, 通通由 DAV 提供者來處理, 其檔案庫位於 /absolute/path/to/repository:

<Location /repos>
  DAV svn
  SVNPath /absolute/path/to/repository
</Location>

如果你計劃支援多個 Subversion 檔案庫, 而它們都有著共同的本地磁碟路徑, 你可以使用另一種指令 SVNParentPath, 指示它們共同的父路徑. 舉個例子, 如果你知道你會在路徑 /usr/local/svn 之下建立多個 Subversion 檔案庫, 並以類似 http://my.server.com/svn/repos1,http://my.server.com/svn/repos2 等等的 URL 供人存取, 你可以使用下列例子中的 httpd.conf 設定語法:

<Location /svn>
  DAV svn
  SVNParentPath /usr/local/svn
</Location>

使用前述的語法, Apache 會將所有路徑以 /svn/ 開始的 URL 都交給 Subversion DAV 供應模組處理, 它會假設任何以 SVNParentPath 指令指定的目錄都是 Subversion 檔案庫. 不像 SVNPath, 這個相當便利的語法可以讓你在建立新的檔案庫時, 仍舊不必重跑 Apache.

權限, 認證, 以及授權

在這個階段, 你應該要強烈地考慮權限的問題. 如果你已經讓 Apache 以普通的網頁伺服器執行了一陣子, 你應該已經有一堆的內容 — 網頁, 命令稿, 諸如此類的. 這些項目都已經以一組權限執行, 讓他們能夠與 Apache 一起使用, 更適切地講, 讓 Apache 與這些檔案一起工作. 當 Apache 以 Subversion 伺服器運作時, 它也需要正確的權限, 以便能夠讀取與寫入你的 Subversion 檔案庫.

你需要決定一套權限系統設定, 能夠滿足 Subversion 的要求, 又不會弄亂先前安裝的網頁或命令稿. 這表示你要變更 Subversion 檔案庫的權限, 以符合 Apache 其它一起協同工作的部份, 或是利用 httpd.confUserGroup 指令, 讓 Apache 以擁有 Subversion 檔案庫的使用者與群組的身份來執行. 設定權限並沒有一個絕對正確的方法, 而且每一個管理員有自已行事的標準. 你只要注意, 要讓 Subversion 檔案庫與 Apache 一起使用時, 權限會是最常發生的問題.

既然我們在談論權限的問題, 我們也應該談談 Apache 提供的認證與授權的機制可以如何運用. 除非你對這些有某種系統層面的設定, 基本上, 你透過 Location 開放的 Subversion 檔案庫是所有人都可以存取的. 換句話說,

  • 任何人都可以用它們的 Subversion 用戶端, 取出檔案庫 URL (或其任意的子目錄) 的工作複本.

  • 任何人只要將他們的瀏覽器指向檔案庫 URL, 就可以互動式地瀏覽檔案庫最新的修訂版, 以及,

  • 任何人可以送交至檔案庫.

如果你要限制整個檔案庫的讀取或寫入存取, 你可以使用 Apache 內建的存取控制功能. 在這些功能中, 最簡單的是基本設證機制, 它只會使用使用者名稱與密碼, 用以確認使用者是他所聲稱的身份. Apache 提供了 htpasswd 工具程式, 來管理接受的使用者名稱與密碼, 也就是你想要授與存取 Subversion 檔案庫權限的使用者. 讓我們授與 Sally 與 Harry 送交存取的權限. 首先, 我們必須把它們加入到密碼檔案.

$ ### 第一次: 以 -c 建立檔案
$ htpasswd -c /etc/svn-auth-file harry
New password: ***** 
Re-type new password: *****
Adding password for user harry
$ htpasswd /etc/svn-auth-file sally
New password: *******
Re-type new password: *******
Adding password for user sally
$
        

接著, 你需要在 httpd.confLocation 區塊中新增幾個指令, 告訴 Apache 如何處理你的新密碼檔. AuthType 指令指定應使用何種認證系統. 在目前的狀況中, 我們想要指定 Basic 認證系統. AuthName 是一個任意的名稱, 讓你用來指定認證領域 (authentication domain). 大多數的瀏覽器在向使用者詢問使用者代號與密碼時, 會將這個名稱顯示在彈出的對話框中. 最後, 使用 AuthUserFile 指令, 指定你以 htpasswd 產生的密碼檔.

在新增這三個指令後, 你的 <Location> 區塊看起來應該像這樣:

<Location /svn>
  DAV svn
  SVNParentPath /usr/local/svn
  AuthType Basic
  AuthName "Subversion repository"
  AuthUserFile /path/to/users/file
</Location>

現在如果你重新啟動 Apache, 任何需要認證的 Subversion 動作都會從 Subversion 用戶端取得使用者代號與密碼, 這可能是使用先前置於快取的值, 或是向使用者詢問. 剩下要作的, 就是告訴 Apache 哪些動作需要這樣的認證.

你可以藉由將 Require valid-user 指令加進 <Location> 區塊, 對所有存取檔案庫的動作進行限制. 以先前的例子而言, 這表示只有聲稱他們自己是 harrysally, 並且能夠提供這些代號的正確密碼的用戶端, 才可以對 Subversion 檔案庫作任何事.

有的時候, 你不需要這麼地嚴格. 舉例來說, Subversion 源碼所在的 http://svn.collab.net/repos/svn 檔案庫就允許世界上的任何人對它進行唯讀的存取 (像是取得工作複本, 或是透過瀏覽器來瀏覽檔案庫), 但是所有寫入的動作僅限由認證用戶為之. 要達到這樣的選擇性限制, 你可以使用 LimitLimitExcept 設定指令. 就像 Location 指令, 這些區塊都有起始與完結標籤, 而且你應該把它們放置在你的 <Location> 區塊.

LimitLimitExcept 出現的參數, 是該區塊中受到影響的 HTTP 要求類別. 舉個例子, 如果除了目前支援的唯讀動作, 其它的存取通通不允許, 你可以使用 LimitExcept 指令, 並且使用 GET, PROPFIND, OPTIONS, 以及 REPORT 的要求類別參數. 然後前述的 Require valid-user 指令就應該擺在 <LimitExcept> 區塊中, 而不只是 <Location> 中而已.

<Location /svn>
  DAV svn
  SVNParentPath /usr/local/svn
  AuthType Basic
  AuthName "Subversion repository"
  AuthUserFile /path/to/users/file
  <LimitExcept GET PROPFIND OPTIONS REPORT>
    Require valid-user
  </LimitExcept>
</Location>

這只是幾個簡單的例子而已. 欲得知更詳細的 Apache 存取控制的資訊, 請參考 http://httpd.apache.org/docs-2.0/misc/tutorials.html 的 Apache 的導覽文件中的 安全 一節.

伺服器名稱與 COPY 要求

Subversion 使用 COPY 要求類別, 進行伺服器端的目錄與檔案的複製. 由於 Apache 模組要進行的完整性檢查, 複製來源必須與目標處於同一台機器上. 要達到這個要求, 你需要告訴 mod_dav 用以作為伺服器主機的名稱. 一般來說, 你可以在 httpd.conf 中使用 ServerName 指令來達到這樣的目的.

ServerName svn.red-bean.com
        

如果你透過 NameVirtualHost 使用 Apache 的虛擬主機功能, 那麼你必須使用 ServerAlias 指令來指定伺服器的其它名稱. 當然囉, 請參考 Apache 的文件, 以瞭解詳細的細節.

瀏覽檔案庫的 HEAD 修訂版

對你的 Subversion 檔案庫進行 Apache/WebDAV 設定, 最有用的功效之一, 就是可以馬上使用普通的瀏覽器來存取最年輕修訂版、納入版本控制的目錄與檔案. 由於 Subversion 使用 URL 來指定受版本控制的資源, 這些用來存取基於 HTTP 的檔案庫存取的 URL, 可直接輸入到網頁瀏覽器之內. 你的瀏覽器會為這個 URL 送出一個 GET 要求, 依該 URL 所代表的是受版本控制的目錄還是檔案, mod_dav_svn 會以目錄列表或檔案內容進行回應.

由於 URL 中沒有包含你想知道的資源的版本資訊, mod_dav_svn 都會以最年輕的版本回應. 這個功能有個很棒的副作用, 就是你可以將 Subversion 的 URL 當作文件參照, 丟給同事使用, 然後這些 URL 永遠都指向該文件的最新版本. 當然囉, 你還可以將這些 URL 當作其它網頁的超連結之用.

你通常會找到指向受版本控制檔案的 URL 的其它用法 — 畢竟, 通常那也是人家有興趣的內容所在. 但是你可能偶而也看過 Subversion 的目錄列表, 很快就會發現產生出來的 HTML 功能很陽春, 一點也不漂亮 (甚至一點也不有趣). 如果想要自訂目錄列表, Subversion 提供了 XML 索引功能. 在 httpd.conf 中的檔案庫的 Location 區塊, 一個 SVNIndexXSLT 指令就會告訴 mod_dav_svn, 在顯示目錄列表時, 產生 XML 的輸出, 並且使用你所指定的 XSLT 式樣表:

<Location /svn>
  DAV svn
  SVNParentPath /usr/local/svn
  SVNIndexXSLT "/svnindex.xsl"
  …
</Location>

透過 SVNIndexXSLT 指令與一個有創意的 XSLT 式樣表, 你可以讓你的目錄列表符合網站其它部份的色彩運用與影像. 或者你喜歡的話, 也可以使用 Subversion 源碼中, 置於 tools/xslt/ 目錄裡的式樣表範例. 請記得 SVNIndexXSLT 指令所提供的路徑, 實際上是 URL 路徑 — 瀏覽器必須要能夠讀取你的式樣表, 才能使用它們!

雜項的 Apache 功能

作為一個強固的網頁伺服器, Apache 本身已有的幾項功能, 也可以用在提供 Subversion 的功能性與安全性. Subversion 透過 Neon 與 Apache 進行溝通, 這是一個通用型的 HTTP/WebDAV 程式庫, 支援了幾個像是 SSL (secure socket layer) 與 Deflate 壓縮 (和 gzipPKZIP 用來將檔案 “縮小 (shrink)” 成較小的區塊的演算法相同) 的機制. 你要作的, 就只是把你想要的功能與 Subversion 和 Apache 編譯在一起, 然後正確地設定程式, 讓它們使用這些功能.

這表示啟用 SSL 的 Subversion 用戶端可以存取開啟 SSL 的 Apache 伺服器, 透過加密的通訊協定進行所有的溝通, 僅僅需要把 http://https:// 的 URL 取代即可. 有些需要把它們的檔案庫對防火牆外開放的公司, 必須要考慮到惡意第三方可能會 “竊聽” 網路封包. SSL 讓這樣不受歡迎的行為不容易造成敏感資料外洩. Apache 可以設定成只讓開啟 SSL 的 Subversion 用戶端與檔案庫進行溝通.

Deflate 壓縮會在用戶端與伺服器加諸小小的負擔, 對網路傳遞進行壓縮與解壓縮, 以將實際的資料傳輸量降至最低. 在網路頻寬相當不足的情況下, 這樣的壓縮功能能夠大幅地加速伺服器與用戶端之間的傳輸速度. 在很極端的情況下, 像這樣被減少的網路傳輸, 就可以決定動作會逾時, 還是成功地完成.

有些比較不引人注意, 但是同樣有用的, 是 Apache 與 Subversion 之間的功能, 像是指定自訂連接埠 (而不使用預設 HTTP 的 80 連接埠), 可使用虛擬網域名稱對 Subversion 檔案庫進行存取的能力, 或是透過代理伺服器存取檔案庫的功能. 這些都被 Neon 支援, 所以 Subversion 不費吹灰之力就得到這些功能.

svnserve, 自訂的 Subversion 伺服器

除了 Apache 之外, Subversion 還提供另一個單獨的伺服器程式, 也就是 svnserve. 這個程式要比 Apache 更輕便, 而且更容易設定. 它會與 Subversion 用戶端透過 TCP/IP 連線以自訂的通訊協定溝通.

svnserve 有兩種基本的使用方法:

未授權 (匿名) 存取

在這個情況下, 一個 svnserve 的背景監控行程會在伺服器上執行, 聆聽外部的連線. svn 用戶端會使用自訂的 svn:// URL schema 連線. 用戶端連線會無條件地接受, 而檔案庫會以未授權的使用者名稱進行存取. 大多數的情況下, 管理員會將這個背景監控程式設定為允取唯讀動作.

授權 (SSH) 存取

在這個情況下, svn 用戶端使用的是自訂的 svn+ssh:// URL schema; 這會啟動一個本地端的 Secure Shell (SSH) 行程, 它會連線至伺服器, 然後進行授權. 使用者必須在伺服器有系統帳號, 才能這樣作. 在授權完成後, SSH 行程會在伺服器上執行一個暫時的私有 svnserve 行程, 以授權使用者的身份執行. 伺服器與用戶端會透過加密的 ssh 通道進行溝通.

請注意, 這些 svnserve 的使用方法並不互相衝突; 你可以很簡單地同時在你的伺服器上, 使用這兩種技巧.

設定匿名 TCP/IP 存取

svnserve 不帶引數執行時, 它會將資料寫至標準輸出, 並自標準輸入讀取資料, 試著與一個 svn 用戶端商議出一個進程:

$ svnserve
( success ( 1 1 ( ANONYMOUS ) ( ) ) ) 

這對任何人都沒有立即的效果; 因為 svnserve 的運作如此, 它才能被 inetd 背景監控程式執行. 但是要將 svnserve 以背景監控程式執行的話, 方法還有很多種.

有一個方法就是對伺服器主機的 inetd 背景監控程式登記一個 svn 服務. 那麼當一個用戶端試著要連接到連接埠 3690 時, [17] inetd 就會起動一個 “只用一次” 的 svnserve 行程來處理該用戶的要求.

當你以這種方式設定的話, 請記住你不該以 root 使用者 (或其它有無限權限的使用者) 的身份來執行 svnserve 行程. 依你匯出檔案庫的所有權與權限的不同, 不同的 — 也許是自訂的 — 的使用者可能更合適. 舉個例子, 你可能會想要建立一個名為 svn 的新使用者, 給這個使用者可使用 Subversion 檔案庫的專有權限, 然後設定你的 svnserve 行程以該使用者的身份執行.

當然囉, 第一個方法只適用於 inetd (或類似 inetd 的) 背景監控程式的機器. 通常這只限於 Unix 平台上的. 另一個方法是將 svnserve 以單一個背景監控程式執行. 以 -d 選項執行的話, svnserve 會馬上自目前的 shell 行程分離, 以背景行程一直地執行下去, 當然還是在連接埠 3690 上等待新進的連線.

$ svnserve -d
$ # svnserve 仍在執行, 但是使用者已經回到提示字元

當一個用戶端透過網路與 svnserve 行程 (以背景監控程式執行, 或是 “只用一次” 的處理行程) 連線時, 完全沒有認證發生. 伺服器行程會以它執行的使用者身份來處理檔案庫, 如果用戶端進行送交的話, 新的修訂版完全不會設定 svn:author 性質.

只要 svnserve 伺服器開始執行, 它會讓整個檔案庫都開放給網路使用. 換句話說, 如果一個用戶端試著要對在 example.com 上執行的 svnserve 行程取出 svn://example.com/usr/local/repos/project, 它會試著去找位於絕對路徑 /usr/local/repos/project 上的檔案庫. 如果你想增加安全性, 你應該以 -r 選項執行 svnserve, 它會限制只開於該路徑下的檔案庫:

$ svnserve -d -r /usr/local
…

透過 -r 選項, 可以有效地變更程式認為是遠端檔案系統空間的根目錄. 用戶端就可以把相同的路徑拿掉, 就會得到比較短的 (也比較不明顯的) URL:

$ svn checkout svn://example.com/repos/project
…

要取消檔案庫的寫入存取, 請以 -R 選項起動 svnserve. 這樣就只允許讀取檔案庫裡的資料.

設定使用 SSH 存取

通常來說, 我們都想知道 (在幾近無限的使用者中) 哪一個使用者該為哪些更動負責, 但是限制哪些使用者有檔案庫的寫入能力要來得更重要. [18] 要達到這兩個目的, 使用 libsvn_ra_svn 的用戶端可以透過 SSH 通道來&#