遷移到 Git

2018-07-10 17:31 更新

遷移到 Git

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

導(dǎo)入

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

Subversion

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

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

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

通過(guò)該命令可以獲得 SVN 作者的列表:

$ svn log ^/ --xml | grep -P "^<author" | sort -u | \
      perl -pe 's/<author>(.*?)<\/author>/$1 = /' > users.txt

它將輸出 XML 格式的日志——你可以找到作者,建立一個(gè)單獨(dú)的列表,然后從 XML 中抽取出需要的信息。(顯而易見(jiàn),本方法要求主機(jī)上安裝了grepsort 和 perl.)然后把輸出重定向到 user.txt 文件,然后就可以在每一項(xiàng)的后面添加相應(yīng)的 Git 用戶數(shù)據(jù)。

為 git svn 提供該文件可以然它更精確的映射作者數(shù)據(jù)。你還可以在 clone 或者 init后面添加 --no-metadata 來(lái)阻止 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)該比原來(lái)整潔多了。原來(lái)的 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

不僅作者一項(xiàng)干凈了不少,git-svn-id 也就此消失了。

你還需要一點(diǎn) post-import(導(dǎo)入后) 清理工作。最起碼的,應(yīng)該清理一下 git svn 創(chuàng)建的那些怪異的索引結(jié)構(gòu)。首先要移動(dòng)標(biāo)簽,把它們從奇怪的遠(yuǎn)程分支變成實(shí)際的標(biāo)簽,然后把剩下的分支移動(dòng)到本地。

要把標(biāo)簽變成合適的 Git 標(biāo)簽,運(yùn)行

$ 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/ 開(kāi)頭的遠(yuǎn)程分支的索引變成真正的(輕巧的)標(biāo)簽。

接下來(lái),把 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 分支,所有的舊標(biāo)簽也變成真正的 Git 標(biāo)簽。最后一項(xiàng)工作就是把新建的 Git 服務(wù)器添加為遠(yuǎn)程服務(wù)器并且向它推送。下面是新增遠(yuǎn)程服務(wù)器的例子:

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

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

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

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

Perforce

你將了解到的下一個(gè)被導(dǎo)入的系統(tǒng)是 Perforce. Git 發(fā)行的時(shí)候同時(shí)也附帶了一個(gè) Perforce 導(dǎo)入腳本,不過(guò)它是包含在源碼的 contrib 部分——而不像 git svn 那樣默認(rèn)可用。運(yùn)行它之前必須獲取 Git 的源碼,可以在 git.kernel.org 下載:

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

在這個(gè) fast-import 目錄下,應(yīng)該有一個(gè)叫做 git-p4 的 Python 可執(zhí)行腳本。主機(jī)上必須裝有 Python 和 p4 工具該導(dǎo)入才能正常進(jìn)行。例如,你要從 Perforce 公共代碼倉(cāng)庫(kù)(譯注: Perforce Public Depot,Perforce 官方提供的代碼寄存服務(wù))導(dǎo)入 Jam 工程。為了設(shè)定客戶端,我們要把 P4PORT 環(huán)境變量 export 到 Perforce 倉(cāng)庫(kù):

$ export P4PORT=public.perforce.com:1666

運(yùn)行 git-p4 clone 命令將從 Perforce 服務(wù)器導(dǎo)入 Jam 項(xiàng)目,我們需要給出倉(cāng)庫(kù)和項(xiàng)目的路徑以及導(dǎo)入的目標(biā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 目錄運(yùn)行一下 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 = "http://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 = "http://public/jam/src/": change = 3108]

每一個(gè) commit 里都有一個(gè) git-p4 標(biāo)識(shí)符。這個(gè)標(biāo)識(shí)符可以保留,以防以后需要引用 Perforce 的修改版本號(hào)。然而,如果想刪除這些標(biāo)識(shí)符,現(xiàn)在正是時(shí)候——在開(kāi)啟新倉(cāng)庫(kù)之前??梢酝ㄟ^(guò) git filter-branch 來(lái)批量刪除這些標(biāo)識(shí)符:

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

現(xiàn)在運(yùn)行一下 git log,你會(huì)發(fā)現(xiàn)這些 commit 的 SHA-1 校驗(yàn)值都發(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)完成,可以開(kāi)始向新的 Git 服務(wù)器推送了。

自定導(dǎo)入腳本

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

下面演示一下如何編寫(xiě)一個(gè)簡(jiǎn)單的導(dǎo)入腳本。假設(shè)你在進(jìn)行一項(xiàng)工作,并且按時(shí)通過(guò)把工作目錄復(fù)制為以時(shí)間戳 back_YY_MM_DD 命名的目錄來(lái)進(jìn)行備份,現(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)入到一個(gè) Git 目錄,我們首先回顧一下 Git 儲(chǔ)存數(shù)據(jù)的方式。你可能還記得,Git 本質(zhì)上是一個(gè) commit 對(duì)象的鏈表,每一個(gè)對(duì)象指向一個(gè)內(nèi)容的快照。而這里需要做的工作就是告訴 fast-import 內(nèi)容快照的位置,什么樣的 commit 數(shù)據(jù)指向它們,以及它們的順序。我們采取一次處理一個(gè)快照的策略,為每一個(gè)內(nèi)容目錄建立對(duì)應(yīng)的 commit ,每一個(gè) commit 與之前的建立鏈接。

正如在第七章 "Git 執(zhí)行策略一例" 一節(jié)中一樣,我們將使用 Ruby 來(lái)編寫(xiě)這個(gè)腳本,因?yàn)樗俏胰粘J褂玫恼Z(yǔ)言而且閱讀起來(lái)簡(jiǎn)單一些。你可以用任何其他熟悉的語(yǔ)言來(lái)重寫(xiě)這個(gè)例子——它僅需要把必要的信息打印到標(biāo)準(zhǔn)輸出而已。同時(shí),如果你在使用 Windows,這意味著你要特別留意不要在換行的時(shí)候引入回車(chē)符(譯注:carriage returns,Windows 換行時(shí)加入的符號(hào),通常說(shuō)的 \r )—— Git 的 fast-import 對(duì)僅使用換行符(LF)而非 Windows 的回車(chē)符(CRLF)要求非常嚴(yán)格。

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

last_mark = nil

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

    # 進(jìn)入目標(biāo)目錄
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

我們?cè)诿恳粋€(gè)目錄里運(yùn)行 print_export ,它會(huì)取出上一個(gè)快照的索引和標(biāo)記并返回本次快照的索引和標(biāo)記;由此我們就可以正確的把二者連接起來(lái)。"標(biāo)記(mark)" 是 fast-import 中對(duì) commit 標(biāo)識(shí)符的叫法;在創(chuàng)建 commit 的同時(shí),我們逐一賦予一個(gè)標(biāo)記以便以后在把它連接到其他 commit 時(shí)使用。因此,在 print_export 方法中要做的第一件事就是根據(jù)目錄名生成一個(gè)標(biāo)記:

mark = convert_dir_to_mark(dir)

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

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

有了整數(shù)來(lái)代表每個(gè) commit,我們現(xiàn)在需要提交附加信息中的日期。由于日期是用目錄名表示的,我們就從中解析出來(lái)。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

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

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

我們差不多可以開(kāi)始為導(dǎo)入腳本輸出提交數(shù)據(jù)了。第一項(xiàng)信息指明我們定義的是一個(gè) commit 對(duì)象以及它所在的分支,隨后是我們生成的標(biāo)記,提交者信息以及提交備注,然后是前一個(gè) 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

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

data (size)\n(contents)

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

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

唯一剩下的就是每一個(gè)快照的內(nèi)容了。這簡(jiǎn)單的很,因?yàn)樗鼈兎謩e處于一個(gè)目錄——你可以輸出 deleeall 命令,隨后是目錄中每個(gè)文件的內(nèi)容。Git 會(huì)正確的記錄每一個(gè)快照:

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

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

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

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

這里,644 是權(quán)限模式(加入有可執(zhí)行文件,則需要探測(cè)之并設(shè)定為 755),而 inline 說(shuō)明我們?cè)诒拘薪Y(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

我們重用了前面定義過(guò)的 export_data,因?yàn)檫@里和指明提交注釋的格式如出一轍。

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

return mark

注意:如果你在用 Windows,一定記得添加一項(xiàng)額外的步驟。前面提過(guò),Windows 使用 CRLF 作為換行字符而 Git fast-import 只接受 LF。為了繞開(kāi)這個(gè)問(wèn)題來(lái)滿足 git fast-import,你需要讓 ruby 用 LF 取代 CRLF:

$stdout.binmode

搞定了。現(xiàn)在運(yù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
(...)

要運(yùn)行導(dǎo)入腳本,在需要導(dǎo)入的目錄把該內(nèi)容用管道定向到 git fast-import。你可以建立一個(gè)空目錄然后運(yùn)行 git init 作為開(kāi)頭,然后運(yùn)行該腳本:

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

你會(huì)發(fā)現(xiàn),在它成功執(zhí)行完畢以后,會(huì)給出一堆有關(guān)已完成工作的數(shù)據(jù)。上例在一個(gè)分支導(dǎo)入了5次提交數(shù)據(jù),包含了18個(gè)對(duì)象?,F(xiàn)在可以運(yùn)行 git log 來(lái)檢視新的歷史:

$ 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

就它了——一個(gè)干凈整潔的 Git 倉(cāng)庫(kù)。需要注意的是此時(shí)沒(méi)有任何內(nèi)容被檢出——?jiǎng)傞_(kāi)始當(dāng)前目錄里沒(méi)有任何文件。要獲取它們,你得轉(zhuǎn)到 master 分支的所在:

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

fast-import 還可以做更多——處理不同的文件模式,二進(jìn)制文件,多重分支與合并,標(biāo)簽,進(jìn)展標(biāo)識(shí)等等。一些更加復(fù)雜的實(shí)例可以在 Git 源碼的 contib/fast-import 目錄里找到;其中較為出眾的是前面提過(guò)的 git-p4 腳本。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)