# 作為客戶端的 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 打交道,一個與 Git 完全不同的系統。 盡管?**可以**?在本地新建分支與合并分支,但是你最好還是通過變基你的工作來保證你的歷史盡可能是直線,并且避免做類似同時與 Git 遠程服務器交互的事情。
不要重寫你的歷史然后嘗試再次推送,同時也不要推送到一個平行的 Git 倉庫來與其他使用 Git 的開發者協作。 Subversion 只能有一個線性的歷史,弄亂它很容易。 如果你在一個團隊中工作,其中有一些人使用 SVN 而另一些人使用 Git,你需要確保每個人都使用 SVN 服務器來協作 - 這樣做會省去很多麻煩。
#### 設置
為了演示這個功能,需要一個有寫入權限的典型 SVN 倉庫。 如果想要拷貝這些例子,你必須獲得一份我的測試倉庫的可寫拷貝。 為了輕松地拷貝,可以使用 Subversion 自帶的一個名為?`svnsync`?的工具。 為了這些測試,我們在 Google Code 上創建了一個?`protobuf`?項目部分拷貝的新 Subversion 倉庫。`protobuf`?是一個將結構性數據編碼用于網絡傳輸的工具。
接下來,你需要先創建一個新的本地 Subversion 倉庫:
~~~
$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn
~~~
然后,允許所有用戶改變版本屬性 - 最容易的方式是添加一個返回值為 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://progit-example.googlecode.com/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
~~~
這相當于運行了兩個命令 -?`git svn init`?以及緊接著的?`git svn fetch`?- 你提供的 URL 。 這會花費一些時間。 測試項目只有 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
~~~
注意你原來提交的 SHA-1 校驗和原來是以?`4af61fd`?開頭,而現在是以?`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 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 [branchname]`:
~~~
$ 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 分支的頭部來指出你的提交將會到哪一個分支 - 應該只有一個,并且它應該是在當前分支歷史中最后一個有?`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 服務器,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 [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)
~~~
這就像是在你上一次和 Subversion 服務器通訊時同步了之后,離線運行的?`blame`?與?`log`?命令。
#### 忽略 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 服務器上協作。 可以用一臺 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*](https://github.com/felipec/git-remote-hg)?找到。
#### git-remote-hg
首先,需要安裝 git-remote-hg。 實際上需要將它的文件放在 PATH 變量的某個目錄中,像這樣:
~~~
$ 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 有一個其他的依賴:`mercurial`?Python 庫。 如果已經安裝了 Python,安裝它就像這樣簡單:
~~~
$ pip install mercurial
~~~
(如果未安裝 Python,訪問?[*https://www.python.org/*](https://www.python.org/)?來獲取它。)
需要做的最后一件事是安裝 Mercurial 客戶端。 如果還沒有安裝的話請訪問[*http://mercurial.selenic.com/*](http://mercurial.selenic.com/)?來安裝。
現在已經準備好搖滾了。 你所需要的一切就是一個你可以推送的 Mercurial 倉庫。 很幸運,每一個 Mercurial 倉庫都可以這樣做,所以我們只需要使用大家用來學習 Mercurial 的“hello world”倉庫就可以了:
~~~
$ hg clone http://selenic.com/repo/hello /tmp/hello
~~~
#### 開始
既然有一個可用的 “server-side” 倉庫,我們可以通過一個典型的工作流來了解。 你將會看到,這兩種系統非常相似,沒有太多的出入。
和 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
~~~
你會注意到與 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`?是一個包含以“ac7955c”開始的 SHA-1 值的 Git 引用文件,是?`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`?輸出 tree 對象中所有項目的模式、類型、對象哈希與文件名。 如果深入挖掘 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
~~~
我們的?`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
|/
~~~
因為使用了?`--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
~~~
完美。 運行測試然后所有測試都通過了,所以我們準備將工作共享給團隊的其他成員。
~~~
$ 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 中,這種類型的引用叫作 “bookmark”,它的行為非常類似于 Git 分支。
Mercurial 的 “branch” 概念則更重量級一些。 變更集生成時的分支會記錄?*在變更集中*,意味著它會永遠地存在于倉庫歷史中。 這個例子描述了一個在?`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 branch:只需要在?`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 的產品(可在?[*http://www.perforce.com/git-fusion*](http://www.perforce.com/git-fusion)?獲得),它將會在服務器這邊同步 Perforce 服務器與 Git 倉庫。
#### 設置
針對我們的例子,我們將會使用最簡單的方式安裝 Git Fusion:下載一個虛擬機來運行 Perforce 守護進程與 Git Fusion。 可以從?[*http://www.perforce.com/downloads/Perforce/20-User*](http://www.perforce.com/downloads/Perforce/20-User)?獲得虛擬機鏡像,下載完成后將它導入到你最愛的虛擬機軟件中(我們將會使用 VirtualBox)。
在第一次啟動機器后,它會詢問你自定義三個 Linux 用戶(`root`、`perforce`?與?`git`)的密碼,并且提供一個實例名字來區分在同一網絡下不同的安裝。 當那些都完成后,將會看到這樣:

Figure 9-1.?Git Fusion 虛擬機啟動屏幕。
應當注意顯示在這兒的 IP 地址,我們將會在后面用到。 接下來,我們將會創建一個 Perforce 用戶。 選擇底部的 “Login” 選項并按下回車(或者用 SSH 連接到這臺機器),然后登錄為`root`。 然后使用這些命令創建一個用戶:
~~~
$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit
~~~
第一個命令將會打開一個 VI 編輯器來自定義用戶,但是可以通過輸入?`:wq`?并回車來接受默認選項。 第二個命令將會提示輸入密碼兩次。 這就是所有我們要通過終端提示符做的事情,所以現在可以退出當前會話了。
接下來要做的事就是告訴 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 下顯得笨重的倉庫路徑轉換為更友好的名字。?`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 的 author/commiter 字段中。 當反過來轉換時,默認的行為是根據存儲在 Git 提交中 author 字段中的郵箱地址來查找 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 作者信息。
最后兩行從創建的 Git 提交中掩蓋了 Bob 與 Joe 的真實名字與郵箱地址。 當你想要將一個內部項目開源,但不想將你的雇員目錄公布到全世界時這很不錯。 注意郵箱地址與全名需要是唯一的,除非想要所有的 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://ben@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 提交。 這發生在服務器端本地,所以會相當快,但是如果有很多歷史,那么它還是會花費一些時間。 后來的抓取會做增量轉換,所以會感覺更像 Git 的本地速度。
如你所見,我們的倉庫看起來像之前使用過的任何一個 Git 倉庫了。 這里有三個分支,Git 已經幫助創建了一個跟蹤?`origin/master`?的本地?`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 認為它成功了。 讓我們從 Perforce 的視角看一下?`README`?文件的歷史,使用?`p4v`?的版本圖功能。

Figure 9-2.?Git 推送后的 Perforce 版本圖
如果你在之前從未看過這個視圖,它似乎讓人困惑,但是它顯示出了作為 Git 歷史圖形化查看器相同的概念。 我們正在查看?`README`?文件的歷史,所以左上角的目錄樹只顯示那個文件在不同分支的樣子。 右上方,我們有不同版本文件關系的可視圖,這個可視圖的全局視圖在右下方。 視圖中剩余的部分顯示出選擇版本的詳細信息(在這個例子中是?`2`)
還要注意的一件事是這個圖看起來很像 Git 歷史中的圖。 Perforce 沒有存儲?`1`?和?`2`?提交的命名分支,所以它在?`.git-fusion`?目錄中生成了一個 “anonymous” 分支來保存它。 這也會在 Git 命名分支不對應 Perforce 命名分支時發生(稍后你可以使用配置文件來映射它們到 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 一樣靈活或完整,但是它允許你在無需修改服務器環境的情況下,做大部分想做的事情。
###### NOTE
為了與 git-p4 一起工作需要在你的?`PATH`?環境變量中的某個目錄中有?`p4`?工具。 在寫這篇文章的時候,它可以在?[*http://www.perforce.com/downloads/Perforce/20-User*](http://www.perforce.com/downloads/Perforce/20-User)?免費獲得。
#### 設置
出于演示的目的,我們將會從上面演示的 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 中名為 “shallow” 克隆;只有最新版本的 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
~~~
注意有一個 “p4” 遠程代表 Perforce 服務器,但是其他東西看起來就像是標準的克隆。 實際上,這有一點誤導;其實遠程倉庫并不存在。
~~~
$ 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>
~~~
除了結尾 git-p4 給我們的幫助性的提示,其它的與你運行?`p4 submit`?后看到的內容大多相同。 當提交或變更集需要一個名字時 git-p4 會分別嘗試使用你的 Git 與 Perforce 設置,但是有些情況下你會想要覆蓋默認行為。 例如,如果你正導入的提交是由沒有 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 頭部的合并提交,還有另一個提交。 我們將會嘗試提交這些到 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 submit`?時 git-p4 會聰明地選擇正確的分支。 不幸的是,git-p4 不能混用 shallow 克隆與多個分支;如果你有一個巨型項目并且想要同時工作在不止一個分支上,可能不得不針對每一個你想要提交的分支運行一次?`git p4 clone`。
為了創建與整合分支,你不得不使用一個 Perforce 客戶端。 Git-p4 只能同步或提交已有分支,并且它一次只能做一個線性的變更集。 如果你在 Git 中合并兩個分支并嘗試提交新的變更集,所有這些會被記錄為一串文件修改;關于哪個分支參與的元數據在整合中會丟失。
#### Git 與 Perforce 總結
Git-p4 將與 Perforce 服務器工作時使用 Git 工作流成為可能,并且它非常擅長這點。 然而,需要記住的重要一點是 Perforce 負責源頭,而你只是在本地使用 Git。 在共享 Git 提交時要相當小心:如果你有一個其他人使用的遠程倉庫,不要在提交到 Perforce 服務器前推送任何提交。
如果想要為源碼管理自由地混合使用 Perforce 與 Git 作為客戶端,可以說服服務器管理員安裝 Git Fusion,Git Fusion 使 Git 作為 Perforce 服務器的首級版本管理客戶端。
## Git 與 TFS
Git 在 Windows 開發者當中變得流行起來,如果你正在 Windows 上編寫代碼并且正在使用 Microsoft 的 Team Foundation Server (TFS),這會是個好機會。 TFS 是一個包含工作項目檢測與跟蹤、支持 Scrum 與其他流程管理方法、代碼審核、版本控制的協作套件。 這里有一點困惑:**TFS**是服務器,它支持通過 Git 與它們自定義的 VCS 來管理源代碼,這被他們稱為?**TFVC**(Team Foundation Version Control)。 Git 支持 TFS(自 2013 版本起)的部分新功能,所以在那之前所有工具都將版本控制部分稱為 “TFS”,即使實際上他們大部分時間都在與 TFVC 工作。
如果發現你的團隊在使用 TFVC 但是你更愿意使用 Git 作為版本控制客戶端,這里為你準備了一個項目。
#### 選擇哪個工具
實際上,這里有兩個工具:git-tf 與 git-tfs。
Git-tfs (可以在?[*https://github.com/git-tfs/git-tfs*](https://github.com/git-tfs/git-tfs)?找到)是一個 .NET 項目,它只能運行在 Windows 上(截至文章完成時)。 為了操作 Git 倉庫,它使用了 libgit2 的 .NET 綁定,一個可靠的面向庫的 Git 實現,十分靈活且性能優越。 Libgit2 并不是一個完整的 Git 實現,為了彌補差距 git-tfs 實際上會調用 Git 命令行客戶端來執行某些操作,因此在操作 Git 倉庫時并沒有任何功能限制。 因為它使用 Visual Studio 程序集對服務器進行操作,所以它對 TFVC 的支持非常成熟。 這并不意味著你需要接觸那些程序集,但是意味著你需要安裝 Visual Studio 的一個最近版本(2010 之后的任何版本,包括 2012 之后的 Express 版本),或者 Visual Studio SDK。
Git-tf(主頁在?[*https://gittf.codeplex.com*](https://gittf.codeplex.com/))是一個 Java 項目,因此它可以運行在任何一個有 Java 運行時環境的電腦上。 它通過 JGit(一個 Git 的 JVM 實現)來與 Git 倉庫交互,這意味著事實上它沒有 Git 功能上的限制。 然而,相對于 git-tfs 它對 TFVC 的支持是有限的 - 例如,它不支持分支。
所以每個工具都有優點和缺點,每個工具都有它適用的情況。 我們在本書中將會介紹它們兩個的基本用法。
###### NOTE
你需要有一個基于 TFVC 的倉庫來執行后續的指令。 現實中它們并沒有 Git 或 Subversion 倉庫那樣多,所以你可能需要創建一個你自己的倉庫。 Codeplex ([*https://www.codeplex.com*](https://www.codeplex.com/)) 或 Visual Studio Online ([*http://www.visualstudio.com*](http://www.visualstudio.com/)) 都是非常好的選擇。
#### 使用:`git-tf`
和其它任何 Git 項目一樣,你要做的第一件事是克隆。 使用?`git-tf`?克隆看起來像這樣:
~~~
$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main project_git
~~~
第一個參數是一個 TFVC 集的 URL,第二個參數類似于?`$/project/branch`?的形式,第三個參數是將要創建的本地 Git 倉庫路徑(最后一項可以省略)。 Git-tf 同一時間只能工作在一個分支上;如果你想要檢入一個不同的 TFVC 分支,你需要從那個分支克隆一份新的。
這會創建一個完整功能的 Git 倉庫:
~~~
$ cd project_git
$ git log --all --oneline --decorate
512e75a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Checkin message
~~~
這叫做?*淺*?克隆,意味著只下載了最新的變更集。 TFVC 并未設計成為每一個客戶端提供一份全部歷史記錄的拷貝,所以 git-tf 默認行為是獲得最新的版本,這樣更快一些。
如果愿意多花一些時間,使用?`--deep`?選項克隆整個項目歷史可能更有價值。
~~~
$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main \
project_git --deep
Username: domain\user
Password:
Connecting to TFS...
Cloning $/myproject into /tmp/project_git: 100%, done.
Cloned 4 changesets. Cloned last changeset 35190 as d44b17a
$ cd project_git
$ git log --all --oneline --decorate
d44b17a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Goodbye
126aa7b (tag: TFS_C35189)
8f77431 (tag: TFS_C35178) FIRST
0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
Team Project Creation Wizard
~~~
注意名字類似?`TFS_C35189`?的標簽;這是一個幫助你知道 Git 提交與 TFVC 變更集關聯的功能。 這是一種優雅的表示方式,因為通過一個簡單的 log 命令就可以看到你的提交是如何與 TFVC 中已存在快照關聯起來的。 它們并不是必須的(并且實際上可以使用?`git config git-tf.tag false`來關閉它們)- git-tf 會在?`.git/git-tf`?文件中保存真正的提交與變更集的映射。
#### 使用:`git-tfs`
Git-tfs 克隆行為略為不同。 觀察:
~~~
PS> git tfs clone --with-branches \
https://username.visualstudio.com/DefaultCollection \
$/project/Trunk project_git
Initialized empty Git repository in C:/Users/ben/project_git/.git/
C15 = b75da1aba1ffb359d00e85c52acb261e4586b0c9
C16 = c403405f4989d73a2c3c119e79021cb2104ce44a
Tfs branches found:
- $/tfvc-test/featureA
The name of the local branch will be : featureA
C17 = d202b53f67bde32171d5078968c644e562f1c439
C18 = 44cd729d8df868a8be20438fdeeefb961958b674
~~~
注意?`--with-branches`?選項。 Git-tfs 能夠映射 TFVC 分支到 Git 分支,這個標記告訴它為每一個 TFVC 分支建立一個本地的 Git 分支。 強烈推薦曾經在 TFS 中新建過分支或合并過分支的倉庫使用這個標記,但是如果使用的服務器的版本比 TFS 2010 更老 - 在那個版本前,“分支” 只是文件夾,所以 git-tfs 無法將它們與普通文件夾區分開。
讓我們看一下最終的 Git 倉庫:
~~~
PS> git log --oneline --graph --decorate --all
* 44cd729 (tfs/featureA, featureA) Goodbye
* d202b53 Branched from $/tfvc-test/Trunk
* c403405 (HEAD, tfs/default, master) Hello
* b75da1a New project
PS> git log -1
commit c403405f4989d73a2c3c119e79021cb2104ce44a
Author: Ben Straub <ben@straub.cc>
Date: Fri Aug 1 03:41:59 2014 +0000
Hello
git-tfs-id: [https://username.visualstudio.com/DefaultCollection]$/myproject/Trunk;C16
~~~
有兩個本地分支,`master`?與?`featureA`,分別代表著克隆(TFVC 中的?`Trunk`)與子分支(TFVC 中的?`featureA`)的初始狀態。 也可以看到?`tfs`?“remote” 也有一對引用:`default`與?`featureA`,代表 TFVC 分支。 Git-tfs 映射從?`tfs/default`?克隆的分支,其他的會有它們自己的名字。
另一件需要注意的事情是在提交信息中的?`git-tfs-id:`?行。 Git-tfs 使用這些標記而不是標簽來關聯 TFVC 變更集與 Git 提交。 有一個潛在的問題是 Git 提交在推送到 TFVC 前后會有不同的 SHA-1 校驗和。
#### Git-tf[s] 工作流程
###### NOTE
無論你使用哪個工具,都需要先設置幾個 Git 配置選項來避免一些問題。
~~~
$ git config set --local core.ignorecase=true
$ git config set --local core.autocrlf=false
~~~
顯然,接下來要做的事情就是要在項目中做一些工作。 TFVC 與 TFS 有幾個功能可能會增加你的工作流程的復雜性:
1. TFVC 無法表示特性分支,這會增加一點復雜度。 這會導致需要以?**非常**?不同的方式使用 TFVC 與 Git 表示的分支。
2. 要意識到 TFVC 允許用戶從服務器上 “檢出” 文件并鎖定它們,這樣其他人就無法編輯了。 顯然它不會阻止你在本地倉庫中編輯它們,但是當推送你的修改到 TFVC 服務器時會出現問題。
3. TFS 有一個 “封閉” 檢入的概念,TFS 構建-測試循環必須在檢入被允許前成功完成。 這使用了 TFVC 的 “shelve” 功能,我們不會在這里詳述。 可以通過 git-tf 手動地模擬這個功能,并且 git-tfs 提供了封閉敏感的?`checkintool`?命令。
出于簡潔性的原因,我們這里介紹的是一種輕松的方式,回避并避免了大部分問題。
#### 工作流程:`git-tf`
假定你完成了一些工作,在?`master`?中做了幾次 Git 提交,然后準備將你的進度共享到服務器。 這是我們的 Git 倉庫:
~~~
$ git log --oneline --graph --decorate --all
* 4178a82 (HEAD, master) update code
* 9df2ae3 update readme
* d44b17a (tag: TFS_C35190, origin_tfs/tfs) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
Team Project Creation Wizard
~~~
我們想要拿到在?`4178a82`?提交的快照并將其推送到 TFVC 服務器。 先說重要的:讓我們看看自從上次連接后我們的隊友是否進行過改動:
~~~
$ git tf fetch
Username: domain\user
Password:
Connecting to TFS...
Fetching $/myproject at latest changeset: 100%, done.
Downloaded changeset 35320 as commit 8ef06a8. Updated FETCH_HEAD.
$ git log --oneline --graph --decorate --all
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
| * 4178a82 (HEAD, master) update code
| * 9df2ae3 update readme
|/
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
Team Project Creation Wizard
~~~
看起來其他人也做了一些改動,現在我們有一個分叉的歷史。 這就是 Git 的優勢,但是我們現在有兩種處理的方式:
1. 像一名 Git 用戶一樣自然的生成一個合并提交(畢竟,那也是?`git pull`?做的),git-tf 可以通過一個簡單的?`git tf pull`?來幫你完成。 然而,我們要注意的是,TFVC 卻并不這樣想,如果你推送合并提交那么你的歷史在兩邊看起來都不一樣,這會造成困惑。 其次,如果你計劃將所有你的改動提交為一次變更集,這可能是最簡單的選擇。
2. 變基使我們的提交歷史變成直線,這意味著我們有個選項可以將我們的每一個 Git 提交轉換為一個 TFVC 變更集。 因為這種方式為其他選項留下了可能,所以我們推薦你這樣做;git-tf 可以很簡單地通過?`git tf pull --rebase`?幫你達成目標。
這是你的選擇。 在本例中,我們會進行變基:
~~~
$ git rebase FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
Team Project Creation Wizard
~~~
現在我們準備好生成一個檢入來推送到 TFVC 服務器上了。 Git-tf 給你一個將自上次修改(即?`--shallow`?選項,默認啟用)以來所有的修改生成的一個單獨的變更集以及為每一個 Git 提交(`--deep`)生成的一個新的變更集。 在本例中,我們將會創建一個變更集:
~~~
$ git tf checkin -m 'Updating readme and code'
Username: domain\user
Password:
Connecting to TFS...
Checking in to $/myproject: 100%, done.
Checked commit 5a0e25e in as changeset 35348
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, tag: TFS_C35348, origin_tfs/tfs, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
Team Project Creation Wizard
~~~
那有一個新標簽?`TFS_C35348`,表明 TFVC 已經存儲了一個相當于?`5a0e25e`?提交的快照。 要重點注意的是,不是每一個 Git 提交都需要在 TFVC 中存在一個相同的副本;例如?`6eb3eb5`?提交,在服務器上并不存在。
這就是主要的工作流程。 有一些你需要考慮的其他注意事項:
* 沒有分支。 Git-tf 同一時間只能從一個 TFVC 分支創建一個 Git 倉庫。
* 協作時使用 TFVC 或 Git,而不是兩者同時使用。 同一個 TFVC 倉庫的不同 git-tf 克隆會有不同的 SHA-1 校驗和,這會導致無盡的頭痛問題。
* 如果你的團隊的工作流程包括在 Git 中協作并定期與 TFVC 同步,只能使用其中的一個 Git 倉庫連接到 TFVC。
#### 工作流程:`git-tfs`
讓我們使用 git-tfs 來走一遍同樣的情景。 這是我們在 Git 倉庫中?`master`?分支上生成的幾個新提交:
~~~
PS> git log --oneline --graph --all --decorate
* c3bd3ae (HEAD, master) update code
* d85e5a2 update readme
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 (tfs/default) Hello
* b75da1a New project
~~~
讓我們看一下在我們工作時有沒有人完成一些其它的工作:
~~~
PS> git tfs fetch
C19 = aea74a0313de0a391940c999e51c5c15c381d91d
PS> git log --all --oneline --graph --decorate
* aea74a0 (tfs/default) update documentation
| * c3bd3ae (HEAD, master) update code
| * d85e5a2 update readme
|/
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project
~~~
是的,那說明我們的同事增加了一個新的 TFVC 變更集,顯示為新的?`aea74a0`?提交,而`tfs/default`?遠程分支已經被移除了。
與 git-tf 相同,我們有兩種基礎選項來解決這個分叉歷史問題:
1. 通過變基來保持歷史是線性的。
2. 通過合并來保留改動。
在本例中,我們將要做一個 “深” 檢入,也就是說每一個 Git 提交會變成一個 TFVC 變更集,所以我們想要變基。
~~~
PS> git rebase tfs/default
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
PS> git log --all --oneline --graph --decorate
* 10a75ac (HEAD, master) update code
* 5cec4ab update readme
* aea74a0 (tfs/default) update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project
~~~
現在已經準備好通過檢入我們的代碼到 TFVC 服務器來完成貢獻。 我們這里將會使用?`rcheckin`?命令將 HEAD 到第一個?`tfs`?遠程分支間的每一個 Git 提交轉換為一個 TFVC 變更集(`checkin`?命令只會創建一個變更集,有些類似于壓縮 Git 提交)。
~~~
PS> git tfs rcheckin
Working with tfs remote: default
Fetching changes from TFS to minimize possibility of late conflict...
Starting checkin of 5cec4ab4 'update readme'
add README.md
C20 = 71a5ddce274c19f8fdc322b4f165d93d89121017
Done with 5cec4ab4b213c354341f66c80cd650ab98dcf1ed, rebasing tail onto new TFS-commit...
Rebase done successfully.
Starting checkin of b1bf0f99 'update code'
edit .git\tfs\default\workspace\ConsoleApplication1/ConsoleApplication1/Program.cs
C21 = ff04e7c35dfbe6a8f94e782bf5e0031cee8d103b
Done with b1bf0f9977b2d48bad611ed4a03d3738df05ea5d, rebasing tail onto new TFS-commit...
Rebase done successfully.
No more to rcheckin.
PS> git log --all --oneline --graph --decorate
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project
~~~
注意在每次成功檢入到 TFVC 服務器后,git-tfs 是如何將剩余的工作變基到服務器上。 這是因為它將?`git-tfs-id`?屬性加入到提交信息的底部,這將會改變 SHA-1 校驗和。 這恰恰是有意設計的,沒有什么事情可以擔心了,但是你應該意識到發生了什么,特別是當你想要與其他人共享 Git 提交時。
TFS 有許多與它的版本管理系統整合的功能,比如工作項目、指定審核者、封閉檢入等等。 僅僅通過命令行工具使用這些功能來工作是很笨重的,但是幸運的是 git-tfs 允許你輕松地運行一個圖形化的檢入工具:
~~~
PS> git tfs checkintool
PS> git tfs ct
~~~
它看起來有點像這樣:

Figure 9-3.?git-tfs 檢入工具。
對 TFS 用戶來說這看起來很熟悉,因為它就是從 Visual Studio 中運行的同一個窗口。
Git-tfs 同樣允許你從你的 Git 倉庫控制 TFVC 分支。 如同這個例子,讓我們創建一個:
~~~
PS> git tfs branch $/tfvc-test/featureBee
The name of the local branch will be : featureBee
C26 = 1d54865c397608c004a2cadce7296f5edc22a7e5
PS> git log --oneline --graph --decorate --all
* 1d54865 (tfs/featureBee) Creation branch $/myproject/featureBee
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project
~~~
在 TFVC 中創建一個分支意味著增加一個使分支存在的變更集,這會映射為一個 Git 提交。 也要注意的是 git-tfs?**創建**?了?`tfs/featureBee`?遠程分支,但是?`HEAD`?始終指向?`master`。 如果你想要在新生成的分支上工作,那你也許應該通過從那次提交創建一個特性分支的方式使你新的提交基于?`1d54865`?提交。
#### Git 與 TFS 總結
Git-tf 與 Git-tfs 都是與 TFVC 服務器交互的很好的工具。 它們允許你在本地使用 Git 的能力,避免與中央 TFVC 服務器頻繁交流,使你做為一個開發者的生活更輕松,而不用強制整個團隊遷移到 Git。 如果你在 Windows 上工作(那很有可能你的團隊正在使用 TFS),你可能會想要使用 git-tfs,因為它的功能更完整,但是如果你在其他平臺工作,你只能使用略有限制的 git-tf。 像本章中大多數工具一樣,你應當使用其中的一個版本系統作為主要的,而使用另一個做為次要的 - 不管是 Git 還是 TFVC 都可以做為協作中心,但不是兩者都用。
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令