當前,大多數開發中的開源項目以及大量的商業項目都使用 Subversion 來管理源碼。作為最流行的開源版本控制系統,Subversion 已經存在了接近十年的時間。它在許多方面與 CVS 十分類似,后者是前者出現之前代碼控制世界的霸主。
Git 最為重要的特性之一是名為 git svn 的 Subversion 雙向橋接工具。該工具把 Git 變成了 Subversion 服務的客戶端,從而讓你在本地享受到 Git 所有的功能,而后直接向 Subversion 服務器推送內容,仿佛在本地使用了 Subversion 客戶端。也就是說,在其他人忍受古董的同時,你可以在本地享受分支合并,使暫存區域,衍合以及 單項挑揀等等。這是個讓 Git 偷偷潛入合作開發環境的好東西,在幫助你的開發同伴們提高效率的同時,它還能幫你勸說團隊讓整個項目框架轉向對 Git 的支持。這個 Subversion 之橋是通向分布式版本控制系統(DVCS, Distributed VCS )世界的神奇隧道。
## git svn
Git 中所有 Subversion 橋接命令的基礎是 git svn 。所有的命令都從它開始。相關的命令數目不少,你將通過幾個簡單的工作流程了解到其中常見的一些。
值得警戒的是,在使用 git svn 的時候,你實際是在與 Subversion 交互,Git 比它要高級復雜的多。盡管可以在本地隨意的進行分支和合并,最好還是通過衍合保持線性的提交歷史,盡量避免類似與遠程 Git 倉庫動態交互這樣的操作。
避免修改歷史再重新推送的做法,也不要同時推送到并行的 Git 倉庫來試圖與其他 Git 用戶合作。Subersion 只能保存單一的線性提交歷史,一不小心就會被搞糊涂。合作團隊中同時有人用 SVN 和 Git,一定要確保所有人都使用 SVN 服務來協作——這會讓生活輕松很多。
### 初始設定
為了展示功能,先要一個具有寫權限的 SVN 倉庫。如果想嘗試這個范例,你必須復制一份其中的測試倉庫。比較簡單的做法是使用一個名為 svnsync 的工具。較新的 Subversion 版本中都帶有該工具,它將數據編碼為用于網絡傳輸的格式。
要嘗試本例,先在本地新建一個 Subversion 倉庫:
~~~
$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn
~~~
然后,允許所有用戶修改 `revprop ——` 簡單的做法是添加一個總是以 0 作為返回值的` pre-revprop-change` 腳本:
~~~
$ cat /tmp/test-svn/hooks/pre-revprop-change
!/bin/sh
exit 0;
$ chmod +x /tmp/test-svn/hooks/pre-revprop-change
~~~
現在可以調用 svnsync init 加目標倉庫,再加源倉庫的格式來把該項目同步到本地了:
`$ svnsync init file:///tmp/test-svn http://progit-example.googlecode.com/svn/`
這將建立進行同步所需的屬性。可以通過運行以下命令來克隆代碼:
~~~
$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Committed revision 2.
Copied properties for revision 2.
Committed revision 3.
...
~~~
別看這個操作只花掉幾分鐘,要是你想把源倉庫復制到另一個遠程倉庫,而不是本地倉庫,那將花掉接近一個小時,盡管項目中只有不到 100 次的提交。 Subversion 每次只復制一次修改,把它推送到另一個倉庫里,然后周而復始——驚人的低效,但是我們別無選擇。
### 入門
有了可以寫入的 `Subversion `倉庫以后,就可以嘗試一下典型的工作流程了。我們從` git svn clone `命令開始,它會把整個 Subversion 倉庫導入到一個本地的 Git 倉庫中。提醒一下,這里導入的是一個貨真價實的 Subversion 倉庫,所以應該把下面的 `file:///tmp/test-svn` 換成你所用的 `Subversion` 倉庫的 URL:
~~~
$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags
Initialized empty Git repository in /Users/schacon/projects/testsvnsync/svn/.git/
r1 = b4e387bc68740b5af56c2a5faf4003ae42bd135c (trunk)
A m4/acx_pthread.m4
A m4/stl_hash.m4
...
r75 = d1957f3b307922124eec6314e15bcda59e3d9610 (trunk)
Found possible branch point: file:///tmp/test-svn/trunk => \
file:///tmp/test-svn /branches/my-calc-branch, 75
Found branch parent: (my-calc-branch) d1957f3b307922124eec6314e15bcda59e3d9610
Following parent with do_switch
Successfully followed parent
r76 = 8624824ecc0badd73f40ea2f01fce51894189b01 (my-calc-branch)
Checked out HEAD:
file:///tmp/test-svn/branches/my-calc-branch r76
~~~
這相當于針對所提供的 URL 運行了兩條命令—— git svn init 加上 git svn fetch 。可能會花上一段時間。我們所用的測試項目僅僅包含 75 次提交并且它的代碼量不算大,所以只有幾分鐘而已。不過,Git 仍然需要提取每一個版本,每次一個,再逐個提交。對于一個包含成百上千次提交的項目,花掉的時間則可能是幾小時甚至數天。
`-T trunk -b branches -t tags` 告訴 Git 該 Subversion 倉庫遵循了基本的分支和標簽命名法則。如果你的主干(譯注:trunk,相當于非分布式版本控制里的master分支,代表開發的主線),分支或者標簽以不同的方式命名,則應做出相應改變。由于該法則的常見性,可以使用 -s 來代替整條命令,它意味著標準布局(s 是 Standard layout 的首字母),也就是前面選項的內容。下面的命令有相同的效果:
`$ git svn clone file:///tmp/test-svn -s`
現在,你有了一個有效的 Git 倉庫,包含著導入的分支和標簽:
~~~
$ git branch -a
* master
my-calc-branch
tags/2.0.2
tags/release-2.0.1
tags/release-2.0.2
tags/release-2.0.2rc1
trunk
~~~
值得注意的是,該工具分配命名空間時和遠程引用的方式不盡相同。克隆普通的 Git 倉庫時,可以以 origin/[branch] 的形式獲取遠程服務器上所有可用的分支——分配到遠程服務的名稱下。然而 git svn 假定不存在多個遠程服務器,所以把所有指向遠程服務的引用不加區分的保存下來。可以用 Git 探測命令 show-ref 來查看所有引用的全名。
~~~
$ git show-ref
1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/heads/master
aee1ecc26318164f355a883f5d99cff0c852d3c4 refs/remotes/my-calc-branch
03d09b0e2aad427e34a6d50ff147128e76c0e0f5 refs/remotes/tags/2.0.2
50d02cc0adc9da4319eeba0900430ba219b9c376 refs/remotes/tags/release-2.0.1
4caaa711a50c77879a91b8b90380060f672745cb refs/remotes/tags/release-2.0.2
1c4cb508144c513ff1214c3488abe66dcb92916f refs/remotes/tags/release-2.0.2rc1
1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/remotes/trunk
~~~
而普通的 Git 倉庫應該是這個模樣:
~~~
$ git show-ref
83e38c7a0af325a9722f2fdc56b10188806d83a1 refs/heads/master
3e15e38c198baac84223acfc6224bb8b99ff2281 refs/remotes/gitserver/master
0a30dd3b0c795b80212ae723640d4e5d48cabdff refs/remotes/origin/master
25812380387fdd55f916652be4881c6f11600d6f refs/remotes/origin/testing
~~~
這里有兩個遠程服務器:一個名為 `gitserver` ,具有一個 master分支;另一個叫 `origin`,具有` master` 和 `testing` 兩個分支。
注意本例中通過 git svn 導入的遠程引用,(Subversion 的)標簽是當作遠程分支添加的,而不是真正的 Git 標簽。導入的 Subversion 倉庫仿佛是有一個帶有不同分支的 tags 遠程服務器。
### 提交到 Subversion
有了可以開展工作的(本地)倉庫以后,你可以開始對該項目做出貢獻并向上游倉庫提交內容了,Git 這時相當于一個 SVN 客戶端。假如編輯了一個文件并進行提交,那么這次提交僅存在于本地的 Git 而非 Subversion 服務器上。
~~~
$ git commit -am 'Adding git-svn instructions to the README'
[master 97031e5] Adding git-svn instructions to the README
1 files changed, 1 insertions(+), 1 deletions(-)
~~~
接下來,可以將作出的修改推送到上游。值得注意的是,Subversion 的使用流程也因此改變了——你可以在離線狀態下進行多次提交然后一次性的推送到 Subversion 的服務器上。向 Subversion 服務器推送的命令是 `git svn dcommit:`
~~~
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r79
M README.txt
r79 = 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
~~~
所有在原 Subversion 數據基礎上提交的 commit 會一一提交到 Subversion,然后你本地 Git 的 commit 將被重寫,加入一個特別標識。這一步很重要,因為它意味著所有 commit 的 SHA-1 指都會發生變化。這也是同時使用 Git 和 Subversion 兩種服務作為遠程服務不是個好主意的原因之一。檢視以下最后一個 commit,你會找到新添加的 git-svn-id (譯注:即本段開頭所說的特別標識):
~~~
$ git log -1
commit 938b1a547c2cc92033b74d32030e86468294a5c8
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date: Sat May 2 22:06:44 2009 +0000
Adding git-svn instructions to the README
git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029
~~~
注意看,原本以 97031e5 開頭的 SHA-1 校驗值在提交完成以后變成了 938b1a5 。如果既要向 Git 遠程服務器推送內容,又要推送到 Subversion 遠程服務器,則必須先向 Subversion 推送(dcommit),因為該操作會改變所提交的數據內容。
### 拉取最新進展
如果要與其他開發者協作,總有那么一天你推送完畢之后,其他人發現他們推送自己修改的時候(與你推送的內容)產生沖突。這些修改在你合并之前將一直被拒絕。在 git svn 里這種情況形似:
~~~
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
Merge conflict during commit: Your file or directory 'README.txt' is probably \
out-of-date: resource out of date; try updating at /Users/schacon/libexec/git-\
core/git-svn line 482
~~~
為了解決該問題,可以運行 git svn rebase ,它會拉取服務器上所有最新的改變,再次基礎上衍合你的修改:
~~~
$ git svn rebase
M README.txt
r80 = ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk)
First, rewinding head to replay your work on top of it...
Applying: first user change
~~~
現在,你做出的修改都發生在服務器內容之后,所以可以順利的運行 `dcommit :`
~~~
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r81
M README.txt
r81 = 456cbe6337abe49154db70106d1836bc1332deed (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
~~~
需要牢記的一點是,Git 要求我們在推送之前先合并上游倉庫中最新的內容,而 git svn 只要求存在沖突的時候才這樣做。假如有人向一個文件推送了一些修改,這時你要向另一個文件推送一些修改,那么 dcommit 將正常工作:
~~~
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M configure.ac
Committed r84
M autogen.sh
r83 = 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk)
M configure.ac
r84 = cdbac939211ccb18aa744e581e46563af5d962d0 (trunk)
W: d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs/remotes/trunk differ, \
using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 \
015e4c98c482f0fa71e4d5434338014530b37fa6 M autogen.sh
First, rewinding head to replay your work on top of it...
Nothing to do.
~~~
這一點需要牢記,因為它的結果是推送之后項目處于一個不完整存在與任何主機上的狀態。如果做出的修改無法兼容但沒有產生沖突,則可能造成一些很難確診的難題。這和使用 Git 服務器是不同的——在 Git 世界里,發布之前,你可以在客戶端系統里完整的測試項目的狀態,而在 SVN 永遠都沒法確保提交前后項目的狀態完全一樣。
即使還沒打算進行提交,你也應該用這個命令從 Subversion 服務器拉取最新修改。sit svn fetch 能獲取最新的數據,不過 git svn rebase 才會在獲取之后在本地進行更新 。
~~~
$ git svn rebase
M generate_descriptor_proto.sh
r82 = bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/trunk.
~~~
不時地運行一下 git svn rebase 可以確保你的代碼沒有過時。不過,運行該命令時需要確保工作目錄的整潔。如果在本地做了修改,則必須在運行 git svn rebase 之前或暫存工作,或暫時提交內容——否則,該命令會發現衍合的結果包含著沖突因而終止。
### Git 分支問題
習慣了 Git 的工作流程以后,你可能會創建一些特性分支,完成相關的開發工作,然后合并他們。如果要用 git svn 向 Subversion 推送內容,那么最好是每次用衍合來并入一個單一分支,而不是直接合并。使用衍合的原因是 Subversion 只有一個線性的歷史而不像 Git 那樣處理合并,所以 git svn 在把快照轉換為 Subversion 的 commit 時只能包含第一個祖先。
假設分支歷史如下:創建一個 experiment 分支,進行兩次提交,然后合并到 master 。在 dcommit 的時候會得到如下輸出:
~~~
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M CHANGES.txt
Committed r85
M CHANGES.txt
r85 = 4bfebeec434d156c36f2bcd18f4e3d97dc3269a2 (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
COPYING.txt: locally modified
INSTALL.txt: locally modified
M COPYING.txt
M INSTALL.txt
Committed r86
M INSTALL.txt
M COPYING.txt
r86 = 2647f6b86ccfcaad4ec58c520e369ec81f7c283c (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
~~~
在一個包含了合并歷史的分支上使用 dcommit 可以成功運行,不過在 Git 項目的歷史中,它沒有重寫你在 experiment 分支中的兩個 commit ——另一方面,這些改變卻出現在了 SVN 版本中同一個合并 commit 中。
在別人克隆該項目的時候,只能看到這個合并 commit 包含了所有發生過的修改;他們無法獲知修改的作者和時間等提交信息。
### Subversion 分支
Subversion 的分支和 Git 中的不盡相同;避免過多的使用可能是最好方案。不過,用 git svn 創建和提交不同的 Subversion 分支仍是可行的。
#### 創建新的 SVN 分支
要在 Subversion 中建立一個新分支,需要運行 git svn branch [分支名] :
~~~
$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r87 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => \
file:///tmp/test-svn/branches/opera, 87
Found branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f
Following parent with do_switch
Successfully followed parent
r89 = 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera)
~~~
這相當于在 Subversion 中的 `svn copy trunk branches/opera` 命令,并會對` Subversion` 服務器進行相關操作。值得注意的是它沒有檢出和轉換到那個分支;如果現在進行提交,將提交到服務器上的 trunk, 而非` opera。`
### 切換當前分支
Git 通過搜尋提交歷史中 Subversion 分支的頭部來決定 dcommit 的目的地——而它應該只有一個,那就是當前分支歷史中最近一次包含 git-svn-id 的提交。
如果需要同時在多個分支上提交,可以通過導入 Subversion 上某個其他分支的 commit 來建立以該分支為 dcommit 目的地的本地分支。比如你想擁有一個并行維護的 opera 分支,可以運行
`$ git branch opera remotes/opera`
然后,如果要把 opera 分支并入 trunk (本地的 master 分支),可以使用普通的 git merge。不過最好提供一條描述提交的信息(通過 -m),否則這次合并的記錄是 Merge branch opera ,而不是任何有用的東西。
記住,雖然使用了 git merge 來進行這次操作,并且合并過程可能比使用 Subversion 簡單一些(因為 Git 會自動找到適合的合并基礎),這并不是一次普通的 Git 合并提交。最終它將被推送回 commit 無法包含多個祖先的 Subversion 服務器上;因而在推送之后,它將變成一個包含了所有在其他分支上做出的改變的單一 commit。把一個分支合并到另一個分支以后,你沒法像在 Git 中那樣輕易的回到那個分支上繼續工作。提交時運行的 dcommit 命令擦除了全部有關哪個分支被并入的信息,因而以后的合并基礎計算將是不正確的—— dcommit 讓 git merge 的結果變得類似于 git merge --squash。不幸的是,我們沒有什么好辦法來避免該情況—— Subversion 無法儲存這個信息,所以在使用它作為服務器的時候你將永遠為這個缺陷所困。為了不出現這種問題,在把本地分支(本例中的 opera)并入 trunk 以后應該立即將其刪除。
### 對應 Subversion 的命令
git svn 工具集合了若干個與 Subversion 類似的功能,對應的命令可以簡化向 Git 的轉化過程。下面這些命令能實現 Subversion 的這些功能。
#### SVN 風格的歷史
習慣了 Subversion 的人可能想以 SVN 的風格顯示歷史,運行 git svn log 可以讓提交歷史顯示為 SVN 格式:
~~~
$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) | 2 lines
autogen change
------------------------------------------------------------------------
r86 | schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May 2009) | 2 lines
Merge branch 'experiment'
------------------------------------------------------------------------
r85 | schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May 2009) | 2 lines
updated the changelog
~~~
關于 git svn log ,有兩點需要注意。首先,它可以離線工作,不像 svn log 命令,需要向 Subversion 服務器索取數據。其次,它僅僅顯示已經提交到 Subversion 服務器上的` commit`。在本地尚未` dcommit `的 Git 數據不會出現在這里;其他人向 `Subversion` 服務器新提交的數據也不會顯示。等于說是顯示了最近已知 Subversion 服務器上的狀態。
### SVN 日志
類似 git svn log 對 git log 的模擬,svn annotate 的等效命令是 git svn blame [文件名]。其輸出如下:
~~~
$ git svn blame README.txt
2 temporal Protocol Buffers - Google's data interchange format
2 temporal Copyright 2008 Google Inc.
2 temporal http://code.google.com/apis/protocolbuffers/
2 temporal
22 temporal C++ Installation - Unix
22 temporal =======================
2 temporal
79 schacon Committing in git-svn.
78 schacon
2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol
2 temporal Buffer compiler (protoc) execute the following:
2 temporal
~~~
同樣,它不顯示本地的 Git 提交以及 Subversion 上后來更新的內容。
### SVN 服務器信息
還可以使用 git svn info 來獲取與運行 svn info 類似的信息:
~~~
$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)
~~~
它與 blame 和 log 的相同點在于離線運行以及只更新到最后一次與 Subversion 服務器通信的狀態。
### 略 Subversion 之所略
假如克隆了一個包含了 svn:ignore 屬性的 Subversion 倉庫,就有必要建立對應的 .gitignore 文件來防止意外提交一些不應該提交的文件。git svn 有兩個有益于改善該問題的命令。第一個是` git svn create-ignore,`它自動建立對應的 .gitignore 文件,以便下次提交的時候可以包含它。
第二個命令是` git svn show-ignore`,它把需要放進 `.gitignore` 文件中的內容打印到標準輸出,方便我們把輸出重定向到項目的黑名單文件:
`$ git svn show-ignore > .git/info/exclude`
這樣一來,避免了 `.gitignore` 對項目的干擾。如果你是一個 Subversion 團隊里唯一的 Git 用戶,而其他隊友不喜歡項目包含 .gitignore,該方法是你的不二之選。
## Git-Svn 總結
git svn 工具集在當前不得不使用 Subversion 服務器或者開發環境要求使用 Subversion 服務器的時候格外有用。不妨把它看成一個跛腳的 Git,然而,你還是有可能在轉換過程中碰到一些困惑你和合作者們的迷題。為了避免麻煩,試著遵守如下守則:
保持一個不包含由` git merge` 生成的` commit `的線性提交歷史。將在主線分支外進行的開發通通衍合回主線;避免直接合并。
不要單獨建立和使用一個 Git 服務來搞合作。可以為了加速新開發者的克隆進程建立一個,但是不要向它提供任何不包含` git-svn-id `條目的內容。甚至可以添加一個` pre-receive` 掛鉤來在每一個提交信息中查找 git-svn-id 并拒絕提交那些不包含它的 commit。
如果遵循這些守則,在` Subversion` 上工作還可以接受。然而,如果能遷徙到真正的` Git` 服務器,則能為團隊帶來更多好處。
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 安裝 Git
- 1.5 初次運行 Git 前的配置
- 1.6 獲取幫助
- 1.7 小結
- 2. Git基礎
- 2.1 取得項目的 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 技巧和竅門
- 2.8 小結
- 3. Git分支
- 3.1 何謂分支
- 3.2 分支的新建與合并
- 3.3 分支的管理
- 3.4 利用分支進行開發的工作流程
- 3.5 遠程分支
- 3.6 分支的衍合
- 3.7 小結
- 4. 服務器上的Git
- 4.1 協議
- 4.2 在服務器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設服務器
- 4.5 公共訪問
- 4.6 GitWeb
- 4.7 Gitosis
- 4.8 Gitolite
- 4.9 Git 守護進程
- 4.10 Git 托管服務
- 4.11 小結
- 5. 分布式Git
- 5.1 分布式工作流程
- 5.2 為項目作貢獻
- 5.3 項目的管理
- 5.4 小結
- 6. Git工具
- 6.1 修訂版本(Revision)選擇
- 6.2 交互式暫存
- 6.3 儲藏(Stashing)
- 6.4 重寫歷史
- 6.5 使用 Git 調試
- 6.6 子模塊
- 6.7 子樹合并
- 6.8 總結
- 7. 自定義Git
- 7.1 配置 Git
- 7.2 Git屬性
- 7.3 Git掛鉤
- 7.4 Git 強制策略實例
- 7.5 總結
- 8. Git與其他系統
- 8.1 Git 與 Subversion
- 8.2 遷移到 Git
- 8.3 總結
- 9. Git 內部原理
- 9.2 Git 對象
- 9.3 Git References
- 9.4 Packfiles
- 9.5 The Refspec
- 9.6 傳輸協議
- 9.7 維護及數據恢復
- 9.8 總結
- 9.1 底層命令 (Plumbing) 和高層命令 (Porcelain)