-
A1. 附錄 A: Git 在其他環境
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小結
-
A2. 附錄 B: 在應用程式中嵌入 Git
-
A3. 附錄 C: Git 命令
9.1 Git 與其他系統 - Git 客戶端
世界並非完美。通常情況下,你無法立即將接觸到的所有專案都切換到 Git。有時你會被困在使用了其他 VCS 的專案中,並希望它是 Git。本章的第一部分將介紹當您工作的專案託管在不同系統中時,如何將 Git 用作客戶端。
在某些時候,你可能希望將現有專案轉換為 Git。本章的第二部分將介紹如何將專案從幾個特定系統遷移到 Git,以及在沒有預構建匯入工具時也能使用的方法。
Git 客戶端
Git 為開發者提供瞭如此出色的體驗,以至於許多人已經想出如何在自己的工作站上使用它,即使團隊的其他人正在使用完全不同的 VCS。有許多這樣的介面卡,被稱為“橋接器”,可用。在這裡,我們將介紹你最可能在實際中遇到的那些。
Git 與 Subversion
很大一部分開源開發專案和相當多的企業專案使用 Subversion 來管理其原始碼。它已經存在了十多年,在這段時間的大部分時間裡,它都是開源專案的事實上的 VCS 選擇。它在許多方面也與 CVS 非常相似,而 CVS 在此之前是原始碼控制領域的巨頭。
Git 的一個強大功能是與 Subversion 的雙向橋接,稱為 git svn
。這個工具允許你將 Git 作為 Subversion 伺服器的有效客戶端,這樣你就可以使用 Git 的所有本地功能,然後像在本地使用 Subversion 一樣推送到 Subversion 伺服器。這意味著你可以進行本地分支和合並、使用暫存區、使用變基和摘櫻桃(cherry-picking)等等,而你的協作者則繼續以他們陳舊的方式工作。這是將 Git 引入企業環境,並幫助你的開發夥伴提高效率的好方法,同時你也可以爭取將基礎設施更改為完全支援 Git。Subversion 橋接器是通往 DVCS 世界的入門藥。
git svn
在 Git 中,所有 Subversion 橋接命令的基礎命令是 git svn
。它包含相當多的命令,所以我們將通過幾個簡單的工作流程來展示最常見的命令。
需要注意的是,當你使用 git svn
時,你正在與 Subversion 互動,這是一個與 Git 工作方式截然不同的系統。儘管你可以進行本地分支和合並,但通常最好透過變基你的工作來保持歷史記錄儘可能線性,並避免同時與 Git 遠端倉庫互動等操作。
不要重寫你的歷史記錄並嘗試再次推送,也不要同時推送到並行的 Git 倉庫與 Git 開發者協作。Subversion 只能有一個線性的歷史記錄,很容易混淆它。如果你正在與一個團隊合作,並且有些人使用 SVN,另一些人使用 Git,請確保每個人都使用 SVN 伺服器進行協作——這樣做會使你的生活更輕鬆。
設定
為了演示此功能,你需要一個你具有寫入許可權的典型 SVN 倉庫。如果你想複製這些示例,你必須建立一個可寫入的 SVN 測試倉庫副本。為了方便做到這一點,你可以使用 Subversion 附帶的一個名為 svnsync
的工具。
要繼續操作,你首先需要建立一個新的本地 Subversion 倉庫
$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn
然後,允許所有使用者更改 revprops —— 最簡單的方法是新增一個始終退出 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
現在,你可以透過使用目標和來源倉庫呼叫 svnsync init
來將此專案同步到本地機器。
$ svnsync init file:///tmp/test-svn \
http://your-svn-server.example.org/svn/
這會設定運行同步的屬性。然後你可以透過執行以下命令克隆程式碼
$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Transmitting file data .............................[...]
Committed revision 2.
Copied properties for revision 2.
[…]
儘管此操作可能只需幾分鐘,但如果你嘗試將原始倉庫複製到另一個遠端倉庫而不是本地倉庫,即使只有不到 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 /private/tmp/progit/test-svn/.git/
r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk)
A m4/acx_pthread.m4
A m4/stl_hash.m4
A java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java
A java/src/test/java/com/google/protobuf/WireFormatTest.java
…
r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk)
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/my-calc-branch, 75
Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae
Following parent with do_switch
Successfully followed parent
r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch)
Checked out HEAD:
file:///tmp/test-svn/trunk r75
這相當於在你提供的 URL 上運行了兩個命令——先是 git svn init
,然後是 git svn fetch
。這可能需要一段時間。例如,如果測試專案只有大約 75 次提交,並且程式碼庫不大,Git 仍然必須逐個檢出每個版本,並單獨提交。對於有數百或數千次提交的專案,這可能需要數小時甚至數天才能完成。
-T trunk -b branches -t tags
部分告訴 Git 這個 Subversion 倉庫遵循基本的拉分支和標籤約定。如果你將主幹、分支或標籤命名不同,可以更改這些選項。由於這非常常見,你可以用 -s
替換整個部分,表示標準佈局幷包含所有這些選項。以下命令是等效的
$ git svn clone file:///tmp/test-svn -s
此時,你應該有一個有效的 Git 倉庫,其中已經匯入了你的分支和標籤
$ git branch -a
* master
remotes/origin/my-calc-branch
remotes/origin/tags#2.0.2
remotes/origin/tags/release-2.0.1
remotes/origin/tags/release-2.0.2
remotes/origin/tags/release-2.0.2rc1
remotes/origin/trunk
請注意這個工具如何將 Subversion 標籤作為遠端引用來管理。讓我們用 Git 內部命令 show-ref
仔細看看
$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags#2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk
當從 Git 伺服器克隆時,Git 不會這樣做;下面是一個剛克隆後帶有標籤的倉庫的樣子
$ git show-ref
c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master
32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1
75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2
23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0
7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0
6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0
Git 將標籤直接獲取到 refs/tags
中,而不是將其視為遠端分支。
提交回 Subversion
現在你有一個工作目錄,你可以在專案上做一些工作,並使用 Git 有效地作為 SVN 客戶端將你的提交推回上游。如果你編輯其中一個檔案並提交,你將有一個本地存在於 Git 中但 Subversion 伺服器上不存在的提交
$ git commit -am 'Adding git-svn instructions to the README'
[master 4af61fd] Adding git-svn instructions to the README
1 file changed, 5 insertions(+)
接下來,你需要將你的更改推送到上游。請注意這如何改變你與 Subversion 的工作方式——你可以離線進行多次提交,然後一次性將它們全部推送到 Subversion 伺服器。要推送到 Subversion 伺服器,你執行 git svn dcommit
命令
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r77
M README.txt
r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk)
No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk
這會將你在 Subversion 伺服器程式碼之上進行的所有提交,為每個提交執行一次 Subversion 提交,然後重寫你的本地 Git 提交以包含一個唯一的識別符號。這很重要,因為它意味著你提交的所有 SHA-1 校驗和都會改變。部分由於這個原因,不建議同時使用基於 Git 的專案遠端版本與 Subversion 伺服器。
$ git log -1
commit 95e0222ba6399739834380eb10afcd73e0670bc5
Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68>
Date: Thu Jul 24 03:08:36 2014 +0000
Adding git-svn instructions to the README
git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68
注意,最初提交時以 4af61fd
開頭的 SHA-1 校驗和現在以 95e0222
開頭。如果你想同時推送到 Git 伺服器和 Subversion 伺服器,你必須首先推送到(dcommit
)Subversion 伺服器,因為該操作會改變你的提交資料。
拉取新更改
如果你正在與其他開發者合作,那麼在某個時候,其中一個人會推送,然後另一個人會嘗試推送一個衝突的更改。該更改將被拒絕,直到你合併了他們的工作。在 git svn
中,它看起來是這樣的
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M README.txt
Current branch master is up to date.
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.
為了解決這種情況,你可以執行 git svn rebase
,它會從伺服器拉取你還沒有的任何更改,並將你所做的工作變基到伺服器上的內容之上
$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...
ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.
現在,你所有的工作都在 Subversion 伺服器上的內容之上,所以你可以成功地 dcommit
了
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r85
M README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk
請注意,與 Git 不同的是,Git 要求你在推送之前合併你本地還沒有的上游工作,而 git svn
只有在更改衝突時才要求你這樣做(這與 Subversion 的工作方式非常相似)。如果另一個人將更改推送到一個檔案,然後你將更改推送到另一個檔案,你的 dcommit
將正常工作
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M configure.ac
Committed r87
M autogen.sh
r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk)
M configure.ac
r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk)
W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M autogen.sh
First, rewinding head to replay your work on top of it...
這一點很重要,因為結果是一個在你推送時,兩臺電腦上都不存在的專案狀態。如果更改不相容但沒有衝突,你可能會遇到難以診斷的問題。這與使用 Git 伺服器不同——在 Git 中,你可以在釋出之前在客戶端系統上完全測試狀態,而在 SVN 中,你永遠無法確定提交前和提交後的狀態是否完全相同。
即使你還沒有準備好自己提交,你也應該執行此命令以從 Subversion 伺服器拉取更改。你可以執行 git svn fetch
來獲取新資料,但 git svn rebase
會執行 fetch 然後更新你的本地提交。
$ git svn rebase
M autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.
時不時執行 git svn rebase
可以確保你的程式碼始終是最新的。但是,當你執行此命令時,你需要確保你的工作目錄是乾淨的。如果你有本地更改,你必須在執行 git svn rebase
之前暫存你的工作或暫時提交它——否則,如果 rebase 會導致合併衝突,該命令將停止。
Git 分支問題
當你熟悉了 Git 工作流後,你可能會建立主題分支,在上面進行工作,然後將它們合併進來。如果你透過 git svn
推送到 Subversion 伺服器,你可能希望每次都將你的工作變基到單個分支上,而不是將分支合併在一起。偏愛變基的原因是 Subversion 有線性的歷史記錄,並且不像 Git 那樣處理合併,所以 git svn
在將快照轉換為 Subversion 提交時只跟隨第一個父級。
假設你的歷史記錄如下:你建立了一個 experiment
分支,進行了兩次提交,然後將它們合併回 master
。當你 dcommit
時,你看到這樣的輸出
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M CHANGES.txt
Committed r89
M CHANGES.txt
r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk)
M COPYING.txt
M INSTALL.txt
Committed r90
M INSTALL.txt
M COPYING.txt
r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk)
No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk
在具有合併歷史記錄的分支上執行 dcommit
可以正常工作,但是當你檢視 Git 專案歷史記錄時,它並沒有重寫你在 experiment
分支上進行的任何提交——相反,所有這些更改都出現在單個合併提交的 SVN 版本中。
當其他人克隆這份工作時,他們看到的只是包含所有被壓縮工作內容的合併提交,就好像你運行了 git merge --squash
一樣;他們看不到關於它來自何處或何時提交的提交資料。
Subversion 分支
Subversion 中的分支與 Git 中的分支不同;如果能儘量避免使用它,那可能是最好的。但是,你可以使用 git svn
在 Subversion 中建立分支並提交到分支。
建立一個新的 SVN 分支
要在 Subversion 中建立一個新分支,你執行 git svn branch [new-branch]
$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/opera, 90
Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0
Following parent with do_switch
Successfully followed parent
r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)
這等同於 Subversion 中的 svn copy trunk branches/opera
命令,並且在 Subversion 伺服器上操作。重要的是,它不會將你檢出到那個分支;如果你此時提交,該提交將進入伺服器上的 trunk
,而不是 opera
。
切換活動分支
Git 透過在你的歷史記錄中查詢你的任何 Subversion 分支的尖端來確定你的 dcommit 將去哪個分支——你應該只有一個,並且它應該是你當前分支歷史記錄中帶有 git-svn-id
的最後一個。
如果你想同時在多個分支上工作,你可以透過在匯入的 Subversion 提交上啟動它們來設定本地分支以 dcommit
到特定的 Subversion 分支。如果你想要一個可以單獨工作的 opera
分支,你可以執行
$ git branch opera remotes/origin/opera
現在,如果你想將你的 opera
分支合併到 trunk
(你的 master
分支)中,你可以使用正常的 git merge
來完成。但是你需要提供一個描述性的提交資訊(透過 -m
),否則合併資訊將是“Merge branch opera”而不是有用的內容。
請記住,儘管你正在使用 git merge
執行此操作,並且合併可能比在 Subversion 中容易得多(因為 Git 會自動為你檢測合適的合併基礎),但這並不是一個正常的 Git 合併提交。你必須將這些資料推送回一個無法處理跟蹤多個父級的提交的 Subversion 伺服器;因此,在你推送之後,它將看起來像一個將另一個分支的所有工作壓縮到單個提交中的提交。在你將一個分支合併到另一個分支後,你不能輕易地返回並繼續在該分支上工作,就像你通常在 Git 中那樣。你執行的 dcommit
命令會擦除任何表示合併了哪個分支的資訊,因此隨後的合併基礎計算將是錯誤的——dcommit
會使你的 git merge
結果看起來像是你運行了 git merge --squash
。不幸的是,沒有好的方法可以避免這種情況——Subversion 無法儲存此資訊,因此在使用它作為伺服器時,你將始終受其限制。為了避免問題,在將其合併到主幹後,你應該刪除本地分支(在本例中為 opera
)。
Subversion 命令
git svn
工具集提供了許多命令來幫助簡化向 Git 的過渡,提供了與 Subversion 中類似的一些功能。以下是一些為你提供 Subversion 曾經擁有的功能的命令。
SVN 風格歷史
如果你習慣了 Subversion 並想以 SVN 輸出風格檢視你的歷史記錄,你可以執行 git svn log
來檢視 SVN 格式的提交歷史記錄
$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines
autogen change
------------------------------------------------------------------------
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines
Merge branch 'experiment'
------------------------------------------------------------------------
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines
updated the changelog
關於 git svn log
,有兩件重要的事情你應該知道。首先,它離線工作,不像真正的 svn log
命令,後者會向 Subversion 伺服器請求資料。其次,它只顯示你已提交到 Subversion 伺服器的提交。你尚未 dcommit 的本地 Git 提交不會顯示;同時其他人對 Subversion 伺服器進行的提交也不會顯示。它更像是 Subversion 伺服器上提交的最後已知狀態。
SVN 註解
就像 git svn log
命令離線模擬 svn log
命令一樣,你可以透過執行 git svn blame [FILE]
獲得 svn annotate
的等效功能。輸出如下所示
$ git svn blame README.txt
2 temporal Protocol Buffers - Google's data interchange format
2 temporal Copyright 2008 Google Inc.
2 temporal http://code.google.com/apis/protocolbuffers/
2 temporal
22 temporal C++ Installation - Unix
22 temporal =======================
2 temporal
79 schacon Committing 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 the following:
2 temporal
同樣,它不顯示你在本地 Git 中進行的提交或同時推送到 Subversion 的提交。
SVN 伺服器資訊
你也可以透過執行 git svn 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 May 2009)
這與 blame
和 log
類似,它離線執行,並且僅在你上次與 Subversion 伺服器通訊時保持最新。
忽略 Subversion 忽略的內容
如果你克隆一個在任何地方設定了 svn:ignore
屬性的 Subversion 倉庫,你很可能希望設定相應的 .gitignore
檔案,以免意外提交不應提交的檔案。git svn
有兩個命令可以幫助解決這個問題。第一個是 git svn create-ignore
,它會自動為你建立相應的 .gitignore
檔案,以便你的下一次提交可以包含它們。
第二個命令是 git svn show-ignore
,它將你需要放入 .gitignore
檔案中的行列印到標準輸出,這樣你就可以將輸出重定向到你的專案排除檔案中
$ git svn show-ignore > .git/info/exclude
這樣,你就不會在專案中散佈 .gitignore
檔案。如果你是 Subversion 團隊中唯一的 Git 使用者,並且你的隊友不希望專案中有 .gitignore
檔案,這是一個不錯的選擇。
Git-Svn 總結
如果你的開發環境必須使用 Subversion 伺服器,或者你被困在這樣的環境中,那麼 git svn
工具就很有用。但是,你應該將其視為受限的 Git,否則你可能會遇到轉換問題,讓你和你的協作者感到困惑。為了避免麻煩,請嘗試遵循以下準則
-
保持線性的 Git 歷史記錄,不包含由
git merge
建立的合併提交。將你在主線分支之外進行的任何工作變基回主線分支;不要將其合併進去。 -
不要設定並協作使用單獨的 Git 伺服器。可以有一個來加快新開發人員的克隆速度,但不要將任何沒有
git-svn-id
條目的內容推送到其中。你甚至可能想新增一個pre-receive
鉤子,檢查每個提交訊息中是否有git-svn-id
,並拒絕包含沒有此資訊的提交的推送。
如果你遵循這些準則,與 Subversion 伺服器一起工作會更容易忍受。但是,如果可以遷移到真正的 Git 伺服器,這樣做可以為你的團隊帶來更多收益。
Git 與 Mercurial
分散式版本控制系統(DVCS)的領域不僅僅只有 Git。實際上,這個領域中還有許多其他系統,每個系統都有自己正確進行分散式版本控制的角度。
好訊息是,如果你偏愛 Git 的客戶端行為,但正在處理一個原始碼由 Mercurial 控制的專案,那麼有一種方法可以將 Git 用作 Mercurial 託管倉庫的客戶端。由於 Git 與伺服器倉庫的通訊是透過遠端進行的,因此這種橋接以遠端 helper 的形式實現也就不足為奇了。該專案的名稱是 git-remote-hg,可以在 https://github.com/felipec/git-remote-hg 找到。
git-remote-hg
首先,你需要安裝 git-remote-hg。這基本上就是將它的檔案放在你的路徑中的某個位置,像這樣
$ curl -o ~/bin/git-remote-hg \
https://raw.githubusercontent.com/felipec/git-remote-hg/master/git-remote-hg
$ chmod +x ~/bin/git-remote-hg
...假設 ~/bin
在你的 $PATH
中。Git-remote-hg 還有一個依賴項:Python 的 mercurial
庫。如果你已經安裝了 Python,這就像這樣簡單
$ pip install mercurial
如果你沒有安裝 Python,請訪問 https://python.club.tw/ 並首先安裝它。
最後你需要的是 Mercurial 客戶端。如果你還沒有安裝,請訪問 https://www.mercurial-scm.org/ 並安裝它。
現在你準備好了。你只需要一個可以推送的 Mercurial 倉庫。幸運的是,每個 Mercurial 倉庫都可以這樣操作,所以我們將使用每個人都用來學習 Mercurial 的“hello world”倉庫
$ hg clone http://selenic.com/repo/hello /tmp/hello
開始
現在我們有了一個合適的“服務端”倉庫,我們可以經歷一個典型的工作流程。正如你將看到的,這兩個系統足夠相似,以至於摩擦很小。
一如既往,首先我們克隆 Git
$ git clone hg::/tmp/hello /tmp/hello-git
$ cd /tmp/hello-git
$ git log --oneline --graph --decorate
* ac7955c (HEAD, origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master, master) Create a makefile
* 65bb417 Create a standard 'hello, world' program
你會注意到,使用 Mercurial 倉庫時使用標準的 git clone
命令。這是因為 git-remote-hg 工作在相當低的級別,使用類似於 Git 的 HTTP/S 協議實現(遠端助手)的機制。由於 Git 和 Mercurial 都設計為每個客戶端都擁有完整的倉庫歷史副本,因此此命令會進行完整克隆,包括專案的所有歷史記錄,並且速度相當快。
log
命令顯示了兩次提交,其中最新的提交被一堆引用指向。事實證明,其中一些引用實際上並不存在。讓我們看看 .git
目錄中實際有什麼
$ tree .git/refs
.git/refs
├── heads
│ └── master
├── hg
│ └── origin
│ ├── bookmarks
│ │ └── master
│ └── branches
│ └── default
├── notes
│ └── hg
├── remotes
│ └── origin
│ └── HEAD
└── tags
9 directories, 5 files
Git-remote-hg 試圖讓事情更符合 Git 慣例,但在底層它管理著兩個略有不同的系統之間的概念對映。refs/hg
目錄是實際遠端引用儲存的地方。例如,refs/hg/origin/branches/default
是一個 Git 引用檔案,其中包含以“ac7955c”開頭的 SHA-1,這是 master
指向的提交。所以 refs/hg
目錄有點像一個偽造的 refs/remotes/origin
,但它在書籤和分支之間增加了區分。
notes/hg
檔案是 git-remote-hg 如何將 Git 提交雜湊對映到 Mercurial 變更集 ID 的起點。讓我們探索一下
$ cat notes/hg
d4c10386...
$ git cat-file -p d4c10386...
tree 1781c96...
author remote-hg <> 1408066400 -0800
committer remote-hg <> 1408066400 -0800
Notes for master
$ git ls-tree 1781c96...
100644 blob ac9117f... 65bb417...
100644 blob 485e178... ac7955c...
$ git cat-file -p ac9117f
0a04b987be5ae354b710cefeba0e2d9de7ad41a9
所以 refs/notes/hg
指向一個樹,在 Git 物件資料庫中,它是一個包含名稱的其他物件列表。git ls-tree
輸出樹中專案的模式、型別、物件雜湊和檔名。一旦我們深入到其中一個樹專案,我們發現它裡面有一個名為“ac9117f”(master
指向的提交的 SHA-1 雜湊)的 blob,其內容是“0a04b98”(即 default
分支尖端 Mercurial 變更集的 ID)。
好訊息是,我們大部分時間都不必擔心這些。典型的工作流程與使用 Git 遠端倉庫並沒有太大不同。
在我們繼續之前,還有一件事需要處理:忽略檔案。Mercurial 和 Git 在這方面使用了非常相似的機制,但你可能不希望實際將 .gitignore
檔案提交到 Mercurial 倉庫中。幸運的是,Git 有一種在磁碟倉庫本地忽略檔案的方法,並且 Mercurial 格式與 Git 相容,所以你只需要將其複製過來即可
$ cp .hgignore .git/info/exclude
.git/info/exclude
檔案就像 .gitignore
一樣,但不包含在提交中。
工作流程
假設我們已經在 master
分支上完成了一些工作並進行了一些提交,並且你已準備好將其推送到遠端倉庫。這是我們當前倉庫的樣子
$ git log --oneline --graph --decorate
* ba04a2a (HEAD, master) Update makefile
* d25d16f Goodbye
* ac7955c (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Create a makefile
* 65bb417 Create a standard 'hello, world' program
我們的 master
分支比 origin/master
超前兩次提交,但這兩次提交只存在於我們的本地機器上。讓我們看看是否有人同時在做重要工作
$ git fetch
From hg::/tmp/hello
ac7955c..df85e87 master -> origin/master
ac7955c..df85e87 branches/default -> origin/branches/default
$ git log --oneline --graph --decorate --all
* 7b07969 (refs/notes/hg) Notes for default
* d4c1038 Notes for master
* df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
| * ba04a2a (HEAD, master) Update makefile
| * d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard 'hello, world' program
由於我們使用了 --all
標誌,我們看到了 git-remote-hg 內部使用的“notes”引用,但我們可以忽略它們。其餘的正如我們所期望的;origin/master
已經前進了一次提交,我們的歷史記錄現在已經分叉。與本章中我們使用的其他系統不同,Mercurial 能夠處理合併,所以我們不會做任何花哨的事情。
$ git merge origin/master
Auto-merging hello.c
Merge made by the 'recursive' strategy.
hello.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --decorate
* 0c64627 (HEAD, master) Merge remote-tracking branch 'origin/master'
|\
| * df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
* | ba04a2a Update makefile
* | d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard 'hello, world' program
完美。我們運行了測試,一切透過,所以我們準備與團隊其他人分享我們的工作
$ git push
To hg::/tmp/hello
df85e87..0c64627 master -> master
就這樣!如果你檢視 Mercurial 倉庫,你會發現這確實達到了我們的預期
$ hg log -G --style compact
o 5[tip]:4,2 dc8fa4f932b8 2014-08-14 19:33 -0700 ben
|\ Merge remote-tracking branch 'origin/master'
| |
| o 4 64f27bcefc35 2014-08-14 19:27 -0700 ben
| | Update makefile
| |
| o 3:1 4256fc29598f 2014-08-14 19:27 -0700 ben
| | Goodbye
| |
@ | 2 7db0b4848b3c 2014-08-14 19:30 -0700 ben
|/ Add some documentation
|
o 1 82e55d328c8c 2005-08-26 01:21 -0700 mpm
| Create a makefile
|
o 0 0a04b987be5a 2005-08-26 01:20 -0700 mpm
Create a standard 'hello, world' program
編號為 2 的變更集是由 Mercurial 建立的,而編號為 3 和 4 的變更集是由 git-remote-hg 透過推送 Git 提交建立的。
分支和書籤
Git 只有一種分支:一個當提交發生時會移動的引用。在 Mercurial 中,這種引用被稱為“書籤”,它的行為方式與 Git 分支非常相似。
Mercurial 的“分支”概念更為“重量級”。變更集所屬的分支是與變更集一起記錄的,這意味著它將始終存在於倉庫歷史記錄中。這是一個在 develop
分支上進行的提交示例
$ hg log -l 1
changeset: 6:8f65e5e02793
branch: develop
tag: tip
user: Ben Straub <ben@straub.cc>
date: Thu Aug 14 20:06:38 2014 -0700
summary: More documentation
注意以“branch”開頭的行。Git 無法真正複製這一點(也不需要;兩種型別的分支都可以表示為 Git 引用),但 git-remote-hg 需要理解這種差異,因為 Mercurial 關心它。
建立 Mercurial 書籤就像建立 Git 分支一樣簡單。在 Git 端
$ git checkout -b featureA
Switched to a new branch 'featureA'
$ git push origin featureA
To hg::/tmp/hello
* [new branch] featureA -> featureA
就是這樣。在 Mercurial 端,它看起來是這樣的
$ hg bookmarks
featureA 5:bd5ac26f11f9
$ hg log --style compact -G
@ 6[tip] 8f65e5e02793 2014-08-14 20:06 -0700 ben
| More documentation
|
o 5[featureA]:4,2 bd5ac26f11f9 2014-08-14 20:02 -0700 ben
|\ Merge remote-tracking branch 'origin/master'
| |
| o 4 0434aaa6b91f 2014-08-14 20:01 -0700 ben
| | update makefile
| |
| o 3:1 318914536c86 2014-08-14 20:00 -0700 ben
| | goodbye
| |
o | 2 f098c7f45c4f 2014-08-14 20:01 -0700 ben
|/ Add some documentation
|
o 1 82e55d328c8c 2005-08-26 01:21 -0700 mpm
| Create a makefile
|
o 0 0a04b987be5a 2005-08-26 01:20 -0700 mpm
Create a standard 'hello, world' program
請注意修訂版本 5 上的新 [featureA]
標籤。這些標籤在 Git 端的作用與 Git 分支完全相同,只有一個例外:你不能從 Git 端刪除書籤(這是遠端 helper 的限制)。
你也可以在一個“重量級”Mercurial 分支上工作:只需在 branches
名稱空間中放置一個分支即可
$ git checkout -b branches/permanent
Switched to a new branch 'branches/permanent'
$ vi Makefile
$ git commit -am 'A permanent change'
$ git push origin branches/permanent
To hg::/tmp/hello
* [new branch] branches/permanent -> branches/permanent
在 Mercurial 端看起來是這樣的
$ hg branches
permanent 7:a4529d07aad4
develop 6:8f65e5e02793
default 5:bd5ac26f11f9 (inactive)
$ hg log -G
o changeset: 7:a4529d07aad4
| branch: permanent
| tag: tip
| parent: 5:bd5ac26f11f9
| user: Ben Straub <ben@straub.cc>
| date: Thu Aug 14 20:21:09 2014 -0700
| summary: A permanent change
|
| @ changeset: 6:8f65e5e02793
|/ branch: develop
| user: Ben Straub <ben@straub.cc>
| date: Thu Aug 14 20:06:38 2014 -0700
| summary: More documentation
|
o changeset: 5:bd5ac26f11f9
|\ bookmark: featureA
| | parent: 4:0434aaa6b91f
| | parent: 2:f098c7f45c4f
| | user: Ben Straub <ben@straub.cc>
| | date: Thu Aug 14 20:02:21 2014 -0700
| | summary: Merge remote-tracking branch 'origin/master'
[...]
分支名稱“permanent”與標記為 7 的變更集一起被記錄。
從 Git 端的角度來看,使用這兩種分支樣式是相同的:只需像往常一樣檢出、提交、獲取、合併、拉取和推送即可。你應該知道的一件事是,Mercurial 不支援重寫歷史記錄,只支援新增歷史記錄。這是我們互動式變基和強制推送後 Mercurial 倉庫的樣子
$ hg log --style compact -G
o 10[tip] 99611176cbc9 2014-08-14 20:21 -0700 ben
| A permanent change
|
o 9 f23e12f939c3 2014-08-14 20:01 -0700 ben
| Add some documentation
|
o 8:1 c16971d33922 2014-08-14 20:00 -0700 ben
| goodbye
|
| o 7:5 a4529d07aad4 2014-08-14 20:21 -0700 ben
| | A permanent change
| |
| | @ 6 8f65e5e02793 2014-08-14 20:06 -0700 ben
| |/ More documentation
| |
| o 5[featureA]:4,2 bd5ac26f11f9 2014-08-14 20:02 -0700 ben
| |\ Merge remote-tracking branch 'origin/master'
| | |
| | o 4 0434aaa6b91f 2014-08-14 20:01 -0700 ben
| | | update makefile
| | |
+---o 3:1 318914536c86 2014-08-14 20:00 -0700 ben
| | goodbye
| |
| o 2 f098c7f45c4f 2014-08-14 20:01 -0700 ben
|/ Add some documentation
|
o 1 82e55d328c8c 2005-08-26 01:21 -0700 mpm
| Create a makefile
|
o 0 0a04b987be5a 2005-08-26 01:20 -0700 mpm
Create a standard "hello, world" program
變更集 8、9 和 10 已經建立並屬於 permanent
分支,但舊的變更集仍然存在。這對於使用 Mercurial 的隊友來說可能會非常令人困惑,所以儘量避免這種情況。
Mercurial 總結
Git 和 Mercurial 足夠相似,跨越邊界工作相當輕鬆。如果你避免更改已經離開你的機器的歷史記錄(通常推薦這樣做),你甚至可能不會意識到另一端是 Mercurial。
Git 與 Perforce
Perforce 在企業環境中是一種非常流行的版本控制系統。它自 1995 年以來一直存在,這使其成為本章中介紹的最早的系統。因此,它在設計時就考慮了當時的限制;它假設你始終連線到單箇中央伺服器,並且本地磁碟上只保留一個版本。誠然,它的功能和限制非常適合某些特定問題,但有許多專案使用 Perforce,而 Git 實際上會工作得更好。
如果你想混合使用 Perforce 和 Git,有兩種選擇。我們將介紹的第一種是 Perforce 製造商提供的“Git Fusion”橋接,它允許你將 Perforce 倉庫的子樹作為讀寫 Git 倉庫暴露。第二種是 git-p4,一個客戶端橋接,它允許你將 Git 用作 Perforce 客戶端,而無需對 Perforce 伺服器進行任何重新配置。
Git Fusion
Perforce 提供了一個名為 Git Fusion 的產品(可在 https://www.perforce.com/manuals/git-fusion/ 獲取),它將 Perforce 伺服器與伺服器端的 Git 倉庫同步。
設定
對於我們的示例,我們將使用 Git Fusion 最簡單的安裝方法,即下載一個執行 Perforce 守護程式和 Git Fusion 的虛擬機器。你可以從 https://www.perforce.com/downloads 獲取虛擬機器映象,下載完成後,將其匯入你喜歡的虛擬化軟體(我們將使用 VirtualBox)。
首次啟動機器時,它會要求你自定義三個 Linux 使用者的密碼(root
、perforce
和 git
),並提供一個例項名稱,該名稱可用於區分此安裝與同一網路上的其他安裝。完成所有這些操作後,你將看到以下內容

你應該記下這裡顯示的 IP 地址,我們稍後會用到它。接下來,我們將建立一個 Perforce 使用者。選擇底部的“Login”選項並按回車(或 SSH 到機器),然後以 root
身份登入。然後使用以下命令建立一個使用者
$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit
第一個會開啟一個 VI 編輯器來定製使用者,但你可以透過輸入 :wq
並按回車來接受預設值。第二個會提示你輸入兩次密碼。我們只需要在 shell 提示符下做這些,所以退出會話。
接下來你需要做的是告訴 Git 不要驗證 SSL 證書。Git Fusion 映象附帶了一個證書,但它是針對一個與你的虛擬機器 IP 地址不匹配的域,所以 Git 會拒絕 HTTPS 連線。如果這是一個永久安裝,請查閱 Perforce Git Fusion 手冊以安裝不同的證書;對於我們的示例目的,這將足夠了
$ export GIT_SSL_NO_VERIFY=true
現在我們可以測試一切是否正常工作了。
$ git clone https://10.0.1.254/Talkhouse
Cloning into 'Talkhouse'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 630, done.
remote: Compressing objects: 100% (581/581), done.
remote: Total 630 (delta 172), reused 0 (delta 0)
Receiving objects: 100% (630/630), 1.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (172/172), done.
Checking connectivity... done.
虛擬機器映象預裝了一個示例專案,你可以克隆它。這裡我們透過 HTTPS 克隆,使用我們上面建立的 john
使用者;Git 會要求連線的憑據,但憑據快取將允許我們跳過後續請求的這一步。
Fusion 配置
安裝 Git Fusion 後,你將希望調整配置。這實際上使用你最喜歡的 Perforce 客戶端很容易做到;只需將 Perforce 伺服器上的 //.git-fusion
目錄對映到你的工作區即可。檔案結構如下所示
$ tree
.
├── objects
│ ├── repos
│ │ └── [...]
│ └── trees
│ └── [...]
│
├── p4gf_config
├── repos
│ └── Talkhouse
│ └── p4gf_config
└── users
└── p4gf_usermap
498 directories, 287 files
objects
目錄供 Git Fusion 內部用於將 Perforce 物件對映到 Git,反之亦然,你無需對其中任何內容進行操作。此目錄中有一個全域性的 p4gf_config
檔案,以及每個倉庫一個檔案——這些是決定 Git Fusion 行為的配置檔案。讓我們看看根目錄中的檔案
[repo-creation]
charset = utf8
[git-to-perforce]
change-owner = author
enable-git-branch-creation = yes
enable-swarm-reviews = yes
enable-git-merge-commits = yes
enable-git-submodules = yes
preflight-commit = none
ignore-author-permissions = no
read-permission-check = none
git-merge-avoidance-after-change-num = 12107
[perforce-to-git]
http-url = none
ssh-url = none
[@features]
imports = False
chunked-push = False
matrix2 = False
parallel-push = False
[authentication]
email-case-sensitivity = no
我們不會在這裡深入探討這些標誌的含義,但請注意,這只是一個 INI 格式的文字檔案,非常類似於 Git 用於配置的檔案。此檔案指定了全域性選項,然後可以由倉庫特定的配置檔案(如 repos/Talkhouse/p4gf_config
)覆蓋。如果你開啟此檔案,你將看到一個 [@repo]
部分,其中包含一些與全域性預設值不同的設定。你還將看到如下所示的部分
[Talkhouse-master]
git-branch-name = master
view = //depot/Talkhouse/main-dev/... ...
這是 Perforce 分支和 Git 分支之間的對映。該部分的名稱可以是你喜歡的任何名稱,只要名稱是唯一的即可。git-branch-name
允許你將一個在 Git 下可能很麻煩的倉庫路徑轉換為一個更友好的名稱。view
設定控制 Perforce 檔案如何使用標準檢視對映語法對映到 Git 倉庫中。可以指定多個對映,如下例所示
[multi-project-mapping]
git-branch-name = master
view = //depot/project1/main/... project1/...
//depot/project2/mainline/... project2/...
這樣,如果你的正常工作區對映包含目錄結構中的更改,你可以使用 Git 倉庫複製該結構。
我們要討論的最後一個檔案是 users/p4gf_usermap
,它將 Perforce 使用者對映到 Git 使用者,你甚至可能不需要它。當從 Perforce 變更集轉換為 Git 提交時,Git Fusion 的預設行為是查詢 Perforce 使用者,並使用其中儲存的電子郵件地址和全名作為 Git 中作者/提交者欄位。當進行反向轉換時,預設是查詢 Git 提交作者欄位中儲存電子郵件地址的 Perforce 使用者,並以該使用者的身份提交變更集(並應用許可權)。在大多數情況下,這種行為都很好,但請考慮以下對映檔案
john john@example.com "John Doe"
john johnny@appleseed.net "John Doe"
bob employeeX@example.com "Anon X. Mouse"
joe employeeY@example.com "Anon Y. Mouse"
每行格式為 <user> <email> "<full name>"
,建立一個單一的使用者對映。前兩行將兩個不同的電子郵件地址對映到同一個 Perforce 使用者賬戶。這在你使用多個不同電子郵件地址(或更改電子郵件地址)建立了 Git 提交,但希望它們對映到同一個 Perforce 使用者時很有用。當從 Perforce 變更集建立 Git 提交時,第一行與 Perforce 使用者匹配的行將用於 Git 作者資訊。
最後兩行隱藏了 Bob 和 Joe 的實際姓名和電子郵件地址,使其不出現在建立的 Git 提交中。如果你想將內部專案開源,但不想將你的員工目錄釋出給全世界,這會很有用。請注意,電子郵件地址和全名應該是唯一的,除非你希望所有 Git 提交都歸因於單個虛構作者。
工作流程
Perforce Git Fusion 是 Perforce 和 Git 版本控制之間的雙向橋接。讓我們來看看從 Git 端工作的感覺如何。我們假設我們已經使用如上所示的配置檔案映射了“Jam”專案,我們可以像這樣克隆它
$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (1704/1704), done.
Receiving objects: 100% (2070/2070), 1.21 MiB | 0 bytes/s, done.
remote: Total 2070 (delta 1242), reused 0 (delta 0)
Resolving deltas: 100% (1242/1242), done.
Checking connectivity... done.
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/origin/rel2.1
$ git log --oneline --decorate --graph --all
* 0a38c33 (origin/rel2.1) Create Jam 2.1 release branch.
| * d254865 (HEAD, origin/master, origin/HEAD, master) Upgrade to latest metrowerks on Beos -- the Intel one.
| * bd2f54a Put in fix for jam's NT handle leak.
| * c0f29e7 Fix URL in a jam doc
| * cc644ac Radstone's lynx port.
[...]
第一次這樣做時,可能需要一些時間。發生的事情是 Git Fusion 正在將 Perforce 歷史記錄中所有適用的變更集轉換為 Git 提交。這發生在伺服器本地,所以相對較快,但如果你有大量歷史記錄,仍然可能需要一些時間。隨後的 fetches 會進行增量轉換,所以感覺會更像 Git 的原生速度。
如你所見,我們的倉庫看起來與你可能使用的任何其他 Git 倉庫完全相同。有三個分支,Git 貼心地建立了一個本地 master
分支,它跟蹤 origin/master
。讓我們做一些工作,並建立幾個新的提交
# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]
我們有兩個新提交。現在我們看看是否有人在工作
$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
d254865..6afeb15 master -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]
看起來有人在!從這個檢視你可能看不出來,但 6afeb15
提交實際上是使用 Perforce 客戶端建立的。從 Git 的角度來看,它看起來就像另一個提交,這正是其要點所在。讓我們看看 Perforce 伺服器如何處理合併提交
$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
README | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
6afeb15..89cba2b master -> master
Git 認為它工作了。讓我們使用 p4v
的修訂圖功能,從 Perforce 的角度看看 README
檔案的歷史記錄

如果你以前從未見過這種檢視,它可能看起來令人困惑,但它顯示的概念與 Git 歷史的圖形檢視器相同。我們正在檢視 README
檔案的歷史記錄,所以左上角的目錄樹只顯示了該檔案在各個分支中出現的情況。右上角有一個檔案不同修訂版本之間關係的視覺圖,該圖的宏觀檢視在右下角。檢視的其餘部分用於顯示所選修訂版本(在本例中為 2
)的詳細資訊。
需要注意的一點是,圖表看起來與 Git 歷史記錄中的圖表完全相同。Perforce 沒有命名分支來儲存 1
和 2
提交,因此它在 .git-fusion
目錄中建立了一個“匿名”分支來儲存它們。對於不對應命名 Perforce 分支的命名 Git 分支(你可以稍後使用配置檔案將它們對映到 Perforce 分支),也會發生這種情況。
大部分操作都在幕後進行,但最終結果是團隊中的一個人可以使用 Git,另一個人可以使用 Perforce,而他們彼此都不知道對方的選擇。
Git-Fusion 總結
如果你有(或能獲得)訪問你的 Perforce 伺服器的許可權,Git Fusion 是讓 Git 和 Perforce 互相通訊的好方法。它涉及一些配置,但學習曲線並不陡峭。這是本章中少數不出現關於使用 Git 全部功能警告的部分之一。這並不是說 Perforce 會對你拋給它的一切都感到滿意——如果你嘗試重寫已推送的歷史記錄,Git Fusion 會拒絕它——但 Git Fusion 努力讓人感覺是原生的。你甚至可以使用 Git 子模組(儘管它們在 Perforce 使用者看來會很奇怪),併合並分支(這在 Perforce 端將記錄為一次整合)。
如果你無法說服你的伺服器管理員設定 Git Fusion,仍然有一種方法可以一起使用這些工具。
Git-p4
Git-p4 是 Git 和 Perforce 之間的雙向橋接。它完全在你的 Git 倉庫內部執行,因此你不需要對 Perforce 伺服器有任何訪問許可權(當然,除了使用者憑據之外)。Git-p4 不像 Git Fusion 那樣靈活或完整,但它允許你在不侵入伺服器環境的情況下完成大部分你想做的事情。
注意
|
要使用 git-p4,你需要在 |
設定
出於示例目的,我們將從上面所示的 Git Fusion OVA 執行 Perforce 伺服器,但我們將繞過 Git Fusion 伺服器,直接訪問 Perforce 版本控制。
為了使用 p4
命令列客戶端(git-p4 依賴於它),你需要設定幾個環境變數
$ export P4PORT=10.0.1.254:1666
$ export P4USER=john
開始
與 Git 中的任何操作一樣,第一個命令是克隆
$ git p4 clone //depot/www/live www-shallow
Importing from //depot/www/live into www-shallow
Initialized empty Git repository in /private/tmp/www-shallow/.git/
Doing initial import of //depot/www/live/ from revision #head into refs/remotes/p4/master
這在 Git 術語中建立了一個“淺層”克隆;只有最新的 Perforce 版本被匯入到 Git 中;請記住,Perforce 的設計並不是為了向每個使用者提供每個版本。這足以將 Git 用作 Perforce 客戶端,但對於其他目的來說還不夠。
完成之後,我們有一個功能齊全的 Git 倉庫
$ cd myproject
$ git log --oneline --all --graph --decorate
* 70eaf78 (HEAD, p4/master, p4/HEAD, master) Initial import of //depot/www/live/ from the state at revision #head
注意 Perforce 伺服器的“p4”遠端倉庫,但其他一切看起來都像一個標準的克隆。實際上,這有點誤導;那裡實際上並沒有遠端倉庫。
$ git remote -v
這個倉庫中根本不存在遠端倉庫。Git-p4 建立了一些引用來表示伺服器的狀態,它們在 git log
中看起來像遠端引用,但它們不受 Git 本身管理,你無法向它們推送。
工作流程
好的,我們來做一些工作。假設你已經在一個非常重要的功能上取得了一些進展,並且你已準備好向團隊其他人展示它。
$ git log --oneline --all --graph --decorate
* 018467c (HEAD, master) Change page title
* c0fb617 Update link
* 70eaf78 (p4/master, p4/HEAD) Initial import of //depot/www/live/ from the state at revision #head
我們已經完成了兩次新提交,並準備將它們提交到 Perforce 伺服器。讓我們檢查一下今天是否還有其他人工作
$ git p4 sync
git p4 sync
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12142 (100%)
$ git log --oneline --all --graph --decorate
* 75cd059 (p4/master, p4/HEAD) Update copyright
| * 018467c (HEAD, master) Change page title
| * c0fb617 Update link
|/
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
看起來他們有,而且 master
和 p4/master
已經分歧。Perforce 的分支系統與 Git 完全不同,因此提交合並提交沒有任何意義。Git-p4 建議你變基你的提交,甚至提供了一個快捷方式來執行此操作
$ git p4 rebase
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
No changes to import!
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
Applying: Update link
Applying: Change page title
index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
你可能從輸出中看出來,但 git p4 rebase
是 git p4 sync
後面跟著 git rebase p4/master
的快捷方式。它比這更智慧一些,尤其是在處理多個分支時,但這只是一個很好的近似。
現在我們的歷史記錄再次變為線性,我們已準備好將更改貢獻回 Perforce。git p4 submit
命令將嘗試為 p4/master
和 master
之間的每個 Git 提交建立一個新的 Perforce 修訂版本。執行它會將我們帶入我們喜歡的編輯器,檔案的內容看起來像這樣
# A Perforce Change Specification.
#
# Change: The change number. 'new' on a new changelist.
# Date: The date this specification was last modified.
# Client: The client on which the changelist was created. Read-only.
# User: The user who created the changelist.
# Status: Either 'pending' or 'submitted'. Read-only.
# Type: Either 'public' or 'restricted'. Default is 'public'.
# Description: Comments about the changelist. Required.
# Jobs: What opened jobs are to be closed by this changelist.
# You may delete jobs from this list. (New changelists only.)
# Files: What opened files from the default changelist are to be added
# to this changelist. You may delete files from this list.
# (New changelists only.)
Change: new
Client: john_bens-mbp_8487
User: john
Status: new
Description:
Update link
Files:
//depot/www/live/index.html # edit
######## git author ben@straub.cc does not match your p4 account.
######## Use option --preserve-user to modify authorship.
######## Variable git-p4.skipUserNameCheck hides this message.
######## everything below this line is just the diff #######
--- //depot/www/live/index.html 2014-08-31 18:26:05.000000000 0000
+++ /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/index.html 2014-08-31 18:26:05.000000000 0000
@@ -60,7 +60,7 @@
</td>
<td valign=top>
Source and documentation for
-<a href="http://www.perforce.com/jam/jam.html">
+<a href="jam.html">
Jam/MR</a>,
a software build tool.
</td>
這大部分與你執行 p4 submit
時看到的內容相同,只是末尾有 git-p4 貼心包含的一些內容。Git-p4 在需要為提交或變更集提供名稱時,會嘗試分別遵循你的 Git 和 Perforce 設定,但在某些情況下你可能想要覆蓋它。例如,如果你匯入的 Git 提交是由沒有 Perforce 使用者賬戶的貢獻者編寫的,你可能仍然希望生成的變更集看起來像是他們編寫的(而不是你)。
Git-p4 貼心地將 Git 提交中的訊息作為此 Perforce 變更集的內容匯入,所以我們只需儲存並退出,兩次(每個提交一次)。生成的 shell 輸出將如下所示
$ git p4 submit
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Synchronizing p4 checkout...
... - file(s) up-to-date.
Applying dbac45b Update link
//depot/www/live/index.html#4 - opened for edit
Change 12143 created with 1 open file(s).
Submitting change 12143.
Locking 1 files ...
edit //depot/www/live/index.html#5
Change 12143 submitted.
Applying 905ec6a Change page title
//depot/www/live/index.html#5 - opened for edit
Change 12144 created with 1 open file(s).
Submitting change 12144.
Locking 1 files ...
edit //depot/www/live/index.html#6
Change 12144 submitted.
All commits applied!
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12144 (100%)
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
$ git log --oneline --all --graph --decorate
* 775a46f (HEAD, p4/master, p4/HEAD, master) Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
結果就像我們剛剛執行了一次 git push
,這與實際發生的情況最接近。
請注意,在此過程中,每個 Git 提交都轉換為一個 Perforce 變更集;如果你想將它們壓縮成單個變更集,可以在執行 git p4 submit
之前使用互動式變基來完成。另請注意,所有作為變更集提交的提交的 SHA-1 雜湊值都已更改;這是因為 git-p4 會在其轉換的每個提交末尾新增一行
$ git log -1
commit 775a46f630d8b46535fc9983cf3ebe6b9aa53145
Author: John Doe <john@example.com>
Date: Sun Aug 31 10:31:44 2014 -0800
Change page title
[git-p4: depot-paths = "//depot/www/live/": change = 12144]
如果你嘗試提交一個合併提交會發生什麼?我們來試試看。這是我們陷入的局面
$ git log --oneline --all --graph --decorate
* 3be6fd8 (HEAD, master) Correct email address
* 1dcbf21 Merge remote-tracking branch 'p4/master'
|\
| * c4689fc (p4/master, p4/HEAD) Grammar fix
* | cbacd0a Table borders: yes please
* | b4959b6 Trademark
|/
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
在 775a46f
之後,Git 和 Perforce 的歷史記錄出現分歧。Git 端有兩次提交,然後是一個與 Perforce 頭部的合併提交,然後是另一次提交。我們現在將嘗試在 Perforce 端的單個變更集之上提交這些內容。讓我們看看如果現在嘗試提交會發生什麼
$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
b4959b6 Trademark
cbacd0a Table borders: yes please
3be6fd8 Correct email address
-n
標誌是 --dry-run
的縮寫,它會嘗試報告如果實際執行提交命令會發生什麼。在這種情況下,看起來我們將建立三個 Perforce 變更集,它們對應於 Perforce 伺服器上尚未存在的三個非合併提交。這聽起來正是我們想要的,讓我們看看結果如何
$ git p4 submit
[…]
$ git log --oneline --all --graph --decorate
* dadbd89 (HEAD, p4/master, p4/HEAD, master) Correct email address
* 1b79a80 Table borders: yes please
* 0097235 Trademark
* c4689fc Grammar fix
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
我們的歷史記錄變得線性,就像我們提交前進行了變基一樣(這實際上正是發生的情況)。這意味著你可以自由地在 Git 端建立、工作、拋棄和合並分支,而不必擔心你的歷史記錄會以某種方式與 Perforce 不相容。如果你可以變基它,你就可以將其貢獻給 Perforce 伺服器。
分支
如果你的 Perforce 專案有多個分支,你並非束手無策;git-p4 可以以一種讓你感覺像 Git 的方式處理這種情況。假設你的 Perforce depot 佈局如下
//depot
└── project
├── main
└── dev
假設你有一個 dev
分支,其檢視規範看起來像這樣
//depot/project/main/... //depot/project/dev/...
Git-p4 可以自動檢測到這種情況並做正確的事情
$ git p4 clone --detect-branches //depot/project@all
Importing from //depot/project@all into project
Initialized empty Git repository in /private/tmp/project/.git/
Importing revision 20 (50%)
Importing new branch project/dev
Resuming with change 20
Importing revision 22 (100%)
Updated branches: main dev
$ cd project; git log --oneline --all --graph --decorate
* eae77ae (HEAD, p4/master, p4/HEAD, master) main
| * 10d55fb (p4/project/dev) dev
| * a43cfae Populate //depot/project/main/... //depot/project/dev/....
|/
* 2b83451 Project init
請注意 depot 路徑中的“@all”指定符;它告訴 git-p4 不僅克隆該子樹的最新變更集,還克隆所有曾經觸及這些路徑的變更集。這更接近 Git 的克隆概念,但如果你正在處理一個擁有悠久歷史的專案,這可能需要一段時間。
--detect-branches
標誌告訴 git-p4 使用 Perforce 的分支規範將分支對映到 Git 引用。如果 Perforce 伺服器上沒有這些對映(這是使用 Perforce 的完全有效方式),你可以告訴 git-p4 分支對映是什麼,你將得到相同的結果
$ git init project
Initialized empty Git repository in /tmp/project/.git/
$ cd project
$ git config git-p4.branchList main:dev
$ git clone --detect-branches //depot/project@all .
將 git-p4.branchList
配置變數設定為 main:dev
告訴 git-p4,“main”和“dev”都是分支,並且第二個是第一個的子分支。
如果我們現在 git checkout -b dev p4/project/dev
並進行一些提交,git-p4 會足夠智慧地在執行 git p4 submit
時定位到正確的分支。不幸的是,git-p4 無法混合淺層克隆和多個分支;如果你有一個龐大的專案並且想在多個分支上工作,你將不得不為每個你想要提交的分支執行一次 git p4 clone
。
要建立或整合分支,你必須使用 Perforce 客戶端。Git-p4 只能同步和提交到現有分支,並且只能一次一個線性變更集。如果你在 Git 中合併兩個分支並嘗試提交新的變更集,所有記錄的都只是一堆檔案更改;關於整合中涉及哪些分支的元資料將丟失。
Git 與 Perforce 總結
Git-p4 使得將 Git 工作流與 Perforce 伺服器結合使用成為可能,並且它在這方面做得相當不錯。但是,重要的是要記住 Perforce 負責原始碼,而你只是使用 Git 在本地工作。請務必小心共享 Git 提交;如果你有一個其他人使用的遠端倉庫,請不要推送任何尚未提交到 Perforce 伺服器的提交。
如果你想自由混合使用 Perforce 和 Git 作為原始碼控制客戶端,並且你可以說服伺服器管理員安裝它,那麼 Git Fusion 會使使用 Git 成為 Perforce 伺服器的一等版本控制客戶端。