什么是 GNU?

作者:Arnold Robbins

什么是 RCS?RCS 是版本控制系统(Revision Control System)。它的工作是管理文件修订和更新的过程。它可以并且应该用于程序文本和文档,以及任何其他频繁修订的文件。RCS 允许您检索文件的早期版本,同时保留更改的日志,谁进行了更改以及原因。RCS 使比较文件的任意两个版本变得容易,并提供了一种机制,用于合并源文件的两个不同开发“分支”的更改。

RCS 最初由 Purdue 大学的 Walter F. Tichy 博士编写。从 1983 年开始,随着它作为 4.2BSD 中用户贡献软件的一部分发布,它在 Unix 社区中得到了广泛使用。1985 年 7 月的“Software—Practice And Experience”杂志上的一篇文章对其进行了描述。

为什么要使用 RCS?

RCS 为软件开发人员提供了安全网。在开发、修复和改进程序时,更改是不可避免的。通过在 RCS 中保存文件的稳定版本,如果一组更改不起作用,您可以稍后返回到已知状态。

如果有多个人在同一个文件上工作,RCS 允许您“锁定”文件,以便只有一个人可以进行更改。其他人仍然可以使用该文件,例如,用于编译。

除了跟踪对文件所做的更改之外,RCS 还跟踪谁进行了更改以及何时进行更改。RCS 还记录描述更改的日志消息。这使得在最终隔离致命错误时,很容易找出是谁破坏了程序。

使用 RCS

用户界面有意地非常简单,主要由两个命令组成:cico。首先,创建一个目录来保存程序,然后 cd 进入该目录。然后创建一个名为 RCS 的目录。虽然不是必需的,但这是最干净的做法;所有 RCS 文件都将保存在 RCS 子目录中。我们还将创建程序的第一个版本。

$ mkdir hello
$ cd hello
$ mkdir RCS
$ cat > hello.c # editors are for wimps! :-)
#include <stdio.h>
int main(void)
{
   printf("hello, world\n");
}
^D
$ ls -l hello.c
-rw-r--r--  1 arnold       66 Nov  5 22:33 hello.c

我们现在有了一个 C 源文件,可以开始使用了。编译并运行后,它会打印出全世界 C 程序员喜爱的众所周知的友好问候语。

在确保它可以编译之后,首先要做的是使用 RCS “检入”程序。这是通过 ci 完成的。

$ ci hello.c
RCS/hello.c,v <-- hello.c
enter description, terminated with single `.' or end of file:
NOTE: This is NOT the log message!
> world famous C program that prints a friendly
message.
> .
initial revision: 1.1
done
$ ls
RCS

首次检入文件时,RCS 需要对文件进行描述。它提醒我们这不是日志消息,因此,像“初始版本”这样的描述在这里是不合适的。> 是信息提示符。另请注意,原始文件已被删除。RCS 已将文件安全地存储在 RCS 目录中的 RCS 文件 hello.c,v 中。

检出文件

嗯,一个我们无法编译的文件用处不大,所以接下来要做的是获取一个副本,以便我们实际上可以编译程序并使用它。这是通过 co 完成的,它代表“检出”(check out)。

$ co hello.c
RCS/hello.c,v -> hello.c
revision 1.1
done
$ ls -l hello.c
-r--r--r-- 1 arnold 66 Nov 5 22:43 hello.c
$ gcc hello.c -O -o hello; ./hello
hello, world

请注意,文件返回给我们,但具有只读权限。因此,我们被允许使用该文件,但不能更改它。在正常使用中,例如,要构建整个源树以安装软件,您将以只读方式检出文件,编译程序,然后删除源文件。

锁定文件

当您要更改文件时怎么办?程序确实会演变,那么您如何获得下一个版本呢?首先要做的是检出文件,但要锁定该文件。锁定表示您,且只有您,被允许检入文件的新版本。如果有多个人将使用源文件,这是必要的,这样两个人就不会互相破坏对方的工作。

$ co -l hello.c
RCS/hello.c,v -> hello.c
 revision 1.1 (locked)
done
$ ls -l hello.c
-rw-r--r-- 1 arnold         66 Nov  5 22:51 hello.c

这将检出文件并锁定它。请注意,权限现在允许写入文件。我们可以编辑文件,并对其进行更改。

$ sam hello.c   # a nifty editor,
# watch for a future column on it.
$ cat hello.c
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
   if (argc > 1 && strcmp(argv[1], "-advice") == 0) {
      printf("Don't Panic!\n");
      exit(42);
   }
   printf("hello, world\n");
   exit(0);
}
$ gcc -O hello.c -o hello
$ ./hello -advice
Don't Panic!
$ ./hello
hello, world

我们的程序现在有了一个新选项 -advice,它会打印一条友好的建议并以众所周知的特殊值退出。默认行为保持不变,只是现在也为正常情况使用了 exit

我们现在可以将新版本检入 RCS。假设我们希望对文件进行进一步的工作,ci 还允许我们使用 -l 选项。使用此选项,ci 将执行检入并自动为我们执行 co -l,以便我们可以继续处理该文件。

$ ci -l hello.c
RCS/hello.c,v <- hello.c
new revision: 1.2; previous revision: 1.1
enter log message, terminated with single `.' or end of file:
> Added -advice option, and made regular case use exit.
> .
done
$ ls -l hello.c
-rw-r--r-- 1 arnold        208 Nov 5 22:54 hello.c

在这里我们看到输入日志消息的位置。日志消息应该相对简洁,描述更改了什么以及原因。在商业环境中,您可能还会将与特定修复相关的错误编号输入到日志中。我们还看到该文件仍然可用于进一步编辑(权限 -rw-r--r--)。

比较文件的版本

您可以使用 rcsdiff 命令比较文件的任意两个版本。此命令接受常规 diff 命令的所有选项。但是,它占用了 -r 选项来提供版本号。默认情况下,rcsdiff 将工作文件的当前版本与最近检入的版本进行比较。使用一个 -r 选项,它会将当前文件与指定的先前版本进行比较。您可以提供两个 -r 选项的实例,使其比较两个不同的版本,这两个版本都不是当前版本。这是一个默认(也是最常见的)情况的示例

$ rcsdiff -c hello.c
==================================================
RCS file: RCS/hello.c,v
retrieving revision 1.1
diff -c -r1.1 hello.c
*** 1.1 1994/11/06 03:36:45
--- hello.c     1994/11/06 03:54:49
***************
*** 1,6 ****
   #include <stdio.h>
!  int main(void)
   {
        printf("hello, world\n");
--- 1,12 ----
   #include <stdio.h>
+  #include <string.h>
!  int main(int argc, char **argv)
   {
+       if (argc > 1 && strcmp(argv[1], "-advice") == 0) {
+           printf("Don't Panic!\n");
+           exit(42);
+       }
        printf("hello, world\n");
+       exit(0);
   }

这会生成一个“上下文差异”,显示更改内容的周围上下文,而不仅仅是更改本身。标有 ! 的行表示从旧版本到新版本的已更改行,标有 + 的行表示已添加的行。

rcsdiff 程序使生成可以使用 patch 应用的更新变得容易。当程序完成时,只需检入构成它的所有文件,并使用新的、更高级别的版本号,例如 3.0。然后,对于下一个版本,针对版本 3.0 对所有文件运行 rcsdiff。

$ rcsdiff -c -r3.0 RCS/* > myprog-3.0-4.0.patch 2>&1

(这没有捕捉到 4.0 中添加的全新文件的情况,或者 3.0 中删除的文件,但您明白了。)

作为旁注,为了构建和安装 RCS 软件,您需要拥有 GNU 版本的 diff。Linux 系统已经有了这个。如果您没有 GNU diff,您也应该获取它,因为它功能非常齐全,并且明显比标准 Unix 版本的 diff 快。

自动跟踪感兴趣的信息

通常能够查看源文件的内容并判断文件的版本很有用。RCS 允许您通过在检出文件时对文件的内容执行“关键字替换”来做到这一点。有大量的这些关键字;co 手册页完整地记录了它们。最常见的关键字是 $Id$$Log$

$Id$ 关键字被替换为描述文件名、版本、检出日期和时间、作者以及状态(例如,Exp,表示实验性)的文本。通常,这嵌入在 C 字符串常量中,以便可以使用 ident 命令识别从文件生成的二进制文件。

$Log$ 关键字被替换为最新日志消息的文本。这通常放在注释内,以便源文件是自文档化的,显示更改了什么以及何时更改。这很有用,但应谨慎使用:如果文件经常更改,则此日志可能会变得很大。

我们现在将添加关键字并显示文件在检入更改后的版本之前和之后的状态。

$ sam hello.c
$ cat hello.c
#include <stdio.h>
#include <string.h>
static const char rcsid[] = "$Id$";
/*
 * $Log$
 */
int main(int argc, char **argv)
{
   if (argc > 1 && strcmp(argv[1], "-advice") == 0) {
        printf("Don't Panic!\n");
        exit(42);
}
   printf("hello, world\n");
   exit(0);
}
$ ci -l hello.c
RCS/hello.c,v  <-  hello.c
new revision: 1.3; previous revision: 1.2
enter log message, terminated with single `.' or end of file:
>> add id and log keywords.
>> .
done
$ cat hello.c
#include <stdio.h>
#include <string.h>
static const char rcsid[] = "$Id: hello.c,v 1.3 1994/11/07 03:41:32
arnold Exp arnold $";
/*
 * $Log: hello.c,v $
 * Revision 1.3  1994/11/07  03:41:32  arnold
 * add id and log keywords.
 *
 */
int main(int argc, char **argv)
{
   if (argc > 1 && strcmp(argv[1], "-advice") == 0) {
        printf("Don't Panic!\n");
        exit(42);
}
   printf("hello, world\n");
   exit(0);
}

我们看到 RCS 已经填写了两个关键字的信息。当程序被编译时,ident 命令将为我们提供有关用于编译程序的所有文件中包含 RCS id 的信息。

$ gcc -O hello.c -o hello
$ ident hello
hello:
   $Id: hello.c,v 1.3 1994/11/07 03:41:32 arnold Exp arnold $
其他 RCS 命令

您可以使用 cicorcsdiff 完成几乎所有您需要做的事情。RCS 还附带了一些其他有趣的命令。

rcs 命令用于更改 RCS 文件的状态。特别是,它可以用于锁定未锁定的文件或解除其他人对 RCS 文件的锁定。后一种操作是危险的,仅应在紧急情况下进行。rcs 可以执行许多其他操作;有关详细信息,请参见手册页。

可以从开发主线(或“主干”)分出“分支”。例如,假设 hello.c 的发布版本是 2.6,版本 2.7 将是下一个发布版本。程序员 Mary 正在编写版本 2.7,而程序员 Joe 必须维护版本 2.6。通常,Joe 会从主开发主干上启动一个单独的分支,生成版本 2.6.1.1、2.6.1.2 等等。RCS 可以维护从主干分出的任意数量的分支,以及从分支分出的分支。但是,正如您可能想象的那样,跟踪多层分支可能会变得混乱。

在某个时候,Mary 会希望确保 Joe 的所有修复都已合并到她的 hello.c 版本中;她将使用 rcsmerge 来做到这一点。(rcsmerge 使用一个单独的程序,该程序也随 RCS 一起提供,名为 merge,它负责合并文件的实际工作。)

最后,rlog 命令将打印出特定源文件的所有日志消息。这使您可以查看文件的完整更改历史记录。

$ rlog hello.c
RCS file: RCS/hello.c,v
Working file: hello.c
head: 1.3
branch:
locks: strict
        arnold: 1.3
access list:
symbolic names:
comment leader: " * "
keyword substitution: kv
total revisions: 3;     selected revisions: 3
description:
world famous C program that prints a friendly message.
----------------------------
revision 1.3    locked by: arnold;
date: 1994/11/07 03:41:32;  author: arnold;  state: Exp;  lines: +6 -0
add id and log keywords.
----------------------------
 revision 1.2
date: 1994/11/07 03:40:21;  author: arnold;  state: Exp;  lines: +7 -1
Added -advice option, and made regular case use exit.
----------------------------
revision 1.1
date: 1994/11/07 03:38:50;  author: arnold;  state: Exp;
Initial revision
 ====================================================

rlog 打印出的初始内容大部分在 RCS 手册页中进行了解释。我们感兴趣的是输出的描述和日志消息部分,它们告诉我们程序是什么,进行了哪些更改,由谁以及何时进行。有趣的是,时间戳是 UTC,而不是本地时间。这是为了让不同时区的开发人员可以协作而不会在他们的 Id 字符串中出现差异。

RCS 未解决的问题

RCS 未解决的主要问题是多人同时处理文件以及发布管理的更大问题,即确保发布是完整和最新的。

为此目的提供了一个单独的软件包:cvs,并发版本系统(Concurrent Version System)。来自 cvs 发行版中的 README 文件

cvsrcs(1) 版本控制系统的前端,它将版本控制的概念从单个目录中的文件集合扩展到由版本控制文件组成的分层目录集合。这些目录和文件可以组合在一起以形成软件版本。cvs 提供了管理这些软件版本以及控制多个软件开发人员之间并发编辑源文件所需的功能。

您可以从 ftp.gnu.ai.mit.edu 的 /pub/gnu 获取 cvs。在撰写本文时,当前版本是 cvs-1.3.tar.gz。在您阅读本文时,CVS 1.4 可能已经发布,因此请查找 cvs-1.4.tar.gz,如果存在该版本,请检索该版本。

总结

RCS 以易于使用的软件包提供了完整、灵活的版本控制。与 make 一样,RCS 是任何认真的程序员都需要学习和使用的软件包。

更多信息

致谢

感谢 Paul Eggert 审阅本文。他的评论非常有用;其中一些几乎逐字逐句地被采纳。还要感谢 Miriam Robbins 迫使我运行 spell

Arnold Robbins 是一位专业程序员和半专业作家。自 1987 年以来,他一直在为 GNU 项目做志愿者工作,自 1981 年以来一直使用 Unix 和类 Unix 系统。

加载 Disqus 评论