內容目录

上一个主题

6. 迁移到Git

下一个主题

6.2. 更多版本控制系统的迁移

本页

6.1. CVS版本库到Git的迁移

CVS是最早广泛使用的版本控制系统,因为其服务器端存储结构的简单直白,至今仍受到不少粉丝的钟爱。但是毕竟是几十年前的产物,因为设计上的原因导致缺乏现代版本控制系统的一些必须功能,如:没有原子提交,分支管理不便(慢),分支合并困难因为合并过程缺乏跟踪,不支持文件名/目录名的修改等等。很多CVS的用户都已经转换到Subversion这一更好的集中式版本控制系统了。如果还在使用CVS,那么可以考虑直接迁移到Git。

CVS到Git迁移可以使用cvs2svn软件包中的cvs2git命令。为什么该项目叫做cvs2svn而非cvs2git呢?这是因为该项目最早是为CVS版本库迁移到Subversion版本库服务的,只是最近才增加了CVS版本转换为Git版本库的功能。cvs2svn将CVS转换为Subversion版本库的过程一直以稳定著称,在cvs2svn 2.1版开始,增加了将CVS版本库转换为Git版本库的功能,无疑让这个工具更具生命力,也减少了之前CVS到Git库的转换环节。在推出cvs2git功能之前,通常的CVS到Git迁移路径是用cvs2svn将CVS版本库迁移到Subversion版本库,再用git-svn将Subversion版本库迁移到Git。

关于cvs2svn及cvs2git可以参考下面的链接:

6.1.1. 安装cvs2svn(含cvs2git)

Linux下cvs2svn的安装

大部分Linux发行版都提供cvs2svn的发布包,可以直接用平台自带的cvs2svn软件包。cvs2svn在2.1版本之后开始引入了到Git库的转换,2.3.0版本有了独立的cvs2git转换脚本,cvs2git正在逐渐完善当中,因此尽量选择最新版本的cvs2svn。

例如在Debian或Ubuntu下,可以通过下面命令查看源里面的cvs2svn版本。

$ aptitude versions cvs2svn
p   2.1.1-1               stable                     990
pi  2.3.0-2               testing,unstable           1001

可以看出Debian的Testing和Sid的仓库中才有2.3.0版本的cvs2svn。于是执行下面的命令安装在Testing版本才有的2.3.0-2版本的cvs2svn:

$ sudo aptitude cvs2svn/testing

如果对应的Linux发行版没有对应的版本也可以从源码开始安装。cvs2svn的官方版本库在http://cvs2svn.tigris.org/svn/cvs2svn/trunk,已经有人将cvs2svn项目转换为Git库。可以从Git库下载源码,安装cvs2svn。

  • 下载cvs2svn源代码

    $ git clone git://repo.or.cz/cvs2svn.git
    
  • 进入cvs2svn源码目录,安装cvs2svn。

    $ cd cvs2svn
    $ sudo make install
    
  • 安装用户手册。

    $ sudo make man
    

cvs2svn对其他软件包的依赖:

  • Python 2.4或以上版本(Python 3.x暂不支持)。
  • RCS:如果在转换中使用了--use-rcs,就需要安装RCS软件包。参见:http://www.cs.purdue.edu/homes/trinkle/RCS/
  • CVS:如果在转换中使用了--use-cvs,就需要安装CVS软件包。参见:http://ccvs.cvshome.org/
  • Git:1.5.4.4或以上的版本。之前版本的Git的git fast-import 命令有Bug,加载cvs2git导出文件有问题。

Mac OS X下cvs2svn的安装

Mac OS X下可以使用Homebrew安装cvs2svn。

  • Mac OS X缺省安装的Python缺少cvs2svn依赖的gdbm模组,先用Homebrew来重新安装python。

    $ brew install python
    
  • 安装cvs2svn

    $ export PATH=/usr/local/bin:$PATH
    $ brew install cvs2svn
    

6.1.2. 版本库转换(命令行参数模式)

转换CVS版本库的注意事项:

  • 使用cvs2git对CVS版本库转换,必须在CVS的服务器端执行,即cvs2git必须能够通过文件系统直接访问CVS版本库中的,v文件。

  • 在转换前,确保所有人的修改都已经提交到CVS版本库中。

  • 在转换前,停止CVS版本库的访问,以免在转换过程中有新提交写入。

  • 在转换前,对原始版本库进行备份,以免误操作对版本库造成永久的破坏。

  • 在转换完成后,永久停止CVS版本库的写入服务,可以仅开放只读服务。

    这是由于cvs2git是一次性操作,不能对CVS后续提交执行增量式的到Git库转换,因此当CVS版本库转换完毕后,须停止CVS服务。

  • 先做小规模的试验性转换。

    转换CVS版本库切忌一上来就对整个版本库进行转换,等到发现日志乱码、文件名乱码、提交者ID不完全后重新转换会浪费大量时间。

    应该先选择CVS版本库中的部分文件和目录作为样本,进行小规模的转换测试。

  • 不要对包含CVSROOT目录的版本库的根进行操作,可以先对服务器目录布局进行调整。

    如果转换直接针对包含CVSROOT目录的版本库根目录进行操作,会导致CVSROOT目录下的文件及更改历史也被纳入到Git版本库,这是不需要的。

检查CVS版本库中的文件名乱码

CVS中保存的数据在服务器端直接和同名文件(文件多了一个“,v”后缀)相对应,当转换的CVS版本库是从其他平台(如Windows)拷贝过来的,就可能因为平台本身字符集不一致导致中文文件名包含乱码,在CVS版本库转换过程造成乱码。可以先对有问题的目录名和文件名进行重命名,转换为当前平台正确的编码。

小规模的转换试验

前面提到过,最好先进行小规模的转换试验,然后再对整个版本库进行转换。例如版本库是如下方式部署:CVSROOT/cvshome/user,需要将之下的jiangxin/homepage/worldhello转换为一个Git版本库。先检查一下版本库中的数据,找出典型的目录用于转换。

典型的数据是这样的:包含中文文件名,并且日志中包含中文。例如在版本库中,执行CVS查看日志命令,看到类似下面的输出。

RCS file: /cvshome/user/jiangxin/homepage/worldhello/archive/2003/.mhonarc.db,v
Working file: archive/2003/.mhonarc.db
head: 1.16
branch:
locks: strict
access list:
symbolic names:
keyword substitution: kv
total revisions: 16;    selected revisions: 16
description:
----------------------------
revision 1.16
date: 2004-09-21 15:56:30 +0800;  author: jiangxin;  state: Exp;  lines: +3 -3;  commitid: c2c414fdea20000;
<D0><U+07B8><C4><D3>ʼ<FE><B5><D8><A3><BB>
<D0><U+07B8><C4><CB><D1><CB><F7><D2><FD><C7>棻
----------------------------

日志乱码是因为CVS并没有对日志的字符转换为统一的UTF-8字符集。此版本库之前用CVSNT维护,缺省字符集为GBK。那么就先对有乱码的这一个目录进行一下试验性的转换。

  • 调用cvs2git执行转换,产生两个导出文件。这两个导出文件将作为Git版本库创建时的导入文件。

    命令行用了两个--encoding参数设置编码,会依次进行尝试将日志中的非Ascii字符转换为UTF-8。

    $ cvs2git --blobfile git-blob.dat --dumpfile git-dump.dat \
      --encoding utf8 --encoding gbk --username cvs2git \
      /cvshome/user/jiangxin/homepage/worldhello/archive/2003/
    
  • 成功导出后,产生两个导出文件,一个保存各个文件的各个不同版本的数据内容,即在命令行指定的输出文件git-blob.dat。另外一个文件是上面命令行指定的git-dump.dat用于保存各个提交相关信息(提交者、提交时间、提交日志等)。

    $ du -sh git*dat
    9.8M    git-blob.dat
    24K     git-dump.dat
    

    可以看出保存文件内容的导出文件(git-blob.dat)相对更大一些。

  • 创建空的Git库,使用Git通用的数据迁移命令git fast-import将cvs2git的导出文件导入版本库中。

    $ mkdir test
    $ cd test
    $ git init
    $ cat ../git-blob.dat ../git-dump.dat | git fast-import
    
  • 检查导出结果。

    $ git reset HEAD
    $ git checkout .
    $ git log -1
    commit 8334587cb241076bcd2e710b321e8e16b5e46bba
    Author: jiangxin <>
    Date:   Tue Sep 21 07:56:31 2004 +0000
    
        修改邮件地址;
        修改搜索引擎;
    

很好,导出的Git库的日志,中文乱码问题已经解决。但是会发现提交日志中的作者(Author)字段信息不完整:缺乏邮件地址。这是因为CVS的提交者仅为用户登录ID,而Git的提交者信息还要包含邮件地址。cvs2git提供参数实现两种提交者ID的转换,不过需要通过配置文件予以指定,这就需要采用下面介绍的转换方法。

6.1.3. 版本库转换(配置文件模式)

使用命令行参数调用cvs2git麻烦、可重用性差,而且可配置项有限。采用cvs2git配置文件模式运行不但能够简化cvs2git的命令行参数,而且能够提供更多的命令行无法提供的配置项,可以更精确的对CVS到Git版本库转换进行定制。

cvs2svn软件包提供了一个cvs2git的配置示例文件,见源码中的cvs2git-example.options[1]。将该示例文件在本地复制一份,对其进行更改。该文件是Python代码格式,以“#”(井号)开始的行是注释,文件缩进不要随意更改,因为缩进也是Python语法的一部分。可以考虑针对下列选项进行定制。

  • 设置CVS版本库位置。

    使用配置文件方式运行cvs2git,只能在配置文件中设置要转换的CVS版本库位置,而不能在命令行进行设置。具体说是在配置文件的最后面run_optionsset_project方法中指定。

    run_options.set_project(
        # CVS 版本库的位置(不是工作区,而是包含,v 文件的版本库)
        # 可以是版本库下的子目录。
        r'/cvshome/user/jiangxin/homepage/worldhello/archive/2003/',
    
  • 导出文件的位置也在配置文件中预先设置好了,也不能再在命令行中设置。

    • 导出CVS版本文件的内容至文件cvs2svn-tmp/git-blob.dat

      缺省使用cvs命令做导出,最稳定。

      ctx.revision_collector = GitRevisionCollector(
          'cvs2svn-tmp/git-blob.dat',
      
          #RCSRevisionReader(co_executable=r'co'),
          CVSRevisionReader(cvs_executable=r'cvs'),
          )
      
    • 另外一个导出文件的缺省位置:cvs2svn-tmp/git-dump.dat

      ctx.output_option = GitOutputOption(
          os.path.join(ctx.tmpdir, 'git-dump.dat'),
      
          # The blobs will be written via the revision recorder, so in
          # OutputPass we only have to emit references to the blob marks:
          GitRevisionMarkWriter(),
      
          # Optional map from CVS author names to git author names:
          author_transforms=author_transforms,
          )
      
  • 设置无提交用户信息时使用的用户名。这个用户名可以用接下来的用户映射转换为Git用户名。

    ctx.username = 'cvs2svn'
    
  • 建立CVS用户和Git用户之间的映射。Git用户名可以用Python的tuple语法(name, email)或者用字符串name <email>来表示。

    author_transforms={
        'jiangxin'  : ('Jiang Xin', 'jiangxin@ossxp.com'),
        'dev1'      : u'开发者1 <dev1@ossxp.com>',
    
        'cvs2svn'   : 'cvs2svn <admin@example.com>',
        }
    
  • 字符集编码。即如何转换日志中的用户名、提交说明以及文件名的编码。

    对于可能在日志中出现中,必须做出下面类似设置。编码的顺序对输出也会有影响,一般将utf8放在gbk之前能保证当日志中同时出现两种编码时都能正常转换[2]

    ctx.cvs_author_decoder = CVSTextDecoder(
        [
            'utf8',
            'gbk',
            ],
        fallback_encoding='gbk'
        )
    
    ctx.cvs_log_decoder = CVSTextDecoder(
        [
            'utf8',
            'gbk',
            ],
        fallback_encoding='gbk'
        )
    
    ctx.cvs_filename_decoder = CVSTextDecoder(
        [
            'utf8',
            'gbk',
            ],
        #fallback_encoding='ascii'
        )
    
  • 是否忽略.cvsignore文件?缺省保留.cvsignore文件。

    无论选择保留或是不保留,最好在转换后手工进行.cvsignore.gitignore的转换。因为 cvs2git不能自动将.cvsignore文件转换为.gitignore文件。

    ctx.keep_cvsignore = True
    
  • 对文件换行符等的处理。下面的配置原本是针对CVS到Subversion的属性转换,但是也会影响到Git转换时的换行符设置。

    维持默认值比较安全。

    ctx.file_property_setters.extend([
        # 基于配置文件设置文件的 mime 类型
        #MimeMapper(r'/etc/mime.types', ignore_case=False),
    
        # 对于二进制文件(-kb模式)不设置 svn:eol-style 属性(对于 Subverson 来说)
        CVSBinaryFileEOLStyleSetter(),
    
        # 如果文件是二进制,并且 svn:mime-type 没有设置,将其设置为 'application/octet-stream'。
        CVSBinaryFileDefaultMimeTypeSetter(),
    
        # 如果希望根据文件的 mime 类型来判断文件的换行符,打开下面注释
        #EOLStyleFromMimeTypeSetter(),
    
        # 如果上面的规则没有为文件设置换行符类型,则为 svn:eol-style 设置缺省类型。
        # (二进制文件除外)
        # 缺省把文件视为二进制,不为其设置换行符类型,这样最安全。
        # 如果确认 CVS 的二进制文件都已经设置了 -kb 参数,或者使用上面的规则能够对
        # 文件类型做出正确判断,也可以使用下面参数为非二进制文件设置缺省换行符号。
        ## 'native': 服务器端文件的换行符保存为 LF,客户端根据需要自动转换。
        ## 'CRLF':   服务器端文件的换行符保存为 CRLF,客户端亦为 CRLF。
        ## 'CR':     服务器端文件的换行符保存为 CR,客户端亦为 CR。
        ## 'LF':     服务器端文件的换行符保存为 LF,客户端亦为 LF。
        DefaultEOLStyleSetter(None),
    
        # 如果文件没有设置 svn:eol-style ,也不为其设置 svn:keywords 属性
        SVNBinaryFileKeywordsPropertySetter(),
    
        # 如果 svn:keywords 未色环只,基于文件的 CVS 模式进行设置。
        KeywordsPropertySetter(config.SVN_KEYWORDS_VALUE),
    
        # 设置文件的 svn:executable 属性,如果文件在 CVS 中标记为可执行文件。
        ExecutablePropertySetter(),
        ])
    
  • 是否只迁移主线,忽略分支和里程碑?

    缺省对所有分支和里程碑都进行转换。如果选择忽略分支和里程碑,将False修改为True

    ctx.trunk_only = False
    
  • 分支和里程碑迁移及转换。

    global_symbol_strategy_rules = [
    
        # 和正则表达式匹配的 CVS 标识,转换为 Git 的分支。
        #ForceBranchRegexpStrategyRule(r'branch.*'),
    
        # 和正则表达式匹配的 CVS 标识,转换为 Git 的里程碑。
        #ForceTagRegexpStrategyRule(r'tag.*'),
    
        # 忽略和正则表达式匹配的 CVS 标识,不进行(到Git分支/里程碑)转换。
        #ExcludeRegexpStrategyRule(r'unknown-.*'),
    
        # 岐义的CVS标识的处理选项。
        # 缺省根据使用频率自动确定转换为分支或里程碑。
        HeuristicStrategyRule(),
        # 或者全部转换为分支。
        #AllBranchRule(),
        # 或者全部转换为里程碑。
        #AllTagRule(),
    
        ...
    
    run_options.set_project(
    
        ...
    
        # A list of symbol transformations that can be used to rename
        # symbols in this project.
        symbol_transforms=[
            # 是否需要重新命名里程碑?第一个参数用于匹配,第二个参数用于替换。
            #RegexpSymbolTransform(r'release-(\d+)_(\d+)',
            #                      r'release-\1.\2'),
            #RegexpSymbolTransform(r'release-(\d+)_(\d+)_(\d+)',
            #                      r'release-\1.\2.\3'),
    

使用配置文件的 cvs2git转换过程

参照上面的方法,从缺省的cvs2git配置文件定制,在本地创建一个文件,例如名为cvs2git.options文件。

  • 使用cvs2git配置文件,命令行大大简化了。

    $ cvs2git --options cvs2git.options
    
  • 成功导出后,产生两个导出文件,都保存在cvs2git-tmp目录中。

    一个保存各个文件的各个不同版本的数据内容,即在命令行指定的输出文件git-blob.dat。另外一个文件是上面命令行指定的git-dump.dat用于保存各个提交相关信息(提交者、提交时间、提交日志等)。

    可以看出保存文件内容的导出文件相对更大一些。

    $ du -sh cvs2svn-tmp/*
    9.8M    cvs2svn-tmp/git-blob.dat
    24K     cvs2svn-tmp/git-dump.dat
    
  • 创建空的Git库,使用Git通用的数据迁移命令git fast-import将cvs2git的导出文件导入版本库中。

    $ mkdir test
    $ cd test
    $ git init
    $ cat ../cvs2svn-tmp/git-blob.dat \
          ../cvs2svn-tmp/git-dump.dat | git fast-import
    
  • 检查导出结果。

    $ git reset HEAD
    $ git checkout .
    $ git log -1
    commit e3f12f57a77cbffcf62e19012507d041f1c9b03d
    Author: Jiang Xin <jiangxin@ossxp.com>
    Date:   Tue Sep 21 07:56:31 2004 +0000
    
        修改邮件地址;
        修改搜索引擎;
    

可以看到,这一次的转换结果不但日志中的中文可以显示,而且提交者ID也转换成了Git的风格。

修改cvs2git.optoins中的CVS版本库地址,开始正式的转换过程。

6.1.4. 迁移后版本库检查

完成迁移还不能算是大功告成,还需要进行细致的检验。

6.1.4.1. 文件名和日志的中文

如果转换过程参考了前面的步骤和注意事项,文件名和版本库提交日志中的中文不应该出现乱码。

6.1.4.2. 图片文件被破坏

最典型的错误就是转换后部分图片被破坏导致无法显示。这是怎么造成的呢?

CVS缺省将提交的文件以文本方式添加,除非用户在添加文件时使用了-kb参数。用命令行提交的用户经常会忘记,这就导致一些二进制文件(如图片文件)被以文本文件的方式添加到其中。文本文件在CVS检入和检出时会进行换行符转换 ,在服务器端换行符保存为LF,在Windows上检出时为CRLF。如果误做文本文件方式添加的图片中恰好出现CRLF,则在Windows上似乎没有问题(仍然是CRLF),但是CVS库转换成 Git库后,图片文件在Windows上再检出时文件数据中原来CRLF被换成了LF,导致文件被破坏。

出现这种情况是CVS版本库使用和管理上出现了问题,应该在CVS版本库中对有问题的文件重新设置属性,标记为二进制文件。然后再进行CVS版本库到Git库的转换。

6.1.4.3. .cvsignore文件的转换

CVS版本库中可能存在.cvsignore文件用于设置文件忽略,相当于Git版本库中的.gitignore。因为当前版本的 cvs2git不能自动将.cvsignore转换为.gitignore,需要在版本库迁移后手工完成。CVS的.cvsignore文件只对目录内文件有效,不会向下作用到子目录上,这一点和Git的.gitignore相区别。还有不同就是.cvsignore文件每一行用空格分割多个忽略,而Git每个忽略为单独的一行。

6.1.4.4. 迁移后的测试

一个简单的检查方法是,在同一台机器上分别用CVS和Git检出(或克隆),然后比较本地的差异。要在不同的系统上(Windows,Linux)分别进行测试。


[1]http://repo.or.cz/w/cvs2svn.git/blob/HEAD:/cvs2git-example.options
[2]部分中文的UTF8编码在GBK中存在古怪的对应