遷移到 Git

2018-09-27 15:51 更新

如果在其他版本控制系統(tǒng)中保存了某項目的代碼而后決定轉(zhuǎn)而使用 Git,那么該項目必須經(jīng)歷某種形式的遷移。本節(jié)將介紹 Git 中包含的一些針對常見系統(tǒng)的導(dǎo)入腳本,并將展示編寫自定義的導(dǎo)入腳本的方法。

導(dǎo)入

你將學習到如何從專業(yè)重量級的版本控制系統(tǒng)中導(dǎo)入數(shù)據(jù)—— Subversion 和 Perforce —— 因為據(jù)我所知這二者的用戶是(向 Git)轉(zhuǎn)換的主要群體,而且 Git 為此二者附帶了高質(zhì)量的轉(zhuǎn)換工具。

Subversion

讀過前一節(jié)有關(guān) git svn 的內(nèi)容以后,你應(yīng)該能輕而易舉的根據(jù)其中的指導(dǎo)來 git svn clone 一個倉庫了;然后,停止 Subversion 的使用,向一個新 Git server 推送,并開始使用它。想保留歷史記錄,所花的時間應(yīng)該不過就是從 Subversion 服務(wù)器拉取數(shù)據(jù)的時間(可能要等上好一會就是了)。

然而,這樣的導(dǎo)入并不完美;而且還要花那么多時間,不如干脆一次把它做對!首當其沖的任務(wù)是作者信息。在 Subversion,每個提交者在都在主機上有一個用戶名,記錄在提交信息中。上節(jié)例子中多處顯示了 schacon ,比如 blame 的輸出以及 git svn log。如果想讓這條信息更好的映射到 Git 作者數(shù)據(jù)里,則需要 從 Subversion 用戶名到 Git 作者的一個映射關(guān)系。建立一個叫做 user.txt 的文件,用如下格式表示映射關(guān)系:

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 文件,然后就可以在每一項的后面添加相應(yīng)的 Git 用戶數(shù)據(jù)。

為 git svn 提供該文件可以然它更精確的映射作者數(shù)據(jù)。你還可以在 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

現(xiàn)在 my_project 目錄下導(dǎo)入的 Subversion 應(yīng)該比原來整潔多了。原來的 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

現(xiàn)在是這樣:

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(導(dǎo)入后) 清理工作。最起碼的,應(yīng)該清理一下 git svn 創(chuàng)建的那些怪異的索引結(jié)構(gòu)。首先要移動標簽,把它們從奇怪的遠程分支變成實際的標簽,然后把剩下的分支移動到本地。

要把標簽變成合適的 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
現(xiàn)在所有的舊分支都變成真正的 Git 分支,所有的舊標簽也變成真正的 Git 標簽。最后一項工作就是把新建的 Git 服務(wù)器添加為遠程服務(wù)器并且向它推送。下面是新增遠程服務(wù)器的例子:

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

為了讓所有的分支和標簽都得到上傳,我們使用這條命令:

$ git push origin --all
$ git push origin --tags

所有的分支和標簽現(xiàn)在都應(yīng)該整齊干凈的躺在新的 Git 服務(wù)器里了。

Perforce

你將了解到的下一個被導(dǎo)入的系統(tǒng)是 Perforce. Git 發(fā)行的時候同時也附帶了一個 Perforce 導(dǎo)入腳本,不過它是包含在源碼的 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 目錄下,應(yīng)該有一個叫做 git-p4Python 可執(zhí)行腳本。主機上必須裝有 Python 和 p4 工具該導(dǎo)入才能正常進行。例如,你要從 Perforce 公共代碼倉庫(譯注: Perforce Public Depot,Perforce 官方提供的代碼寄存服務(wù))導(dǎo)入 Jam 工程。為了設(shè)定客戶端,我們要把 P4PORT 環(huán)境變量 export 到 Perforce 倉庫:

$ export P4PORT=public.perforce.com:1666

運行git-p4 clone命令將從 Perforce服務(wù)器導(dǎo)入 Jam 項目,我們需要給出倉庫和項目的路徑以及導(dǎo)入的目標路徑:

$ 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%)

現(xiàn)在去 /opt/p4import 目錄運行一下 git log ,就能看到導(dǎo)入的成果:

$ 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 的修改版本號。然而,如果想刪除這些標識符,現(xiàn)在正是時候——在開啟新倉庫之前??梢酝ㄟ^git filter-branch 來批量刪除這些標識符:

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

現(xiàn)在運行一下 git log,你會發(fā)現(xiàn)這些 commit 的 SHA-1 校驗值都發(fā)生了改變,而那些 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

至此導(dǎo)入已經(jīng)完成,可以開始向新的 Git 服務(wù)器推送了。

自定導(dǎo)入腳本

如果先前的系統(tǒng)不是 Subversion 或 Perforce 之一,先上網(wǎng)找一下有沒有與之對應(yīng)的導(dǎo)入腳本——導(dǎo)入 CVS,Clear Case,Visual Source Safe,甚至存檔目錄的導(dǎo)入腳本已經(jīng)存在。假如這些工具都不適用,或者使用的工具很少見,抑或你需要導(dǎo)入過程具有更多可制定性,則應(yīng)該使用 git fast-import。該命令從標準輸入讀取簡單的指令來寫入具體的 Git 數(shù)據(jù)。這樣創(chuàng)建 Git 對象比運行純 Git 命令或者手動寫對象要簡單的多(更多相關(guān)內(nèi)容見第九章)。通過它,你可以編寫一個導(dǎo)入腳本來從導(dǎo)入源讀取必要的信息,同時在標準輸出直接輸出相關(guān)指示。你可以運行該腳本并把它的輸出管道連接到 git fast-import。

下面演示一下如何編寫一個簡單的導(dǎo)入腳本。假設(shè)你在進行一項工作,并且按時通過把工作目錄復(fù)制為以時間戳 back_YY_MM_DD 命名的目錄來進行備份,現(xiàn)在你需要把它們導(dǎo)入 Git 。目錄結(jié)構(gòu)如下:

$ ls /opt/import_from
back_2009_01_02
back_2009_01_04
back_2009_01_14
back_2009_02_03
current

為了導(dǎo)入到一個 Git 目錄,我們首先回顧一下 Git 儲存數(shù)據(jù)的方式。你可能還記得,Git 本質(zhì)上是一個 commit 對象的鏈表,每一個對象指向一個內(nèi)容的快照。而這里需要做的工作就是告訴 fast-import 內(nèi)容快照的位置,什么樣的 commit 數(shù)據(jù)指向它們,以及它們的順序。我們采取一次處理一個快照的策略,為每一個內(nèi)容目錄建立對應(yīng)的 commit ,每一個 commit 與之前的建立鏈接。

正如在第七章 "Git 執(zhí)行策略一例" 一節(jié)中一樣,我們將使用 Ruby 來編寫這個腳本,因為它是我日常使用的語言而且閱讀起來簡單一些。你可以用任何其他熟悉的語言來重寫這個例子——它僅需要把必要的信息打印到標準輸出而已。同時,如果你在使用 Windows,這意味著你要特別留意不要在換行的時候引入回車符(譯注:carriage returns,Windows 換行時加入的符號,通常說的\r )—— git fast-import 對僅使用換行符(LF)而非 Windows 的回車符(CRLF)要求非常嚴格。

首先,進入目標目錄并且找到所有子目錄,每一個子目錄將作為一個快照被導(dǎo)入為一個 commit。我們將依次進入每一個子目錄并打印所需的命令來導(dǎo)出它們。腳本的主循環(huán)大致是這樣:

last_mark = nil

循環(huán)遍歷所有目錄

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 標識符的叫法;在創(chuàng)建 commit 的同時,我們逐一賦予一個標記以便以后在把它連接到其他 commit 時使用。因此,在 print_export 方法中要做的第一件事就是根據(jù)目錄名生成一個標記:

mark = convert_dir_to_mark(dir)

實現(xiàn)該函數(shù)的方法是建立一個目錄的數(shù)組序列并使用數(shù)組的索引值作為標記,因為標記必須是一個整數(shù)。這個方法大致是這樣的:

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end
有了整數(shù)來
~~~代表每個 commit,我們現(xiàn)在需要提交附加信息中的日期。由于日期是用目錄名表示的,我們就從中解析出來。print_export 文件的下一行將是:

`date = convert_dir_to_date(dir)`

而` convert_dir_to_date` 則定義為

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

它為每個目錄返回一個整型值。提交附加信息里最后一項所需的是提交者數(shù)據(jù),我們在一個全局變量中直接定義之:

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

我們差不多可以開始為導(dǎo)入腳本輸出提交數(shù)據(jù)了。第一項信息指明我們定義的是一個 commit 對象以及它所在的分支,隨后是我們生成的標記,提交者信息以及提交備注,然后是前一個 commit 的索引,如果有的話。代碼大致這樣:

打印導(dǎo)入所需的信息

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

時區(qū)(-0700)處于簡化目的使用硬編碼。如果是從其他版本控制系統(tǒng)導(dǎo)入,則必須以變量的形式指明時區(qū)。 提交備注必須以特定格式給出:

`data (size)\n(contents)`

該格式包含了單詞 data,所讀取數(shù)據(jù)的大小,一個換行符,最后是數(shù)據(jù)本身。由于隨后指明文件內(nèi)容的時候要用到相同的格式,我們寫一個輔助方法,export_data:

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

唯一剩下的就是每一個快照的內(nèi)容了。這簡單的很,因為它們分別處于一個目錄——你可以輸出 deleeall 命令,隨后是目錄中每個文件的內(nèi)容。Git 會正確的記錄每一個快照:

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

注意:由于很多系統(tǒng)把每次修訂看作一個 commit 到另一個 commit 的變化量,`fast-import` 也可以依據(jù)每次提交獲取一個命令來指出哪些文件被添加,刪除或者修改過,以及修改的內(nèi)容。我們將需要計算快照之間的差別并且僅僅給出這項數(shù)據(jù),不過該做法要復(fù)雜很多——還如不直接把所有數(shù)據(jù)丟給 Git 然它自己搞清楚。假如前面這個方法更適用于你的數(shù)據(jù),參考 fast-import 的 man 幫助頁面來了解如何以這種方式提供數(shù)據(jù)。

列舉新文件內(nèi)容或者指明帶有新內(nèi)容的已修改文件的格式如下:

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

這里,644 是權(quán)限模式(加入有可執(zhí)行文件,則需要探測之并設(shè)定為 755),而 inline 說明我們在本行結(jié)束之后立即列出文件的內(nèi)容。我們的 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,因為這里和指明提交注釋的格式如出一轍。

最后一項工作是返回當前的標記以便下次循環(huán)的使用。

## return mark
注意:如果你在用 Windows,一定記得添加一項額外的步驟。前面提過,Windows 使用` CRLF` 作為換行字符而` git fast-import` 只接受 LF。為了繞開這個問題來滿足` git fast-import,`你需要讓 ruby 用 LF 取代 CRLF:

`$stdout.binmode`

搞定了?,F(xiàn)在運行該腳本,你將得到如下內(nèi)容:

$ 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
(...)

要運行導(dǎo)入腳本,在需要導(dǎo)入的目錄把該內(nèi)容用管道定向到 `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

你會發(fā)現(xiàn),在它成功執(zhí)行完畢以后,會給出一堆有關(guān)已完成工作的數(shù)據(jù)。上例在一個分支導(dǎo)入了5次提交數(shù)據(jù),包含了18個對象?,F(xiàn)在可以運行 `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 倉庫。需要注意的是此時沒有任何內(nèi)容被檢出——剛開始當前目錄里沒有任何文件。要獲取它們,你得轉(zhuǎn)到 master 分支的所在:

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


`fast-import` 還可以做更多——處理不同的文件模式,二進制文件,多重分支與合并,標簽,進展標識等等。一些更加復(fù)雜的實例可以在 Git 源碼的 contib/fast-import 目錄里找到;其中較為出眾的是前面提過的 git-p4 腳本。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號