如果在其他版本控制系統中儲存了某項目的代碼而後決定轉而使用 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 libfast-import
還可以做更多——處理不同的檔案模式,二進位檔案,多重分支與合并,標籤,進展標識等等。一些更加複雜的執行個體可以在 Git 源碼的 contib/fast-import 目錄裡找到;其中較為出眾的是前面提過的 git-p4 指令碼。 |