==== Git 与 TFS

(((Interoperation with other VCSs, TFS)))
(((TFS)))((("TFVC", see="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-tf 与 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[]）是一个 Java 项目，因此它可以运行在任何一个有 Java 运行时环境的电脑上。
它通过 JGit（一个 Git 的 JVM 实现）来与 Git 仓库交互，这意味着事实上它没有 Git 功能上的限制。
然而，相对于 git-tfs 它对 TFVC 的支持是有限的——例如，它不支持分支。

所以每个工具都有优点和缺点，每个工具都有它适用的情况。
我们在本书中将会介绍它们两个的基本用法。

[NOTE]
====
你需要有一个基于 TFVC 的仓库来执行后续的指令。
现实中它们并没有 Git 或 Subversion 仓库那样多，所以你可能需要创建一个你自己的仓库。
Codeplex (https://www.codeplex.com[]) 或 Visual Studio Online (http://www.visualstudio.com[]) 都是非常好的选择。
====


===== 使用：`git-tf`

和其它任何 Git 项目一样，你要做的第一件事是克隆。
使用 `git-tf` 克隆看起来像这样：

[source,console]
----
$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main project_git
----

第一个参数是一个 TFVC 集的 URL，第二个参数类似于 `$/project/branch` 的形式，第三个参数是将要创建的本地 Git 仓库路径（最后一项可以省略）。
Git-tf 同一时间只能工作在一个分支上；如果你想要检入一个不同的 TFVC 分支，你需要从那个分支克隆一份新的。

这会创建一个完整功能的 Git 仓库：

[source,console]
----
$ cd project_git
$ git log --all --oneline --decorate
512e75a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Checkin message
----

这叫做 _浅_ 克隆，意味着只下载了最新的变更集。
TFVC 并未设计成为每一个客户端提供一份全部历史记录的拷贝，所以 git-tf 默认行为是获得最新的版本，这样更快一些。

如果愿意多花一些时间，使用 `--deep` 选项克隆整个项目历史可能更有价值。

[source,console]
----
$ 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 克隆行为略为不同。
观察：

[source,powershell]
----
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 仓库：

[source,powershell]
----
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 配置选项来避免一些问题。

[source,console]
----
$ git config set --local core.ignorecase=true
$ git config set --local core.autocrlf=false
----
====

显然，接下来要做的事情就是要在项目中做一些工作。
TFVC 与 TFS 有几个功能可能会增加你的工作流程的复杂性：

. TFVC 无法表示特性分支，这会增加一点复杂度。
  这会导致需要以 *非常* 不同的方式使用 TFVC 与 Git 表示的分支。
. 要意识到 TFVC 允许用户从服务器上“检出”文件并锁定它们，这样其他人就无法编辑了。
  显然它不会阻止你在本地仓库中编辑它们，但是当推送你的修改到 TFVC 服务器时会出现问题。
. TFS 有一个“封闭”检入的概念，TFS 构建-测试循环必须在检入被允许前成功完成。
  这使用了 TFVC 的“shelve”功能，我们不会在这里详述。
    可以通过 git-tf 手动地模拟这个功能，并且 git-tfs 提供了封闭敏感的 `checkintool` 命令。

出于简洁性的原因，我们这里介绍的是一种轻松的方式，回避并避免了大部分问题。

===== 工作流程：`git-tf`


假定你完成了一些工作，在 `master` 中做了几次 Git 提交，然后准备将你的进度共享到服务器。
这是我们的 Git 仓库：

[source,console]
----
$ 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 服务器。
先说重要的：让我们看看自从上次连接后我们的队友是否进行过改动：

[source,console]
----
$ 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 的优势，但是我们现在有两种处理的方式：

. 像一名 Git 用户一样自然的生成一个合并提交（毕竟，那也是 `git pull` 做的），git-tf 可以通过一个简单的 `git tf pull` 来帮你完成。
  然而，我们要注意的是，TFVC 却并不这样想，如果你推送合并提交那么你的历史在两边看起来都不一样，这会造成困惑。
  其次，如果你计划将所有你的改动提交为一次变更集，这可能是最简单的选择。
. 变基使我们的提交历史变成直线，这意味着我们有个选项可以将我们的每一个 Git 提交转换为一个 TFVC 变更集。
  因为这种方式为其他选项留下了可能，所以我们推荐你这样做；git-tf 可以很简单地通过 `git tf pull --rebase` 帮你达成目标。

这是你的选择。
在本例中，我们会进行变基：

[source,console]
----
$ 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`）生成的一个新的变更集。
在本例中，我们将会创建一个变更集：

[source,console]
----
$ 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` 分支上生成的几个新提交：

[source,powershell]
----
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
----

让我们看一下在我们工作时有没有人完成一些其它的工作：

[source,powershell]
----
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 相同，我们有两种基础选项来解决这个分叉历史问题：

. 通过变基来保持历史是线性的。
. 通过合并来保留改动。

在本例中，我们将要做一个“深”检入，也就是说每一个 Git 提交会变成一个 TFVC 变更集，所以我们想要变基。

[source,powershell]
----
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 提交）。

[source,powershell]
----
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 允许你轻松地运行一个图形化的检入工具：

[source,powershell]
----
PS> git tfs checkintool
PS> git tfs ct
----

它看起来有点像这样：

.git-tfs 检入工具。
image::images/git-tfs-ct.png[git-tfs 检入工具。]

对 TFS 用户来说这看起来很熟悉，因为它就是从 Visual Studio 中运行的同一个窗口。

Git-tfs 同样允许你从你的 Git 仓库控制 TFVC 分支。
如同这个例子，让我们创建一个：

[source,powershell]
----
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 都可以做为协作中心，但不是两者都用。
