Git 與其他系統 - Git 與 Subversion
Git 與 Subversion
當前,大多數開發中的開源項目以及大量的商業項目都使用 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</pre>
現在可以調用
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 +0000Adding git-svn instructions to the README
git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029</pre>
注意看,原本以
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 logr87 | 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</pre>
關于
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。
</ul>
如果遵循這些守則,在 Subversion 上工作還可以接受。然而,如果能遷徙到真正的 Git 服務器,則能為團隊帶來更多好處。
原文地址:http://git-scm.com/book/zh/Git-%E4%B8%8E%E5%85%B6%E4%BB%96%E7%B3%BB%E7%BB%9F-Git-%E4%B8%8E-Subver...