如果在其他版本控制系統中保存了某項目的代碼而后決定轉而使用 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 -P "^<author" | sort -u | \
perl -pe 's/<author>(.*?)<\/author>/$1 = /' > users.txt
~~~
它將輸出 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 標簽,運行
`$ git for-each-ref refs/remotes/tags | cut -d / -f 4- | grep -v @ | while read tagname; do git tag "$tagname" "tags/$tagname"; git branch -r -d "tags/$tagname"; done`
該命令將原本以 tag/ 開頭的遠程分支的索引變成真正的(輕巧的)標簽。
接下來,把 `refs/remotes `下面剩下的索引變成本地分支:
`$ git for-each-ref refs/remotes | cut -d / -f 3- | grep -v @ | while read branchname; do git branch "$branchname" "refs/remotes/$branchname"; git branch -r -d "$branchname"; done`
現在所有的舊分支都變成真正的 Git 分支,所有的舊標簽也變成真正的 Git 標簽。最后一項工作就是把新建的 Git 服務器添加為遠程服務器并且向它推送。下面是新增遠程服務器的例子:
`$ git remote add origin git@my-git-server:myrepository.git`
為了讓所有的分支和標簽都得到上傳,我們使用這條命令:
~~~
$ git push origin --all
$ git push origin --tags
~~~
所有的分支和標簽現在都應該整齊干凈的躺在新的 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 腳本。
- 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)