Git 與其餘系統 - Git 與 Subversion


Git 與 Subversion

當前,大多數開發中的開源項目以及大量的商業項目都使用 Subversion 來管理源碼。做爲最流行的開源版本控制系統,Subversion 已經存在了接近十年的時間。它在許多方面與 CVS 十分相似,後者是前者出現以前代碼控制世界的霸主。git

Git 最爲重要的特性之一是名爲 git svn 的 Subversion 雙向橋接工具。該工具把 Git 變成了 Subversion 服務的客戶端,從而讓你在本地享受到 Git 全部的功能,然後直接向 Subversion 服務器推送內容,彷彿在本地使用了 Subversion 客戶端。也就是說,在其餘人忍受古董的同時,你能夠在本地享受分支合併,使暫存區域,衍合以及 單項挑揀等等。這是個讓 Git 偷偷潛入合做開發環境的好東西,在幫助你的開發同伴們提升效率的同時,它還能幫你勸說團隊讓整個項目框架轉向對 Git 的支持。這個 Subversion 之橋是通向分佈式版本控制系統(DVCS, Distributed VCS )世界的神奇隧道。api

git svn

Git 中全部 Subversion 橋接命令的基礎是 git svn 。全部的命令都從它開始。相關的命令數目很多,你將經過幾個簡單的工做流程瞭解到其中常見的一些。服務器

值得警惕的是,在使用 git svn 的時候,你實際是在與 Subversion 交互,Git 比它要高級複雜的多。儘管能夠在本地隨意的進行分支和合並,最好仍是經過衍合保持線性的提交歷史,儘可能避免相似與遠程 Git 倉庫動態交互這樣的操做。網絡

避免修改歷史再從新推送的作法,也不要同時推送到並行的 Git 倉庫來試圖與其餘 Git 用戶合做。Subersion 只能保存單一的線性提交歷史,一不當心就會被搞糊塗。合做團隊中同時有人用 SVN 和 Git,必定要確保全部人都使用 SVN 服務來協做——這會讓生活輕鬆不少。框架

初始設定

爲了展現功能,先要一個具備寫權限的 SVN 倉庫。若是想嘗試這個範例,你必須複製一份其中的測試倉庫。比較簡單的作法是使用一個名爲 svnsync 的工具。較新的 Subversion 版本中都帶有該工具,它將數據編碼爲用於網絡傳輸的格式。分佈式

要嘗試本例,先在本地新建一個 Subversion 倉庫:svn

$ 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 服務器,則能爲團隊帶來更多好處。

相關文章
相關標籤/搜索