Git詳解之八 Git與其餘系統

Git 與其餘系統

世界不是完美的。大多數時候,將全部接觸到的項目所有轉向 Git 是不可能的。有時咱們不得不爲某個項目使用其餘的版本控制系統(VCS, Version Control System ),其中比較常見的是 Subversion 。你將在本章的第一部分學習使用git svn ,Git 爲 Subversion 附帶的雙向橋接工具。 git

或許如今你已經在考慮將先前的項目轉向 Git 。本章的第二部分將介紹如何將項目遷移到 Git:先介紹從 Subversion 的遷移,而後是 Perforce,最後介紹如何使用自定義的腳本進行非標準的導入。 windows

 

8.1  Git 與 Subversion

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

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

git svn

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

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

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

初始設定

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

要嘗試本例,先在本地新建一個 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 加上 gitsvn 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 [分支名] To create a new branch in Subversion, you rungit svn branch [branchname]:

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

 

8.2  遷移到 Git

若是在其餘版本控制系統中保存了某項目的代碼然後決定轉而使用 Git,那麼該項目必須經歷某種形式的遷移。本節將介紹 Git 中包含的一些針對常見系統的導入腳本,並將展現編寫自定義的導入腳本的方法。

導入

你將學習到如何從專業重量級的版本控制系統中導入數據—— Subversion 和 Perforce —— 由於據我所知這兩者的用戶是(向 Git)轉換的主要羣體,並且 Git 爲此兩者附帶了高質量的轉換工具。

Subversion

讀過前一節有關 git svn 的內容之後,你應該能垂手可得的根據其中的指導來 git svn clone 一個倉庫了;而後,中止 Subversion 的使用,向一個新 Git server 推送,並開始使用它。想保留歷史記錄,所花的時間應該不過就是從 Subversion 服務器拉取數據的時間(可能要等上好一會就是了)。

然而,這樣的導入並不完美;並且還要花那麼多時間,不如干脆一次把它作對!首當其衝的任務是做者信息。在 Subversion,每一個提交者在都在主機上有一個用戶名,記錄在提交信息中。上節例子中多處顯示了schacon ,好比 blame 的輸出以及 git svn log。若是想讓這條信息更好的映射到 Git 做者數據裏,則須要 從 Subversion 用戶名到 Git 做者的一個映射關係。創建一個叫作user.txt 的文件,用以下格式表示映射關係:

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

經過該命令能夠得到 SVN 做者的列表:

$ svn log --xml | grep author | sort -u | perl -pe 's/.>(.?)<./$1 = /'

它將輸出 XML 格式的日誌——你能夠找到做者,創建一個單獨的列表,而後從 XML 中抽取出須要的信息。(顯而易見,本方法要求主機上安裝了grep,sort 和perl.)而後把輸出重定向到 user.txt 文件,而後就能夠在每一項的後面添加相應的 Git 用戶數據。

爲 git svn 提供該文件能夠然它更精確的映射做者數據。你還能夠在 clone 或者 init後面添加--no-metadata 來阻止 git svn 包含那些 Subversion 的附加信息。這樣 import 命令就變成了:

$ git-svn clone http://my-project.googlecode.com/svn/ \
      --authors-file=users.txt --no-metadata -s my_project

如今 my_project 目錄下導入的 Subversion 應該比原來整潔多了。原來的 commit 看上去是這樣:

commit 37efa680e8473b615de980fa935944215428a35a
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

    git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
    be05-5f7a86268029

如今是這樣:

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

不只做者一項乾淨了很多,git-svn-id 也就此消失了。

你還須要一點 post-import(導入後) 清理工做。最起碼的,應該清理一下 git svn 建立的那些怪異的索引結構。首先要移動標籤,把它們從奇怪的遠程分支變成實際的標籤,而後把剩下的分支移動到本地。

要把標籤變成合適的 Git 標籤,運行

$ cp -Rf .git/refs/remotes/tags/* .git/refs/tags/
$ rm -Rf .git/refs/remotes/tags

該命令將本來以 tag/ 開頭的遠程分支的索引變成真正的(輕巧的)標籤。

接下來,把 refs/remotes 下面剩下的索引變成本地分支:

$ cp -Rf .git/refs/remotes/* .git/refs/heads/
$ rm -Rf .git/refs/remotes

如今全部的舊分支都變成真正的 Git 分支,全部的舊標籤也變成真正的 Git 標籤。最後一項工做就是把新建的 Git 服務器添加爲遠程服務器而且向它推送。下面是新增遠程服務器的例子:

$ git remote add origin git@my-git-server:myrepository.git

爲了讓全部的分支和標籤都獲得上傳,咱們使用這條命令:

$ git push origin --all

全部的分支和標籤如今都應該整齊乾淨的躺在新的 Git 服務器裏了。

Perforce

你將瞭解到的下一個被導入的系統是 Perforce. Git 發行的時候同時也附帶了一個 Perforce 導入腳本,不過它是包含在源碼的 contrib 部分——而不像git svn 那樣默承認用。運行它以前必須獲取 Git 的源碼,能夠在 git.kernel.org 下載:

$ git clone git://git.kernel.org/pub/scm/git/git.git
$ cd git/contrib/fast-import

在這個 fast-import 目錄下,應該有一個叫作 git-p4 的 Python 可執行腳本。主機上必須裝有 Python 和p4 工具該導入才能正常進行。例如,你要從 Perforce 公共代碼倉庫(譯註: Perforce Public Depot,Perforce 官方提供的代碼寄存服務)導入 Jam 工程。爲了設定客戶端,咱們要把 P4PORT 環境變量 export 到 Perforce 倉庫:

$ export P4PORT=public.perforce.com:1666

運行 git-p4 clone 命令將從 Perforce 服務器導入 Jam 項目,咱們須要給出倉庫和項目的路徑以及導入的目標路徑:

$ git-p4 clone //public/jam/src@all /opt/p4import
Importing from //public/jam/src@all into /opt/p4import
Reinitialized existing Git repository in /opt/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 4409 (100%)

如今去 /opt/p4import 目錄運行一下 git log ,就能看到導入的成果:

$ git log -2
commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2
Author: Perforce staff <support@perforce.com>
Date:   Thu Aug 19 10:18:45 2004 -0800

    Drop 'rc3' moniker of jam-2.5.  Folded rc2 and rc3 RELNOTES into
    the main part of the document.  Built new tar/zip balls.

    Only 16 months later.

    [git-p4: depot-paths = "//public/jam/src/": change = 4409]

commit ca8870db541a23ed867f38847eda65bf4363371d
Author: Richard Geiger <rmg@perforce.com>
Date:   Tue Apr 22 20:51:34 2003 -0800

    Update derived jamgram.c

    [git-p4: depot-paths = "//public/jam/src/": change = 3108]

每個 commit 裏都有一個 git-p4 標識符。這個標識符能夠保留,以防之後須要引用 Perforce 的修改版本號。然而,若是想刪除這些標識符,如今正是時候——在開啓新倉庫以前。能夠經過git filter-branch 來批量刪除這些標識符:

$ git filter-branch --msg-filter '
        sed -e "/^\[git-p4:/d"
'
Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123)
Ref 'refs/heads/master' was rewritten

如今運行一下 git log,你會發現這些 commit 的 SHA-1 校驗值都發生了改變,而那些 git-p4 字串則從提交信息裏消失了:

$ git log -2
commit 10a16d60cffca14d454a15c6164378f4082bc5b0
Author: Perforce staff <support@perforce.com>
Date:   Thu Aug 19 10:18:45 2004 -0800

    Drop 'rc3' moniker of jam-2.5.  Folded rc2 and rc3 RELNOTES into
    the main part of the document.  Built new tar/zip balls.

    Only 16 months later.

commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2
Author: Richard Geiger <rmg@perforce.com>
Date:   Tue Apr 22 20:51:34 2003 -0800

    Update derived jamgram.c

至此導入已經完成,能夠開始向新的 Git 服務器推送了。

自定導入腳本

若是先前的系統不是 Subversion 或 Perforce 之一,先上網找一下有沒有與之對應的導入腳本——導入 CVS,Clear Case,Visual Source Safe,甚至存檔目錄的導入腳本已經存在。假如這些工具都不適用,或者使用的工具不多見,抑或你須要導入過程具備更多可制定性,則應該使用git fast-import。該命令從標準輸入讀取簡單的指令來寫入具體的 Git 數據。這樣建立 Git 對象比運行純 Git 命令或者手動寫對象要簡單的多(更多相關內容見第九章)。經過它,你能夠編寫一個導入腳原本從導入源讀取必要的信息,同時在標準輸出直接輸出相關指示。你能夠運行該腳本並把它的輸出管道鏈接到git fast-import。

下面演示一下如何編寫一個簡單的導入腳本。假設你在進行一項工做,而且按時經過把工做目錄複製爲以時間戳back_YY_MM_DD 命名的目錄來進行備份,如今你須要把它們導入 Git 。目錄結構以下:

$ ls /opt/import_from
back_2009_01_02
back_2009_01_04
back_2009_01_14
back_2009_02_03
current

爲了導入到一個 Git 目錄,咱們首先回顧一下 Git 儲存數據的方式。你可能還記得,Git 本質上是一個 commit 對象的鏈表,每個對象指向一個內容的快照。而這裏須要作的工做就是告訴fast-import 內容快照的位置,什麼樣的 commit 數據指向它們,以及它們的順序。咱們採起一次處理一個快照的策略,爲每個內容目錄創建對應的 commit ,每個 commit 與以前的創建連接。

正如在第七章 「Git 執行策略一例」 一節中同樣,咱們將使用 Ruby 來編寫這個腳本,由於它是我平常使用的語言並且閱讀起來簡單一些。你能夠用任何其餘熟悉的語言來重寫這個例子——它僅須要把必要的信息打印到標準輸出而已。同時,若是你在使用 Windows,這意味着你要特別留意不要在換行的時候引入回車符(譯註:carriage returns,Windows 換行時加入的符號,一般說的\r )—— Git 的 fast-import 對僅使用換行符(LF)而非 Windows 的回車符(CRLF)要求很是嚴格。

首先,進入目標目錄而且找到全部子目錄,每個子目錄將做爲一個快照被導入爲一個 commit。咱們將依次進入每個子目錄並打印所需的命令來導出它們。腳本的主循環大體是這樣:

last_mark = nil

# 循環遍歷全部目錄
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # 進入目標目錄
    Dir.chdir(dir) do 
      last_mark = print_export(dir, last_mark)
    end
  end
end

咱們在每個目錄裏運行 print_export ,它會取出上一個快照的索引和標記並返回本次快照的索引和標記;由此咱們就能夠正確的把兩者鏈接起來。」標記(mark)」 是fast-import 中對 commit 標識符的叫法;在建立 commit 的同時,咱們逐一賦予一個標記以便之後在把它鏈接到其餘 commit 時使用。所以,在print_export 方法中要作的第一件事就是根據目錄名生成一個標記:

mark = convert_dir_to_mark(dir)

實現該函數的方法是創建一個目錄的數組序列並使用數組的索引值做爲標記,由於標記必須是一個整數。這個方法大體是這樣的:

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

有了整數來表明每一個 commit,咱們如今須要提交附加信息中的日期。因爲日期是用目錄名錶示的,咱們就從中解析出來。print_export 文件的下一行將是:

date = convert_dir_to_date(dir)

而 convert_dir_to_date 則定義爲

def convert_dir_to_date(dir)
  if dir == 'current'
    return Time.now().to_i
  else
    dir = dir.gsub('back_', '')
    (year, month, day) = dir.split('_')
    return Time.local(year, month, day).to_i
  end
end

它爲每一個目錄返回一個整型值。提交附加信息裏最後一項所需的是提交者數據,咱們在一個全局變量中直接定義之:

$author = 'Scott Chacon <schacon@example.com>'

咱們差很少能夠開始爲導入腳本輸出提交數據了。第一項信息指明咱們定義的是一個 commit 對象以及它所在的分支,隨後是咱們生成的標記,提交者信息以及提交備註,而後是前一個 commit 的索引,若是有的話。代碼大體這樣:

# 打印導入所需的信息
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark

時區(-0700)處於簡化目的使用硬編碼。若是是從其餘版本控制系統導入,則必須以變量的形式指明時區。 提交備註必須以特定格式給出:

data (size)\n(contents)

該格式包含了單詞 data,所讀取數據的大小,一個換行符,最後是數據自己。因爲隨後指明文件內容的時候要用到相同的格式,咱們寫一個輔助方法,export_data:

def export_data(string)
  print "data #{string.size}\n#{string}"
end

惟一剩下的就是每個快照的內容了。這簡單的很,由於它們分別處於一個目錄——你能夠輸出 deleeall 命令,隨後是目錄中每一個文件的內容。Git 會正確的記錄每個快照:

puts 'deleteall'
Dir.glob("**/*").each do |file| next if !File.file?(file)
  inline_data(file)
end

注意:因爲不少系統把每次修訂看做一個 commit 到另外一個 commit 的變化量,fast-import 也能夠依據每次提交獲取一個命令來指出哪些文件被添加,刪除或者修改過,以及修改的內容。咱們將須要計算快照之間的差異而且僅僅給出這項數據,不過該作法要複雜不少——還如不直接把全部數據丟給 Git 然它本身搞清楚。假如前面這個方法更適用於你的數據,參考fast-import 的 man 幫助頁面來了解如何以這種方式提供數據。

列舉新文件內容或者指明帶有新內容的已修改文件的格式以下:

M 644 inline path/to/file
data (size)
(file contents)

這裏,644 是權限模式(加入有可執行文件,則須要探測之並設定爲 755),而 inline 說明咱們在本行結束以後當即列出文件的內容。咱們的 inline_data 方法大體是:

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

咱們重用了前面定義過的 export_data,由於這裏和指明提交註釋的格式一模一樣。

最後一項工做是返回當前的標記以便下次循環的使用。

return mark

注意:若是你在用 Windows,必定記得添加一項額外的步驟。前面提過,Windows 使用 CRLF 做爲換行字符而 Git fast-import 只接受 LF。爲了繞開這個問題來知足 git fast-import,你須要讓 ruby 用 LF 取代 CRLF:

$stdout.binmode

搞定了。如今運行該腳本,你將獲得以下內容:

$ ruby import.rb /opt/import_from 
commit refs/heads/master
mark :1
committer Scott Chacon <schacon@geemail.com> 1230883200 -0700
data 29
imported from back_2009_01_02deleteall
M 644 inline file.rb
data 12
version two
commit refs/heads/master
mark :2
committer Scott Chacon <schacon@geemail.com> 1231056000 -0700
data 29
imported from back_2009_01_04from :1
deleteall
M 644 inline file.rb
data 14
version three
M 644 inline new.rb
data 16
new version one
(...)

要運行導入腳本,在須要導入的目錄把該內容用管道定向到 git fast-import。你能夠創建一個空目錄而後運行 git init做爲開頭,而後運行該腳本:

$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:           18 (         1 duplicates                  )
      blobs  :            7 (         1 duplicates          0 deltas)
      trees  :            6 (         0 duplicates          1 deltas)
      commits:            5 (         0 duplicates          0 deltas)
      tags   :            0 (         0 duplicates          0 deltas)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              3
Memory total:          2255 KiB
       pools:          2098 KiB
     objects:           156 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize =   33554432
pack_report: core.packedGitLimit      =  268435456
pack_report: pack_used_ctr            =          9
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =       1356 /       1356
---------------------------------------------------------------------

你會發現,在它成功執行完畢之後,會給出一堆有關已完成工做的數據。上例在一個分支導入了5次提交數據,包含了18個對象。如今能夠運行 git log 來檢視新的歷史:

$ git log -2
commit 10bfe7d22ce15ee25b60a824c8982157ca593d41
Author: Scott Chacon <schacon@example.com>
Date:   Sun May 3 12:57:39 2009 -0700

    imported from current

commit 7e519590de754d079dd73b44d695a42c9d2df452
Author: Scott Chacon <schacon@example.com>
Date:   Tue Feb 3 01:00:00 2009 -0700

    imported from back_2009_02_03

就它了——一個乾淨整潔的 Git 倉庫。須要注意的是此時沒有任何內容被檢出——剛開始當前目錄裏沒有任何文件。要獲取它們,你得轉到 master 分支的所在:

$ ls
$ git reset --hard master
HEAD is now at 10bfe7d imported from current
$ ls
file.rb  lib

fast-import 還能夠作更多——處理不一樣的文件模式,二進制文件,多重分支與合併,標籤,進展標識等等。一些更加複雜的實例能夠在 Git 源碼的contib/fast-import 目錄裏找到;其中較爲出衆的是前面提過的 git-p4 腳本。

8.3  總結

如今的你應該掌握了在 Subversion 上使用 Git 以及把幾乎任何先存倉庫無損失的導入爲 Git 倉庫。下一章將介紹 Git 內部的原始數據格式,從而是使你能親手鍛造其中的每個字節,若是必要的話。

相關文章
相關標籤/搜索