UML软件工程组织

 

 

CVS中的分支与合并
 
2008-01-18 来源:cnitblog.com
 

cvs 允许你把修改隔离在各自的开发线上,这就是分支(branch)。当你改变一个分支中的文件时,这些更改不会出现在开发主干(main trunk)和其它分支中。

在这之后你可以使用 merging 把这些变更从一个分支移动到另一个分支(或主干)。合并首先使用 cvs update -j 命令,将这些变更合并到工作目录。然后你可以提交这个版本,这样也可以将这些变更作用于其它的分支。

1.分支适用于什么情况

假定 tc 的发行版 1.0 已完成。你正在继续开发 tc,计划在 2 个月后创建发行 1.1 的版本。不久你的客户开始抱怨说代码有些问题。你检出了 1.0 的发行版,找到了这个错误(这将会有一个小小的更正)。但是,当前代码的版本是处在一个不稳的状态,并且在下一个月才能有希望稳定下来。这样就没法基于最新源代码去发行一个修复错误的版本。

这种情况下就可以去为所有构成 tc 的 1.0 发行版文件创建版本树的一个分支(branch)。然后你可以修改这分支而不影响到主干。当修订完成时,你可以选定是否要把它同主干合并或继续保留在这个分支里。

2.建立一个分支

使用 tag -b 去建立一个分支;例如,假定你现在有一个工作副本:

     $ cvs tag -b rel-1-0-patches

这将基于工作副本的当前版本分离出一个分支,并分配 `rel-1-0-patches' 名字给该分支。

有一点对理解分支很重要,分支是在 CVS 仓库中创建,而非在工作副本中创建。正如上面的例子,基于当前版本创建一个分支不会自动把当前的工作副本切换到新的分支上。

使用 rtag 命令可以不参考任何工作副本而创建一个分支:

     $ cvs rtag -b -r rel-1-0 rel-1-0-patches tc

`-r rel-1-0' 说明这个分支是基于有 `rel-1-0' 这个标签的文件。这不是从最新的版本建立分支 – 对需要从老的版本分出一个分支很有用(例如,给以前一个认为稳定的发行版改 bug)。

跟使用 `tag' 命令一样,这个 `-b' 标志告诉 rtag 去创建一个分支(而不只是这个版本的符号连接)。注意,标记 `rel-1-0' 可能对不同的文件有不同的版本数字。

因此,这个命令的结果是为 `tc' 模块建立了一个命名为 `rel-1-0-patches' 的新版本分支,它是基于标记为 `rel-1-0' 的版本树。

3.访问分支

你可以通过两种方式恢复分支:重新从仓库检出一份或是从现有的工作副本切换过去。

为了从仓库检出i一个分支,使用 `checkout' 命令并带上 `-r' 标志,后面是这个分支的标签(tag)名:
     $ cvs checkout -r rel-1-0-patches tc

或者如果你已有了一个工作副本,你可以使用 `update -r' 命令切转到这个分支:

     $ cvs update -r rel-1-0-patches tc

或者使用另一个等效的命令:

     $ cd tc
$ cvs update -r rel-1-0-patches

这对工作副本为主干代码或是其它分支都是有效的 – 上面的命令将把它切换到指名的分支。同 `update' 命令相类似,`update -r' 合并你所做的任何改变,通知你出现的冲突。

一旦你的工作副本已经转向一个特定的分支,它将一直保持在这个分支内,除非你又做了其它的操作。这意味着从这个工作副本提交的变更将加到这个分支的新版本中,而不影响到主干版本和其它分支。

想看一个工作副本是基于哪一个分支,可以使用 `status' 命令。在它们输出中查找一个 `Sticky tag' 的域
– 那就是 cvs 告诉你当前工作文件分支号的方式:

     $ cvs status -v driver.c backend.c
===================================================================
File: driver.c          Status: Up-to-date
Version:            1.7     Sat Dec  5 18:25:54 1992
RCS Version:        1.7     /u/cvsroot/yoyodyne/tc/driver.c,v
Sticky Tag:         rel-1-0-patches (branch: 1.7.2)
Sticky Date:        (none)
Sticky Options:     (none)
Existing Tags:
rel-1-0-patches             (branch: 1.7.2)
rel-1-0                     (revision: 1.7)
===================================================================
File: backend.c         Status: Up-to-date
Version:            1.4     Tue Dec  1 14:39:01 1992
RCS Version:        1.4     /u/cvsroot/yoyodyne/tc/backend.c,v
Sticky Tag:         rel-1-0-patches (branch: 1.4.2)
Sticky Date:        (none)
Sticky Options:     (none)
Existing Tags:
rel-1-0-patches             (branch: 1.4.2)
rel-1-0                     (revision: 1.4)
rel-0-4                     (revision: 1.4)

请不要因为每个文件的分支号不同(`1.7.2' 和 `1.4.2')而迷惑。分支的标签是相同的,`rel-1-0-patches',所以这些文件是在相同的分支上。数字简单地反映在每个文件的版本历史中在制造分支的点。在以上的例子中,分支建立之前,`driver.c' 比 `backend.c' 有更多的变更,因此它们的版本编号是不同的。

4.分支与修订版

 通常,一个文件的修订版本历史是一个增长线:

            +-----+    +-----+    +-----+    +-----+    +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 !
+-----+    +-----+    +-----+    +-----+    +-----+

然而,cvs 并不局限于线性的开发。版本树(revision tree) 可以分为不同的分支(branches),每一个分支可以是一个独立的自我维护的开发线。而在一个分支中的变更可以很容易地移回到主干中。

每一个分支均有一个分支号(branch number),由奇数个“.”分开的十进制数组成。把一个整数追加到对应分支赖以分离出的版本号上来创建分支号。使用分支号允许从一个特定版本分离出多个分支。

所有的分支上的版本都把序号追加到分支号上来构成版本号。下面的例子将展示这一点。

+-------------+
Branch 1.2.2.3.2 ->        ! 1.2.2.3.2.1 !
/ +-------------+
/
/
+---------+    +---------+    +---------+
Branch 1.2.2 -> _! 1.2.2.1 !----! 1.2.2.2 !----! 1.2.2.3 !
/ +---------+    +---------+    +---------+
/
/
+-----+    +-----+    +-----+    +-----+    +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 !  <- The main trunk
+-----+    +-----+    +-----+    +-----+    +-----+
!
!
!   +---------+    +---------+    +---------+
Branch 1.2.4 -> +---! 1.2.4.1 !----! 1.2.4.2 !----! 1.2.4.3 !
+---------+    +---------+    +---------+

虽然如何创建具体分支号的细节通常不是你需要考虑的,但这里是它如何工作的:当 cvs 创建一个分支号的时候它取一个未用的偶整数,用 2 开始。这样当你想从 6.4 的版本创建分支时分支号将为 6.4.2。以零结尾的所有分支号(如 6.4.0)被 cvs 内部使用。分支 1.1.1 有特别的含义。

5.魔术分支号

 这一节描述 cvs 的魔术分支(magic branches)特性。在大多数情况下,你不用考虑魔术分支号,cvs 将为你进行处理。然而,在一些特定条件下,它将显现出来,因此理解它如何工作将是有用的。

外表上,分支号码将由奇数个"."分隔的十进制整数组成。 然而那并非完全是这样的。由于效率的原因,cvs 有时插入一个额外的“0”在右末的第二个位置(1.2.4 变为 1.2.0.4,8.9.10.11.12 变为 8.9.10.11.0.12 等)。

cvs 将会很好的将这些称为魔术分支隐蔽在背后进行,但在一些地方这种隐蔽并不完全:

  • 魔术分支号会出现在 cvs log 的输出中。
  • 你不能够对 cvs admin 指定符号分支名。

你可以使用 admin 命令去为一个分支重新分配一个 rcs 希望的那样的符号名。如果 R4patches 是一个分配给分支 1.4.2(魔术分支号为 1.4.0.2) 的一个 numbers.c 文件的命名,你可以使用如下命令:

     $ cvs admin -NR4patches:1.4.2 numbers.c

至少有一个版本已经提交到这个分支时它才会有效。请非常小心不要把一个标签(tag)分配给了一个错误标识号。(现在没法看到昨天的一个标签是如何分配的)。

6.合并一整个分支

你可以把另一个分支上的修改合并到你的工作副本,只要在 update 子命令中加 `-j branchname' 的标志。使用一个 `-j branchname' 选项,它把在分支的最大公共祖先(GCA)和目的修订版之间所做的改变(在下面简单的情况下 GCA 是分支在那里分岔的点)和在那个分支上的最新修订版合并进你的工作副本。

`-j' 的意思是“join”。

Consider this revision tree:

     +-----+    +-----+    +-----+    +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !      <- The main trunk
+-----+    +-----+    +-----+    +-----+
!
!
!   +---------+    +---------+
Branch R1fix -> +---! 1.2.2.1 !----! 1.2.2.2 !
+---------+    +---------+

分支 1.2.2 分配了一个 `R1fix' 的标签(符号名)。下面的例子假定模块 `mod' 只包含一个文件 m.c

     $ cvs checkout mod               # Retrieve the latest revision, 1.4
$ cvs update -j R1fix m.c        # Merge all changes made on the branch,
# i.e. the changes between revision 1.2
# and 1.2.2.2, into your working copy
# of the file.
$ cvs commit -m "Included R1fix" # Create revision 1.5.

在合并时可能会发生冲突。如果这种情况发生,你应该在提交新版本之前解决它。如果你的原文件中包含关键字,你可能会得到比严格意义上的冲突更多的冲突信息。去了解如何避免这个问题。

checkout 命令也支持使用 `-j branchname' 标志。下面的例子同上面所用的例子有相的效果:

     $ cvs checkout -j R1fix mod
$ cvs commit -m "Included R1fix"

注意使用 update -j tagname 也许行但结果可能不是你想要的。

7.从一个分支多次合并

继续我们的例子,现在版本树看起来是这样的:

     +-----+    +-----+    +-----+    +-----+    +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 !   <- The main trunk
+-----+    +-----+    +-----+    +-----+    +-----+
!                           *
!                          *
!   +---------+    +---------+
Branch R1fix -> +---! 1.2.2.1 !----! 1.2.2.2 !
+---------+    +---------+

正如上面所讨论的,星号线表示从 `R1fix' 分支到主干的合并。

现在我们继续开发 `R1fix' 分支:

     +-----+    +-----+    +-----+    +-----+    +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 !   <- The main trunk
+-----+    +-----+    +-----+    +-----+    +-----+
!                           *
!                          *
!   +---------+    +---------+    +---------+
Branch R1fix -> +---! 1.2.2.1 !----! 1.2.2.2 !----! 1.2.2.3 !
+---------+    +---------+    +---------+

然后你可能会希望合并新的变更到主干中去。如果你仍使用 cvs update -j R1fix m.c, cvs 将试图合并你已经合并过的东西,这可能导致一些不希望发生的副作用。

因此,你必须指定你只希望把未被合并分支的改变合并进树干。这样需要使用两个 `-j' 参数,cvs 合并从第一个版本到第二个版本的变化。例如,在这种情况下最简单的方法应该是

     cvs update -j 1.2.2.2 -j R1fix m.c    # Merge changes from 1.2.2.2 to the
# head of the R1fix branch

用此法的问题是你需要手工指定 1.2.2.2 的版本号。一个更好的方法是使用最后完成合并的日期:

     cvs update -j R1fix:yesterday -j R1fix m.c

在每一次合并进树干后加一个标签给 R1fix 分支,然后就可以使用该标签为以后的合并的方式也挺好:

     cvs update -j merged_from_R1fix_to_trunk -j R1fix m.c

8.合并任何两个版本的差异

使用两个 `-j revision' 标志,update(和 checkout)命令能把两个任意不同的版本的差异合并进你的工作文件。

     $ cvs update -j 1.5 -j 1.3 backend.c

将把 1.5 版本恢复到 1.3 版本。注意修订版本的次序!

如果你在操作多个文件时使用这个选择项,你必须了解在不同的文件之间,版本的数字可能是完全不同的。你几乎总是使用符号标签而不是使用版本号来完成多个文件的操作。

使用两个 `-j' 操作也能恢复增加或删除的文件。例如,假定你有一个叫 file1 的文件存在于 1.1 版本中,然后你删除了它(因此增加了一个 dead 版本 1.2)。现在你又打算增加它,并用它原先的内容。下面是如何操作的:

     $ cvs update -j 1.2 -j 1.1 file1
U file1
$ cvs commit -m test
Checking in file1;
/tmp/cvs-sanity/cvsroot/first-dir/file1,v  <--  file1
new revision: 1.3; previous revision: 1.2
done
$

9.合并能添加或删除文件

如果你在合并时做的改变涉及到添加或删除一些文件,update -j 将反映这些变化。

例如:

     cvs update -A
touch a b c
cvs add a b c ; cvs ci -m "added" a b c
cvs tag -b branchtag
cvs update -r branchtag
touch d ; cvs add d
rm a ; cvs rm a
cvs ci -m "added d, removed a"
cvs update -A
cvs update -jbranchtag

在执行这些命令且 `cvs commit'一wancheng 完成之后,文件 a 将被删除,而文件 d 将被加入到主分支。

注意,当用静态标签(`-j tagname')而不是动态标签(`-j branchname') 从一个分支合并改变时,cvs 一般不会删除文件,因为 cvs 不会自动给 dead 版本添加静态标签。除非静态标签是手工添加到 dead 版本上的。使用分支标签从分支合并所有改变或使用两个静态标签作为合并端点合并都会在合并中将企图的修改传播开。

10.合并与关键字

如果你要合并的文件中包含关键字,你会得到一大堆冲突,这是因为关键字与合并的版本关联。

因此,你需要在合并的命令行里面指定 `-kk'开关。用只替换关键字名而不是其展开的值的办法,这个选项确保正合并的版本互相是是相同的,从而避免产生假的冲突。

例如,假定你有一个这样的文件:

            +---------+
_! 1.1.2.1 !   <-  br1
/ +---------+
/
/
+-----+    +-----+
! 1.1 !----! 1.2 !
+-----+    +-----+

你的工作目录当前在树干上(版本 1.2)。合并时会得到下面的信息:

     $ cat file1
key $Revision: 1.2 $
. . .
$ cvs update -j br1
U file1
RCS file: /cvsroot/first-dir/file1,v
retrieving revision 1.1
retrieving revision 1.1.2.1
Merging differences between 1.1 and 1.1.2.1 into file1
rcsmerge: warning: conflicts during merge
$ cat file1
<<<<<<< file1
key $Revision: 1.2 $
=======
key $Revision: 1.1.2.1 $
>>>>>>> 1.1.2.1
. . .

产生这些信息是由于合并尝试将 1.1 与 1.1.2.1 之间的差异合并到你的工作目录。因为版本关键字从 Revision: 1.1 变为 Revision: 1.1.2.1,cvs 试图把该改变合并进你的工作目录,而与你的工作目录里已包含 Revision: 1.2 的事实冲突。

下面是使用 `-kk' 后的结果:

     $ cat file1
key $Revision: 1.2 $
. . .
$ cvs update -kk -j br1
U file1
RCS file: /cvsroot/first-dir/file1,v
retrieving revision 1.1
retrieving revision 1.1.2.1
Merging differences between 1.1 and 1.1.2.1 into file1
$ cat file1
key $Revision$
. . .

这时在文件中 1.1 和 1.1.2.1 版本的关键字双双扩展为明码 Revision,因此把它们之间的改变合并进工作目录不需要改变什么。也就不会有冲突产生。

警告: 在 cvs 1.12.2 之前的版本中,合并时使用 `-kk' 会有严重的问题。那就是 `-kk' 会跨越任何仓库中归档文件中设置的默认关键字扩展模式。对一些用户很不幸,这会造成二进制文件(默认关键字扩展模式设置为 `-kb')的损坏。因此,当仓库中包含二进制文件时,解决冲突必须使用手工修改的方法来替代合并命令中的 `-kk'。

cvs 1.12.2 之后的版本中,命令行对任何 cvs 命令提供的关键字扩展模式不再跨越为二进制文件设置的 `-kb' 关键字扩展模式,然而它还是将跨越其它默认关键字扩展模式。现在即使仓库中包含二进制文件,你也可以在合并中安全地使用 `-kk' 来防止含有 RCS 关键字行的假性冲突。

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号