Git
章節 ▾ 第二版

9.1 Git 與其他系統 - 將 Git 作為用戶端

這個世界並不完美。通常,您無法立即將接觸到的每個專案都切換到 Git。有時您會被困在另一個使用其他 VCS 的專案上,並希望它是 Git。本章的第一部分將學習在您正在處理的專案託管在不同系統中時,如何將 Git 作為用戶端使用。

在某些時候,您可能想要將現有的專案轉換為 Git。本章的第二部分介紹如何從幾個特定系統將您的專案遷移到 Git 中,以及如果沒有預先建立的匯入工具時可使用的方法。

將 Git 作為用戶端

Git 為開發人員提供了如此出色的體驗,以至於許多人已經弄清楚如何在其工作站上使用它,即使其團隊的其他成員正在使用完全不同的 VCS。有許多這樣的轉接器,稱為「橋樑」。在這裡,我們將介紹您最有可能在實際應用中遇到的那些。

Git 與 Subversion

很大一部分的開放原始碼開發專案和相當數量的公司專案都使用 Subversion 來管理其原始碼。它已經存在了十多年,並且在大部分時間裡都是開放原始碼專案的事實 VCS 選擇。它在許多方面也與 CVS 非常相似,後者是當時原始碼控制領域的大人物。

Git 的一個很棒的功能是到 Subversion 的雙向橋樑,稱為 git svn。此工具可讓您將 Git 用作 Subversion 伺服器的有效用戶端,因此您可以使用 Git 的所有本機功能,然後推送至 Subversion 伺服器,就像您在本機使用 Subversion 一樣。這表示您可以在本機執行分支和合併、使用暫存區、使用變基和挑選等操作,同時您的協作者繼續以他們黑暗而古老的方式工作。這是一種將 Git 偷偷導入公司環境的好方法,並幫助您的開發人員同事變得更有效率,同時您會遊說變更基礎架構以完全支援 Git。Subversion 橋樑是通往 DVCS 世界的入門藥物。

git svn

在 Git 中用於所有 Subversion 橋樑命令的基本命令是 git svn。它採用相當多的命令,因此我們將在瀏覽一些簡單的工作流程時顯示最常見的命令。

重要的是要注意,當您使用 git svn 時,您正在與 Subversion 互動,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-svn-id`

$ 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` 會執行擷取,然後更新你的本機提交。

$ 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` – 否則,如果它看到重新設定基準會導致合併衝突,則該命令將會停止。

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`),否則合併將會顯示「合併分支 opera」,而不是有用的資訊。

請記住,雖然你正在使用 `git merge` 來執行此操作,並且合併可能會比在 Subversion 中容易得多 (因為 Git 會自動偵測適當的合併基礎),但這不是一般的 Git 合併提交。 你必須將此資料推送回無法處理追蹤多個父系的提交的 Subversion 伺服器;因此,在你將其推送上去之後,它看起來會像一個單一提交,其中將另一個分支的所有工作壓縮到單一提交中。 在你將一個分支合併到另一個分支之後,你無法像在 Git 中正常情況下那樣輕鬆地回去並繼續在該分支上工作。 你執行的 `dcommit` 命令會抹除任何表示哪個分支已合併的資訊,因此後續的合併基礎計算會錯誤 – `dcommit` 會使你的 `git merge` 結果看起來像是你執行了 `git merge --squash`。 不幸的是,沒有很好的方法可以避免這種情況 – Subversion 無法儲存此資訊,因此當你將其用作伺服器時,你將始終會受到其限制的阻礙。 若要避免問題,你應該在將本機分支 (在此案例中為 `opera`) 合併到主幹之後將其刪除。

Subversion 命令

`git svn` 工具集提供許多命令,透過提供一些與你在 Subversion 中擁有的功能類似的功能來協助簡化轉換到 Git 的過程。 以下是一些可提供你過去使用 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 [檔案] 來取得與 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)

這與 blamelog 類似,它會離線執行,並且僅在您上次與 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 伺服器,或者在需要執行 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 作為 Mercurial 託管儲存庫的客戶端。由於 Git 與伺服器儲存庫的通訊方式是透過遠端,因此這個橋樑是以遠端輔助程式的形式實作的,這應該不足為奇。該專案的名稱是 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://www.python.org/ 並先取得它。

您需要的最後一件事是 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」的 blob (master 所指向的提交的 SHA-1 雜湊),其內容為「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 建立,而編號為34 的變更集則由 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 端刪除書籤 (這是遠端輔助程式的限制)。

您也可以在「重要」的 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

變更集 8910 已經建立,並屬於 permanent 分支,但舊的變更集仍然存在。這對使用 Mercurial 的隊友來說可能會非常令人困惑,因此請盡量避免這樣做。

Mercurial 總結

Git 和 Mercurial 相當相似,因此跨界使用並不會太麻煩。如果您避免變更已離開您電腦的歷史記錄(通常建議這樣做),您甚至可能不會察覺到另一端是 Mercurial。

Git 和 Perforce

Perforce 在企業環境中是非常受歡迎的版本控制系統。它自 1995 年就已存在,是本章涵蓋的最古老系統。因此,它的設計考量了當時的限制;它假設您始終連接到單一中央伺服器,並且本機磁碟上僅保留一個版本。當然,它的功能和限制非常適合一些特定問題,但是許多使用 Perforce 的專案,實際上 Git 會更有效。

如果您想混合使用 Perforce 和 Git,有兩個選項。我們將介紹的第一個選項是 Perforce 製造商提供的「Git Fusion」橋接器,它可讓您將 Perforce depot 的子樹公開為可讀寫的 Git 儲存庫。第二個選項是 git-p4,一個客戶端橋接器,可讓您將 Git 作為 Perforce 客戶端使用,而無需重新配置 Perforce 伺服器。

Git Fusion

Perforce 提供一個名為 Git Fusion 的產品(網址為 https://www.perforce.com/manuals/git-fusion/),它可以在伺服器端將 Perforce 伺服器與 Git 儲存庫同步。

設定

在我們的範例中,我們將使用 Git Fusion 最簡單的安裝方法,即下載一個執行 Perforce daemon 和 Git Fusion 的虛擬機器。您可以從 https://www.perforce.com/downloads 取得虛擬機器映像檔,下載完成後,將其匯入您最愛的虛擬化軟體(我們將使用 VirtualBox)。

首次啟動機器時,它會要求您自訂三個 Linux 使用者 (rootperforcegit) 的密碼,並提供一個執行個體名稱,該名稱可用於區分同一網路上其他安裝。全部完成後,您會看到這個畫面

The Git Fusion virtual machine boot screen
圖 171. Git Fusion 虛擬機器啟動畫面

您應該記下這裡顯示的 IP 位址,我們稍後會用到它。接下來,我們將建立一個 Perforce 使用者。選擇底部的「Login」選項並按 Enter 鍵(或 SSH 到機器),然後以 root 身份登入。然後使用這些命令來建立使用者

$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit

第一個命令會開啟 VI 編輯器來自訂使用者,但您可以輸入 :wq 並按 Enter 鍵來接受預設值。第二個命令會提示您輸入兩次密碼。這就是我們在 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.

虛擬機器映像檔配備了一個您可以複製的範例專案。在這裡,我們使用上面建立的 john 使用者,透過 HTTPS 進行複製;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 下繁瑣的 depot 路徑轉換為更友好的名稱。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 commit 時,Git Fusion 的預設行為是尋找 Perforce 使用者,並使用那裡儲存的電子郵件地址和全名,用於 Git 中的作者/提交者欄位。在另一種方式的轉換中,預設是尋找 Git commit 的作者欄位中儲存的電子郵件地址的 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 commit(或變更電子郵件地址),但希望將它們對應到同一個 Perforce 使用者,這會很有用。從 Perforce 變更集建立 Git commit 時,會將與 Perforce 使用者相符的第一行用於 Git 作者資訊。

最後兩行會遮蔽 Bob 和 Joe 的實際名稱和電子郵件地址,使其不會出現在建立的 Git commit 中。如果您想要開放原始碼內部專案,但不希望將您的員工目錄發佈到全世界,這會很棒。請注意,電子郵件地址和全名應該是唯一的,除非您希望將所有 Git commit 歸因於單一虛構的作者。

工作流程

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 commit。這會在伺服器本機上發生,因此速度相對較快,但如果您有大量歷史記錄,仍然可能需要一些時間。後續的提取會執行增量轉換,因此感覺更像 Git 的原生速度。

如您所見,我們的儲存庫看起來與您可能使用的任何其他 Git 儲存庫完全相同。有三個分支,而 Git 已協助建立一個追蹤 origin/master 的本機 master 分支。讓我們做一些工作,並建立幾個新的 commit

# ...
$ 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.
[...]

我們有兩個新的 commit。現在讓我們檢查是否有其他人正在工作

$ 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 commit 實際上是使用 Perforce 客戶端建立的。它看起來就像 Git 觀點中的另一個 commit,這正是重點。讓我們看看 Perforce 伺服器如何處理合併 commit

$ 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 認為它運作了。讓我們從 Perforce 的觀點查看 README 檔案的歷史記錄,使用 p4v 的修訂圖功能

Perforce revision graph resulting from Git push
圖 172. Git 推送產生的 Perforce 修訂圖

如果您以前從未看過此檢視,它可能看起來令人困惑,但它顯示與 Git 歷史記錄的圖形檢視器相同的概念。我們正在查看 README 檔案的歷史記錄,因此左上方的目錄樹僅顯示該檔案在不同分支中的表面。在右上角,我們有一個檔案的不同修訂版本如何相關的視覺圖,而此圖的總覽在右下角。檢視的其餘部分會提供所選修訂版本(在本例中為 2)的詳細檢視。

需要注意的一件事是,該圖看起來與 Git 歷史記錄中的圖完全相同。Perforce 沒有用於儲存 12 commit 的具名分支,因此它在 .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 那樣彈性或完整,但它確實允許您執行大多數您想做的事情,而不會對伺服器環境造成侵入性。

注意

您需要在 PATH 中的某處使用 p4 工具才能使用 git-p4。在撰寫本文時,它可在 https://www.perforce.com/downloads/helix-command-line-client-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 建立了一些參考(refs)來表示伺服器的狀態,它們在 git log 中看起來像遠端參考,但它們並非由 Git 本身管理,而且您無法推送(push)到它們。

工作流程

好的,我們開始做些工作。假設您在一個非常重要的功能上取得了一些進展,並且準備好向團隊的其他成員展示它。

$ 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

我們已經建立了兩個新的提交(commit),準備提交到 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

看起來他們有,而且 masterp4/master 已經分歧。Perforce 的分支系統與 Git 的完全不同,因此提交合併提交(merge commit)沒有任何意義。Git-p4 建議您對您的提交進行變基(rebase),甚至提供了一個快捷方式來執行此操作。

$ 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 rebasegit p4 sync 後接 git rebase p4/master 的快捷方式。它比那更聰明一點,尤其是在處理多個分支時,但這是一個很好的近似。

現在我們的歷史記錄再次變成線性,我們準備將變更貢獻回 Perforce。git p4 submit 命令會嘗試為 p4/mastermaster 之間的每個 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

Git 和 Perforce 的歷史記錄在 775a46f 之後分歧。Git 端有兩個提交,然後是一個與 Perforce head 合併的提交,然後是另一個提交。我們將嘗試將這些提交到 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
  └── 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

請注意倉庫路徑中的「@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 伺服器的第一級版本控制客戶端。

scroll-to-top