CVS のレポジトリをまるごと Mercurial 化するスクリプト

だんだんスニペット置き場になりつつあるような気がする…。いや、日本語で喋るかプログラミング言語で喋るかの違いしかないよね。

追記1: 今見たら EXIT_SUCCESS がなぜか 1 になってたので修正しました。はずかしー><
追記2: -d:pserver:anonymous... はあくまで例です。好きなCVSレポジトリを指定できます (そりゃそうか)。

以下のスクリプトの使い方

hg convert を使うための前提条件 (cvsps を入れとくとか、hgrc に hgext.convert= を追加しておくとか) はクリアしておかないと正しく動作しません。

基本は

./cvs2hg.sh -d:pserver:anonymous@pam-mysql.cvs.sourceforge.net:/cvsroot/pam-mysql

でOK。これを実行すると、次のようなメッセージが表示され、カレントディレクトリに1モジュール=1レポジトリの対応でレポジトリが作られる。

Retrieving module list from :pserver:anonymous@pam-mysql.cvs.sourceforge.net:/cvsroot/pam-mysql... done.

The following modules will be checked out and converted to Mercurial 
repositories:

  * CVSROOT
  * pam_mysql
  * pam_mysql_ng

Do you want to proceed? [y/n]: y

Checking out module CVSROOT... done.
Converting CVSROOT... done.
Checking out module pam_mysql... done.
Converting pam_mysql... done.
Checking out module pam_mysql_ng... done.
Converting pam_mysql_ng... done.
./cvs2hg.sh

-d を省略すると環境変数 CVSROOT が参照される。

./cvs2hg.sh -d:pserver:anonymous@pam-mysql.cvs.sourceforge.net:/cvsroot/pam-mysql  /path/to/some-location

/path/to/some-location というディレクトリにレポジトリが作られる。

./cvs2hg.sh -d:pserver:anonymous@pam-mysql.cvs.sourceforge.net:/cvsroot/pam-mysql -C

-C オプションをつけると、次のようなメッセージが出て、CVS レポジトリとの同期用クローンがモジュール名に .sync というサフィックスをつけたディレクトリに作られる。

Creating a clone for synchronization in ./CVSROOT.sync... done.
Creating a clone for synchronization in ./pam_mysql.sync... done.
Creating a clone for synchronization in ./pam_mysql_ng.sync... done.

このレポジトリ

cvs up
hg co
hg push

を自動的にやってやれば、MercurialCVSレポジトリを (簡易的に) 同期できる。なお、ブランチは考慮していない。

#!/bin/bash
# CVS to HG (mercurial) fully-automatic conversion script
# 2008 Copyright (C) 詠み人知らず. No rights reserved.

EXIT_SUCCESS=0
EXIT_INT=130
EXIT_TERM=139
EXIT_FATAL=255
CVS=cvs
HG=hg

progname=`basename $0`

usage() {
    echo "usage: $progname [-dCVSROOT] [-C] [output-dir]"
}

prompt() {
    echo -n "$1 [y/n]: "
    while read resp; do
        [ "$resp" = "y" -o "$resp" = "Y" ] && return 0
        [ "$resp" = "n" -o "$resp" = "N" ] && return 1
        echo -n "Enter either \`y' or \`n': "
    done
}

output_dir="."
create_sync_dir=

while [ ! -z "$1" ]; do
    case $1 in
        -d*)
            CVSROOT=`echo $1 | sed -e 's/^-d//'`
            export CVSROOT
            ;;
        -C)
            create_sync_dir=1
            ;;
        *)
            output_dir="$1"
            ;;
    esac
    shift
done

if [ -z "$CVSROOT" ]; then
    echo "$progname: no CVSROOT specified." 2>&1
    exit $EXIT_FATAL
fi

CVSCONV_ROOT=`mktemp -d`
(
    try() {
        trap 'echo interrupted.; return $EXIT_INT' INT
        trap 'echo aborted.; return $EXIT_TERM' TERM
        cvsroot_dir=`echo "$CVSROOT" | sed -e 's/^.*:\([^:]*\)$/\1/g'`
        modules_file="$CVSCONV_ROOT/.modules"
        echo -n "Retrieving module list from $CVSROOT... "
        fifo="$CVSCONV_ROOT/.fifo"
        mkfifo $fifo
        ( sed -e "s#^$cvsroot_dir\\/\\?\\([^/]*\\).*,v\$#\\1#; t e; d; :e" "$fifo" \
            | uniq > "$modules_file" ) &
        if ! error_msg=`"$CVS" rlog -R . 2>&1 > "$fifo"`; then
            echo "failed."
            echo "$progname: subprocess returned error status: $error_msg" >&2
            return $?
        fi
        wait
        echo "done."

        echo
        echo "The following modules will be checked out and converted to Mercurial "
        echo "repositories:"
        echo
        sed -e 's/^/  * /' "$modules_file"
        echo
        if ! prompt "Do you want to proceed?"; then
            echo "Cancelled."
            return 1
        fi
        echo
        cat "$modules_file" | while read module; do
            echo -n "Checking out module $module... "
            pushd "$CVSCONV_ROOT" >/dev/null
            if ! error_msg=`"$CVS" co "$module" 2>&1 > /dev/null`; then
                echo "failed."
                echo "$progname: subprocess returned error status: $error_msg" >&2
                return $?
            fi
            echo "done."
            popd >/dev/null
            co_dir="$CVSCONV_ROOT/$module"
            echo -n "Converting $module... "
            if ! error_msg=`"$HG" convert "$co_dir" "$output_dir/$module" 2>&1 >/dev/null`; then
                echo "failed."
                echo "$progname: subprocess returned error status: $error_msg" >&2
                return $?
            fi
            echo "done."

            if [ ! -z "$create_sync_dir" ]; then
                sync_dir="$output_dir/$module.sync"
                echo -n "Creating a clone for synchronization in $sync_dir... "
                if ! error_msg=`"$HG" clone "$output_dir/$module" "$sync_dir" 2>&1`; then
                    echo "failed."
                    echo "$progname: subprocess returned error status: $error_msg" >&2
                    return $?
                fi
                if ! error_msg=`( cd "$co_dir"; find . -type d -name "CVS" ) |\
                        while read dir; do
                            if [ -d "$sync_dir/\`dirname $dir\`" ]; then
                                cp -R "$co_dir/$dir" "$sync_dir/$dir"
                            fi
                        done 2>&1`; then
                    echo "failed."
                    echo "$progname: subprocess returned error status: $error_msg" >&2
                    return $?
                fi
                if [ ! -e "$sync_dir/.hgignore" ]; then
                    echo "syntax: glob" > "$sync_dir/.hgignore"
                fi
                format=`sed -e '1 { s/^\s*syntax:\s*//; p }; d' "$sync_dir/.hgignore"`
                case "$format" in
                    glob)
                        echo 'CVS' >> "$sync_dir/.hgignore"
                        ;;
                    regexp)
                        echo '^CVS$' >> "$sync_dir/.hgignore"
                        ;;
                esac
                echo "done."
            fi
        done
        return 0
    }
    finally() {
        rm -rf "$CVSCONV_ROOT"
    }
    try; STATUS=$?; finally exit $STATUS
)

exit $?