手作業修正 -> load 完了

svnadmin dump した結果をスクリプトと手作業で編集して svnadmin load するのに成功した。まぁ成功して当たり前なんだけど、一応メモ。使ったのは svn 1.4.2 と Ruby 1.8.5 とエディタ。

svndumpfilter はそんなに使えない

svnadmin dump の結果に対して path で単純なフィルタリングを行う svndumpfilter というものがあるが、これを適用しても特に以下のようなリポジトリレイアウトの場合、あまり役に立たない。

branches/
  各モジュール/
  ...
tags/
  ...
trunk/
  ...

このツールは以下のようなレイアウトになっていて、

モジュール/
  branches/
  tags/
  trunk/
モジュール/
  branches/
  ...
...

ここで言うモジュール単位に分割する際には使える。つまり、リポジトリそのものの操作がのちのち楽なのは後者のレイアウト。

新規にリポジトリを作成する場合はともかく、cvs2svn で既存のリポジトリを変換する場合は何も考えないと前者のレイアウトになってしまうので注意が必要。

※ その他にも cvs2svn は svn:eol-style を native にしてしまうのでクライアントのプラットフォームが混ざっている場合に思わぬ不具合を引き起こす可能性がある。

dump したテキストファイルは mbox のような感じ

なので、mbox っぽいデータを自分でパースしたことのある人ならなんとかなります。XML ではないのでデータの終わりは明示されず、始まりしか明示されないので、始まりの区切りをそこまでのデータの終わりとして機能させれば ok.

ただし改行コードは混ざっているので注意。パースする際に改行コードを無造作に chop しちゃダメ。

タグを打った操作の追跡時に注意が必要

svn copy は特定のパスだけを copy する作業がなかなかやりにくい。これが cvs2svn を使ってリポジトリを変換した際、タグを打つ操作に対応する revision を見るとなるほどと思う。丸ごといちばん上の階層から copy して、不要なパスを delete している。このとき、この delete するパスが存在していないと load の際に失敗する。

つまり、

branches/
  ..
trunk/
  path1/
  path2/
tags/
  ..

というリポジトリがあり、

branches/
  path1/
trunk/
  path1/

のように path1 だけを取り出したいという場合、最初の方の revision で path2 を add する作業をカットしてあると、copy -> delete のコンボ revision の中で path2 を delete するアクションがあってはいけない。

でもまぁ手作業対応もそれなりに可能

今回はまだリポジトリの full dump が 124MB、取り出したい部分の dump ファイルが 26MB で収まっていたので、なんとかエディタで開いてスクリプトで対処しきれなかった余計な部分のカット、逆にスクリプトで取り除きすぎてしまった部分を full dump から戻す作業を行えた。(重かったけど。)ブランチの数もせいぜい数十。

ただこのレベルを越えるとなるともっと精度の高いフィルタが必要だなとは思った。

使ったスクリプト

本当はできるだけ手作業を減らそうと、revision ごとに dump ファイルを分割する機能をつけて、保全した revision の差分を取ってゴニョゴニョ(まだあまり考えていない)する機能をつけようと思っていたのですが、面倒くさくなったので放置です。

実に中途半端な状態ですが、とりあえずバックアップ目的で貼っておきます。なんか使えそうならご自由に。

#! /usr/bin/env ruby

require 'pathname'
require 'logger'

=begin

Subversion の dump ファイルから特定の条件を満たす node および revision
だけの dump ファイルを生成する。

Usage: ruby SCRIPT srcdump > destdump

Revision について
=================

Prop-content-length: は当然ヘッダ部以降 PROPS-END までのサイズ。

Content-length: は Prop-content-length と同じ。この数字は Node の
content-length に影響されない。つまり、Revision の途中で Node がバッサ
リ落ちても影響ない

ただし Node をただ単独で落とすと cvs2svn が作る、丸ごと copy してから要
らないものを削除するという revision に対応できないことがある。

Node-copy* という文字列を見つけたら Revision 丸ごと保存したい。

Node について
=============

Prop-content-length: はヘッダ部以降 PROPS-END までのデータのサイズ
Text-content-length: は PROPS-END 以降次のヘッダまでのデータのサイズ
Content-length: は上の content-length の合計

=end

#
# svnadmin dump した結果を処理するツール
#
module Svndump
  #
  # パースした node および revision に対する共通メソッド
  #
  class Common
    def initialize( loglevel = nil )
      _init_logger( loglevel )

      @curr_rev   = nil
      @curr_node  = nil
      @buf        = []
      @revs       = []
    end

    #
    # Logger の初期化
    #
    def _init_logger( level = nil )
      if ( level.nil? )
        return false
      end

      @logger = Logger.new( STDERR )
      case level
      when 'debug'
        @logger.level = Logger::DEBUG
      when 'info'
        @logger.level = Logger::INFO
      when 'warn'
        @logger.level = Logger::WARN
      when 'error'
        @logger.level = Logger::ERROR
      else # include 'fatal'
        @logger.level = Logger::FATAL
      end

      return true
    end
    private :_init_logger

    #
    # 指定ファイルまたは標準入力から dump ファイルを読み込みあれこれ
    #
    # 行ごとに分割して特徴的なヘッダ行をもとに pre_hook, post_hook を定義
    # chomp() しない。改行コードが混ざっている可能性があるので。
    #
    def read
      while ( line = gets )
        case line
        when /\ARevision-number: ([0-9]+)/
          revision_pre_hook()
          @curr_rev  = $1.to_i
          @curr_node = nil
          revision_post_hook()
        when /\ANode-path: (.*)/
          node_pre_hook()
          @curr_node = $1
          node_post_hook()
        when /\ANode-copy/
          nodecopy_pre_hook()
        end
        @buf.push( line )
      end
    end # of read()

    def revision_pre_hook
    end

    def revision_post_hook
    end

    def node_pre_hook
    end

    def node_post_hook
    end

    def nodecopy_pre_hook
    end

    def clear_rev_buf
      @rev_buf  = []
      @keep_rev = false
    end
    private :clear_rev_buf

    def clear_buf
      @buf      = []
      @storable = true
    end
    private :clear_buf
  end # of class Common

  #
  # dump ファイルを各 revision ごとに分割する
  #
  class Splitter < Common
  end

  #
  # 小さくする
  #
  class Compact < Common
    #
    # コンストラクタ
    #
    def initialize( loglevel = nil )
      super( loglevel )

      @dump_header = true
      @keep_rev    = false
      @rev_buf     = []

      @storable    = true
    end

    #
    # 主処理
    #
    def run
      read()
      after_all()
    end

    #
    # Revision-number の切り替わりのタイミングで呼ばれるメソッド
    #
    def revision_pre_hook
      if ( !@logger.nil? )
        @logger.debug( "in revision_pre_hook() rev #{@curr_rev} node #{@curr_node}" )
      end
      store2rev()
      output_rev()
      if ( @curr_rev == 0 )
        @dump_header = nil
      end
    end

    #
    # Revision-number の行を読んだ瞬間に働く hook
    #
    def revision_post_hook
    end

    #
    # Node-path の切り替わりのタイミングで呼ばれるメソッド
    #
    def node_pre_hook
      if ( !@logger.nil? )
        @logger.debug( "in node_pre_hook() rev #{@curr_rev} node #{@curr_node}" )
      end
      store2rev()
    end

    #
    # Node-path の行を読んだ瞬間に働く hook
    #
    def node_post_hook
    end

    #
    # Node-copy* 行が現れるタイミングで呼ばれるメソッド
    #
    def nodecopy_pre_hook
      @keep_rev = true
    end

    #
    # 全部読み終わった時点で呼ばれるメソッド
    #
    def after_all
      if ( !@logger.nil? )
        @logger.debug( "in after_all() rev #{@curr_rev} node #{@curr_node}" )
      end
      store2rev()
      output_rev()
    end

    #
    # 現在の @buf の内容を保存するかどうか
    #
    def store?
      return ( @keep_rev or @storable )
    end
    private :store?

    #
    # dump header および revision 0 かどうか
    #
    def dump_header?
      return @dump_header
    end
    private :dump_header?

    #
    # @buf_rev の内容を出力する
    #
    # 出力後、クリアもする
    #
    def output_rev
      if ( !@logger.nil? )
        p @curr_rev
        pp @rev_buf
        p "dump_header #{dump_header?}"
        p @rev_buf.size
      end
      if ( dump_header? or @rev_buf.size > 1 )
        @rev_buf.each { |buf|
          puts buf.join()
        }
        @revs.push( @curr_rev )
      end
      clear_rev_buf()
    end

    #
    # @buf の内容を @buf_rev に store
    #
    def store2rev
      if ( !@logger.nil? )
        @logger.debug( "in store2rev() store? #{store?}" )
      end
      if ( store? and @buf.size > 0 )
        @rev_buf.push( @buf )
      end
      clear_buf()
    end

  end # of class Compact
end # of Svndump

#
# テストじゃなかったら実行する
#
if ( Pathname( __FILE__ ).realpath == Pathname( $0 ).realpath )
  tool = Svndump::Compact.new( )
  def tool.node_post_hook
    if (
         # ここの書き方で調整
        )
      @storable = true
    else
      @storable = false
    end
  end
  tool.run
end

cf.

More