## Git詳解之八 Git與其他系統
**Git與其他系統**
世界不是完美的。大多數時候,將所有接觸到的項目全部轉向 Git是不可能的。有時我們不得不為某個項目使用其他的版本控制系統(VCS, Version Control System),其中比較常見的是Subversion。你將在本章的第一部分學習使用git svn?,Git為 Subversion附帶的雙向橋接工具。
或許現在你已經在考慮將先前的項目轉向 Git。本章的第二部分將介紹如何將項目遷移到 Git:先介紹從 Subversion的遷移,然后是 Perforce,最后介紹如何使用自定義的腳本進行非標準的導入。
?
**8.1? Git與 Subversion**
當前,大多數開發中的開源項目以及大量的商業項目都使用 Subversion來管理源碼。作為最流行的開源版本控制系統,Subversion已經存在了接近十年的時間。它在許多方面與 CVS十分類似,后者是前者出現之前代碼控制世界的霸主。
Git 最為重要的特性之一是名為?git svn?的 Subversion雙向橋接工具。該工具把 Git變成了 Subversion服務的客戶端,從而讓你在本地享受到 Git所有的功能,而后直接向 Subversion服務器推送內容,仿佛在本地使用了 Subversion客戶端。也就是說,在其他人忍受古董的同時,你可以在本地享受分支合并,使暫存區域,衍合以及單項挑揀等等。這是個讓 Git偷偷潛入合作開發環境的好東西,在幫助你的開發同伴們提高效率的同時,它還能幫你勸說團隊讓整個項目框架轉向對 Git的支持。這個 Subversion之橋是通向分布式版本控制系統(DVCS, Distributed VCS)世界的神奇隧道。
**git svn**
Git 中所有Subversion 橋接命令的基礎是?gitsvn?。所有的命令都從它開始。相關的命令數目不少,你將通過幾個簡單的工作流程了解到其中常見的一些。
值得警戒的是,在使用?gitsvn?的時候,你實際是在與 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
~~~
現在可以調用?svnsyncinit?加目標倉庫,再加源倉庫的格式來把該項目同步到本地了:
~~~
$ svnsync init file:///tmp/test-svnhttp://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-branchr76
~~~
這相當于針對所提供的 URL運行了兩條命令——?git svn init?加上?gitsvn fetch?。可能會花上一段時間。我們所用的測試項目僅僅包含 75次提交并且它的代碼量不算大,所以只有幾分鐘而已。不過,Git仍然需要提取每一個版本,每次一個,再逐個提交。對于一個包含成百上千次提交的項目,花掉的時間則可能是幾小時甚至數天。
-Ttrunk -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
1cbd4904d9982f386d87f88fce1c24ad7c0f0471refs/heads/master
aee1ecc26318164f355a883f5d99cff0c852d3c4refs/remotes/my-calc-branch
03d09b0e2aad427e34a6d50ff147128e76c0e0f5refs/remotes/tags/2.0.2
50d02cc0adc9da4319eeba0900430ba219b9c376refs/remotes/tags/release-2.0.1
4caaa711a50c77879a91b8b90380060f672745cbrefs/remotes/tags/release-2.0.2
1c4cb508144c513ff1214c3488abe66dcb92916frefs/remotes/tags/release-2.0.2rc1
1cbd4904d9982f386d87f88fce1c24ad7c0f0471refs/remotes/trunk
~~~
而普通的 Git倉庫應該是這個模樣:
~~~
$ git show-ref
83e38c7a0af325a9722f2fdc56b10188806d83a1refs/heads/master
3e15e38c198baac84223acfc6224bb8b99ff2281refs/remotes/gitserver/master
0a30dd3b0c795b80212ae723640d4e5d48cabdffrefs/remotes/origin/master
25812380387fdd55f916652be4881c6f11600d6frefs/remotes/origin/testing
~~~
這里有兩個遠程服務器:一個名為?gitserver?,具有一個?master分支;另一個叫?origin,具有?master?和?testing?兩個分支。
注意本例中通過?gitsvn?導入的遠程引用,(Subversion的)標簽是當作遠程分支添加的,而不是真正的 Git標簽。導入的 Subversion倉庫仿佛是有一個帶有不同分支的 tags遠程服務器。
**提交到 Subversion**
有了可以開展工作的(本地)倉庫以后,你可以開始對該項目做出貢獻并向上游倉庫提交內容了,Git這時相當于一個 SVN客戶端。假如編輯了一個文件并進行提交,那么這次提交僅存在于本地的 Git而非 Subversion服務器上。
~~~
$ git commit -am 'Adding git-svn instructions to theREADME'
[master 97031e5] Adding git-svn instructions to theREADME
?1 files changed, 1insertions(+), 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 222:06:44 2009 +0000
?
??? Adding git-svninstructions 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
~~~
為了解決該問題,可以運行?gitsvn 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 andrefs/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.
~~~
不時地運行一下?gitsvn 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 [分支名]?Tocreate a new branch in Subversion, you rungit svn branch [branchname]:
~~~
$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r87 tofile:///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?,而不是任何有用的東西。
記住,雖然使用了?gitmerge?來進行這次操作,并且合并過程可能比使用 Subversion 簡單一些(因為 Git會自動找到適合的合并基礎),這并不是一次普通的 Git合并提交。最終它將被推送回 commit無法包含多個祖先的 Subversion服務器上;因而在推送之后,它將變成一個包含了所有在其他分支上做出的改變的單一 commit。把一個分支合并到另一個分支以后,你沒法像在 Git中那樣輕易的回到那個分支上繼續工作。提交時運行的dcommit?命令擦除了全部有關哪個分支被并入的信息,因而以后的合并基礎計算將是不正確的—— dcommit讓?git merge?的結果變得類似于gitmerge --squash。不幸的是,我們沒有什么好辦法來避免該情況—— Subversion 無法儲存這個信息,所以在使用它作為服務器的時候你將永遠為這個缺陷所困。為了不出現這種問題,在把本地分支(本例中的opera)并入 trunk以后應該立即將其刪除。
**對應 Subversion的命令**
gitsvn?工具集合了若干個與 Subversion類似的功能,對應的命令可以簡化向 Git的轉化過程。下面這些命令能實現 Subversion的這些功能。
**SVN風格的歷史**
習慣了 Subversion的人可能想以 SVN 的風格顯示歷史,運行?git svn log?可以讓提交歷史顯示為SVN格式:
~~~
$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May2009) | 2 lines
?
autogen change
?
------------------------------------------------------------------------
r86 | schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May2009) | 2 lines
?
Merge branch 'experiment'
?
------------------------------------------------------------------------
r85 | schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May2009) | 2 lines
?
updated the changelog
~~~
關于?gitsvn log?,有兩點需要注意。首先,它可以離線工作,不像?svnlog?命令,需要向 Subversion 服務器索取數據。其次,它僅僅顯示已經提交到 Subversion服務器上的 commit。在本地尚未 dcommit的 Git 數據不會出現在這里;其他人向 Subversion服務器新提交的數據也不會顯示。等于說是顯示了最近已知 Subversion服務器上的狀態。
**SVN日志**
類似?gitsvn log?對?git log?的模擬,svn annotate?的等效命令是git svn blame [文件名]。其輸出如下:
~~~
$ git svn blame README.txt
?2?? temporal Protocol Buffers - Google's datainterchange format
?2?? temporal Copyright 2008 Google Inc.
?2?? temporalhttp://code.google.com/apis/protocolbuffers/
?2?? temporal
22?? temporal C++ Installation- Unix
22?? temporal=======================
?2?? temporal
79??? schaconCommitting 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 thefollowing:
?2?? temporal
~~~
同樣,它不顯示本地的 Git提交以及 Subversion上后來更新的內容。
**SVN服務器信息**
還可以使用?gitsvn 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 May2009)
~~~
它與?blame?和?log?的相同點在于離線運行以及只更新到最后一次與 Subversion 服務器通信的狀態。
**略 Subversion之所略**
假如克隆了一個包含了?svn:ignore?屬性的 Subversion 倉庫,就有必要建立對應的?.gitignore?文件來防止意外提交一些不應該提交的文件。git svn?有兩個有益于改善該問題的命令。第一個是git svn create-ignore,它自動建立對應的.gitignore?文件,以便下次提交的時候可以包含它。
第二個命令是?gitsvn show-ignore,它把需要放進?.gitignore?文件中的內容打印到標準輸出,方便我們把輸出重定向到項目的黑名單文件:
~~~
$ git svn show-ignore > .git/info/exclude
~~~
這樣一來,避免了?.gitignore?對項目的干擾。如果你是一個 Subversion 團隊里唯一的 Git用戶,而其他隊友不喜歡項目包含.gitignore,該方法是你的不二之選。
**Git-Svn總結**
gitsvn?工具集在當前不得不使用 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**
讀過前一節有關?gitsvn?的內容以后,你應該能輕而易舉的根據其中的指導來?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用戶數據。
為?gitsvn?提供該文件可以然它更精確的映射作者數據。你還可以在?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 300: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 300: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 origingit@my-git-server:myrepository.git
~~~
為了讓所有的分支和標簽都得到上傳,我們使用這條命令:
~~~
$ git push origin --all
~~~
所有的分支和標簽現在都應該整齊干凈的躺在新的 Git服務器里了。
**Perforce**
你將了解到的下一個被導入的系統是 Perforce. Git發行的時候同時也附帶了一個Perforce導入腳本,不過它是包含在源碼的?contrib?部分——而不像gitsvn?那樣默認可用。運行它之前必須獲取 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-p4clone?命令將從 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 1910:18:45 2004 -0800
?
??? Drop 'rc3'moniker of jam-2.5.? Folded rc2 and rc3RELNOTES into
??? the main partof the document.? Built new tar/zipballs.
?
??? Only 16 monthslater.
?
??? [git-p4:depot-paths = "//public/jam/src/": change = 4409]
?
commit ca8870db541a23ed867f38847eda65bf4363371d
Author: Richard Geiger <rmg@perforce.com>
Date:?? Tue Apr 2220:51:34 2003 -0800
?
??? Update derivedjamgram.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
~~~
現在運行一下?gitlog,你會發現這些commit的 SHA-1 校驗值都發生了改變,而那些?git-p4?字串則從提交信息里消失了:
~~~
$ git log -2
commit 10a16d60cffca14d454a15c6164378f4082bc5b0
Author: Perforce staff <support@perforce.com>
Date:?? Thu Aug 1910:18:45 2004 -0800
?
??? Drop 'rc3'moniker of jam-2.5.? Folded rc2 and rc3RELNOTES into
??? the main partof the document.? Built new tar/zipballs.
?
??? Only 16 monthslater.
?
commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2
Author: Richard Geiger <rmg@perforce.com>
Date:?? Tue Apr 2220:51:34 2003 -0800
?
??? Update derivedjamgram.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 ifFile.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'
??? returnTime.now().to_i
? else
??? dir =dir.gsub('back_', '')
??? (year, month,day) = dir.split('_')
??? returnTime.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 312:57:39 2009 -0700
??? imported fromcurrent
commit 7e519590de754d079dd73b44d695a42c9d2df452
Author: Scott Chacon <schacon@example.com>
Date:?? Tue Feb 301:00:00 2009 -0700
??? imported fromback_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內部的原始數據格式,從而是使你能親手鍛造其中的每一個字節,如果必要的話。
?