0
回答
面向 Subversion 用户的 Git,第 2 部分: 实施控制
滴滴云服务器,限时包月0.9元,为开发者而生>>>   

Git 为 Linux® 开发人员提供了大量优于 Subversion 的软件版本控制特性,因此协作开发项目的开发人员认为有必要了解它背后的理念。在本期文章中,Ted 将讨论 Git 和 Subversion 中的分支和合并,介绍了用于将修改分为两部分的 “git bisect”,并展示了如何解决合并冲突。

这是共分两部分的系列文章的第二篇。如果还没有阅读第一篇的话,您应当先阅读 面向 Subversion 用户的 Git,第 1 部分:入门指南,因为我将使用与第一部分相同的 Git 和 Subversion (SVN) 设置,并且它将使您习惯我的幽默感。

在 SVN 中执行分支和合并

显然,最让版本控制系统(VCS)管理员感动头痛的问题就是分支合并。大部分开发人员倾向于在 trunk 中提交他们的所有修改。当发生分支和合并时,开发人员就开始抱怨,而 VCS 管理员则开始解决麻烦。

公平地讲,对开发人员来说,分支和合并是令人感到畏惧的操作。操作结果并不是总是很明显,而合并则会因为撤销其他人的工作而引起问题。

SVN 可以很好地管理 trunk,因此许多开发人员没有必要使用分支。1.5 版本之前的 SVN 客户机在跟踪合并方面有一些简单,因此如果您习惯于旧版本的 SVN 客户机的话,那么您可能不清楚 SVN 的 svn:mergeinfo 属性。

还有一个名为 svnmerge.py的出色工具。svnmerge.py 可以在没有 svn:mergeinfo 支持的情况下跟踪合并,因此可以用于较旧版本的 SVN 客户机。

由于 SVN 对合并支持的复杂性和变化性,我不会提供具体的示例。相反,我们将只关注 Git 的分支合并。

Git 中的分支和合并

谈到分支和合并,如果将 Concurrent Versions System (CVS) 比喻成一个呆头呆脑的乡巴佬,那么 SVN 就相当于教区牧师,而 Git 则是市长。Git 实际上被设计为支持简单的分支和合并。这一 Git 特性不仅仅体现在演示中,也体现在日常使用中。

举例而言,Git 具有多个合并策略,包括一个名为 octopus 的策略,允许您一次性合并多个分支。多棒的策略!想象一下在 CVS 或 SVN 中执行这种合并会有多痛苦。Git 还支持一种名为 rebasing 的合并。这里不会详细介绍 rebasing,但是它对于简化存储库历史非常有用,因此您可能需要详细了解它。

在继续下面的合并示例之前,您应当首先熟悉 面向 Subversion 用户的 Git,第 1 部分:入门指南 中的分支步骤。您拥有 HEAD(当前分支,在本 例中为 master)和 empty-gdbinit 分支。首先,让我们将 empty-gdbinit 合并到 HEAD,然后对 HEAD 作出修改,并以其他方式将其合并到 empty-gdbinit


清单 1. 使用 Git 将来自分支的修改合并到 HEAD

				
# start clean
% git clone git@github.com:tzz/datatest.git
# ...clone output...
# what branches are available?
% git branch -a
#* master
# origin/HEAD
# origin/empty-gdbinit
# origin/master
# do the merge
% git merge origin/empty-gdbinit
#Updating 6750342..5512d0a
#Fast forward
# gdbinit | 1005 ---------------------------------------------------------------
# 1 files changed, 0 insertions(+), 1005 deletions(-)
# now push the merge to the server
% git push
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# 6750342..5512d0a master -> master

 

只要您意识到 master 具有 HEAD,那么就可以轻松完成合并,当完成与 empty-gdbinit 分支的合并后,master 分支被推入到远程服务器,与 origin/master 进行同步。换句话说,您从一个远程分支中执行本地合并,然后将结果推到另一个远程分支中。

这里的关键一点在于理解 Git 为什么不关心分支是否可靠。您可以从一个本地分支合并到另一个本地分支,或合并到一个远程分支。Git 服务器只参与到远程操作。与此相反,SVN 始终需要使用 SVN 服务器,因为对于 SVN 来说,服务器上的存储库是惟一可靠的版本。

当然,Git 是一个分布式 VCS,因此没有什么好惊讶的。它的设计目标就是在没有中央权限的情况下工作。但是,对于习惯 CVS 和 SVN 的开发人员说,这种自由会令人感到有些不习惯。

现在,了解了以上内容后,让我们执行另一个本地分支:


清单 2. 创建并切换到机器 A 上的 release 分支

				
# create and switch to the stable branch
% git checkout -b release-stable
#Switched to a new branch "release-stable"
% git branch
# master
#* release-stable
# push the new branch to the origin
% git push --all
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# * [new branch] release-stable -> release-stable

 

现在,在一台不同的机器上,我们将把 gdbinit 文件从主分支中移走。当然,不一定必须使用不同的机器,也可以使用一个不同的目录,但是我为机器 B 重用了 面向 Subversion 用户的 Git,第 1 部分:入门指南 的 Ubuntu 的 “The Other Ted” 身份。


清单 3. 在机器 B 上从主分支中删除 gdbinit

				
# start clean
% git clone git@github.com:tzz/datatest.git
# ...clone output...
% git rm gdbinit
# rm 'gdbinit'
# hey, what branch am I in?
% git branch
#* master
# all right, commit my changes
% git commit -m "removed gdbinit"
#Created commit 259e0fd: removed gdbinit
# 1 files changed, 0 insertions(+), 1 deletions(-)
# delete mode 100644 gdbinit
# and now push the change to the remote branch
% git push
#updating 'refs/heads/master'
# from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
# to 259e0fda9a8e9f3b0a4b3019781b99a914891150
#Generating pack...
#Done counting 3 objects.
#Result has 2 objects.
#Deltifying 2 objects...
# 100% (2/2) done
#Writing 2 objects...
# 100% (2/2) done
#Total 2 (delta 1), reused 0 (delta 0)

 

这里没有什么神秘的(除了 “deltifying”,听上去像是健身房所做的一项运动,或者是与临近大片水域的河流有关)。但是机器 A 在 release-stable 分支中发生了什么呢?


清单 4. 将从主分支删除 gdbinit 的操作合并到机器 A 上的 release-stable 分支

				
# remember, we're in the release-stable branch
% git branch
# master
#* release-stable
# what's different vs. the master?
% git diff origin/master
#diff --git a/gdbinit b/gdbinit
#new file mode 100644
#index 0000000..8b13789
#--- /dev/null
#+++ b/gdbinit
#@@ -0,0 +1 @@
#+
# pull in the changes (removal of gdbinit)
% git pull origin master
#From git@github.com:tzz/datatest
# * branch master -> FETCH_HEAD
#Updating 5512d0a..259e0fd
#Fast forward
# gdbinit | 1 -
# 1 files changed, 0 insertions(+), 1 deletions(-)
# delete mode 100644 gdbinit
# push the changes to the remote server (updating the remote release-stable branch)
% git push
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# 5512d0a..259e0fd release-stable -> release-stable

 

我在 面向 Subversion 用户的 Git,第 1 部分:入门指南 中引用的 mentat 接口再一次出现在 diff 中。您应该知道 /dev/null 是一个没有包含任何内容的特殊文件,因此远程主分支也为空,而本地 release-stable 分支包含 gdbinit 文件。这对于大多数用户来说并不总是很明显。

除此以外,pull 将本地分支与 origin/master 合并,随后 push 使用修改更新 origin/release-stable。和往常一样,“delta” 是 Git 开发人员最喜欢的单词 — 永远不会错过使用它的机会。

将修改分为两部分

我在此不会详细介绍 git bisect 命令,因为它十分复杂,但是在此需要提到它,因为这是一个非常棒的工具。将修改平分为两部分实际上就是在整个 commit 日志中执行一个二进制搜索。“Binary” 意味着搜索从中间分割搜索区间并每次检查中间部分,以确定所需的片段是在中间部分以上,还是在中间部分以下。

它的工作原理很简单。您告诉 Git 版本 A 是好的,而版本 Z 是差的。Git 随后会询问您(或询问一个自动化脚本)位于 A 和 Z 之间的版本,比如说 Q,是否是坏的。如果 Q 是坏的,那么坏的提交就介于 A 和 Q 之间;否则,坏提交介于 Q 和 Z 之间。该过程将一直重复,直到找到坏提交。

特别好的一点是,可以使用一个测试脚本自动实现这种等分。这使得可以为版本 Z 编写一个测试并反向使用它查找特性何时崩溃,大多数开发人员将之称为自动回归测试。这 节省您的时间。

解决冲突

对于任何 VCS,特别是 Git 之类的分布式 VCS 来说,合并冲突是不可避免的。如果两个人在同一个分支中以相互冲突的方式修改一个文件,会发生什么?下面的两个例子都出现在我们到目前为止一直使用的数据 测试存储库的主分支中。

首先,我们对机器 B 上的 encode.pl 作出修改:


清单 5. 机器 B 上的 “Does not work”

				
# we're at time T1
# change the contents
% echo "# this script doesn't work" > encode.pl
% git commit -a -m 'does not work'
#Created commit e61713b: does not work
# 1 files changed, 1 insertions(+), 1 deletions(-)
# we're at time T2 now, what's our status?
% git status
# On branch master
#nothing to commit (working directory clean)

 

现在在未察觉到机器 B 上的修改的情况下对机器 A 上的 encode.pl 作出修改,然后执行 push


清单 6. 机器 A 上的 “Does work”

				
# we're at time T2
# change the contents
% echo "this script does work" > encode.pl
% git commit -a -m 'does not work'
#Created commit e61713b: does not work
# 1 files changed, 1 insertions(+), 1 deletions(-)
# we're at time T3 now, what's our status?
% git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
#nothing to commit (working directory clean)
% git push
#Counting objects: 5, done.
#Delta compression using 2 threads.
#Compressing objects: 100% (2/2), done.
#Writing objects: 100% (3/3), 298 bytes, done.
#Total 3 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# 259e0fd..f949703 master -> master

 

现在,在机器 B 上,执行一个 git pull 命令,会发现事情并不是很顺利:


清单 7. 机器 B 上的 Uh-oh

				
% git pull
#remote: Counting objects: 5, done.
#Compressing objects: 100% (2/2), done.)
#remote: Total 3 (delta 0), reused 0 (delta 0)
#Unpacking 3 objects...
# 100% (3/3) done
#* refs/remotes/origin/master: fast forward to branch 'master'
# of git@github.com:tzz/datatest
# old..new: 259e0fd..f949703
#Auto-merged encode.pl
#CONFLICT (content): Merge conflict in encode.pl
#Automatic merge failed; fix conflicts and then commit the result.
# the next command is optional
% echo uh-oh
#uh-oh
# you can also use "git diff" to see the conflicts
% cat encode.pl
#<<<<<<< HEAD:encode.pl
## this script doesn't work
#=======
#this script works
#>>>>>>> f9497037ce14f87ff984c1391b6811507a4dd86c:encode.pl

 

这种情况在 SVN 上也非常普遍。某些人的修改与您的文件版本也不一致。只需编辑文件并提交:


清单 8. 在机器 B 上修复并提交

				
# fix encode.pl before this to contain only "# this script doesn't work"...
% echo "# this script doesn't work" > encode.pl
# commit, conflict resolved
% git commit -a -m ''
#Created commit 05ecdf1: Merge branch 'master' of git@github.com:tzz/datatest
% git push
#updating 'refs/heads/master'
# from f9497037ce14f87ff984c1391b6811507a4dd86c
# to 05ecdf164f17cd416f356385ce8f5c491b40bf01
#updating 'refs/remotes/origin/HEAD'
# from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
# to f9497037ce14f87ff984c1391b6811507a4dd86c
#updating 'refs/remotes/origin/master'
# from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
# to f9497037ce14f87ff984c1391b6811507a4dd86c
#Generating pack...
#Done counting 8 objects.
#Result has 4 objects.
#Deltifying 4 objects...
# 100% (4/4) done
#Writing 4 objects...
# 100% (4/4) done
#Total 4 (delta 0), reused 0 (delta 0)

 

非常简单,不是吗?让我们看看机器 A 在下一次更新时会发生什么。


清单 9. 在机器 A 上修复和提交

				
% git pull
#remote: Counting objects: 8, done.
#remote: Compressing objects: 100% (3/3), done.
#remote: Total 4 (delta 0), reused 0 (delta 0)
#Unpacking objects: 100% (4/4), done.
#From git@github.com:tzz/datatest
# f949703..05ecdf1 master -> origin/master
#Updating f949703..05ecdf1
#Fast forward
# encode.pl | 2 +-
# 1 files changed, 1 insertions(+), 1 deletions(-)
% cat encode.pl
## this script doesn't work

 

Fast forward 表示本地分支会自动与远程分支同步,因为它没有包含远程分支中所没有的内容。换而言之,fast forward 表示不需要执行合并;所有本地文件不会比远程分支的最近一次推入更新。

最后,我应当提一下 git revertgit reset,这两者对于撤销对 Git 树的提交或其他更改非常有用。本文篇幅有限,因此不做解释,但是您一定要知道如何使用它们。

结束语

本文解释了合并的概念,展示了在两台机器上保持本地和远程分支并解决两者之间的冲突。我还关注了复杂的甚至是神秘的 Git 消息,因为与 SVN 相比较,Git 要更加详细和难于理解。当这一点与 Git 命令的复杂语法相结合时,大多数初学者都会对 Git 望而却步。然而,一旦了解了一些基本的概念,Git 就会变得简单许多 — 甚至令人愉悦!

举报
红薯
发帖于8年前 0回/821阅
顶部