#!/usr/pkg/bin/perl -w
#
# Copyright (c) 2003 Lev A. Serebryakov <lev@serebryakov.spb.ru>
#
# Distributed under terms of BSD license. See LICENSE file.
#
# $Id: refinecvs.pl 1251 2005-11-13 17:49:06Z lev $
#
use strict;
use warnings;

# Manual encoding processings
no encoding;

use Encode;

use Storable;
use Digest::MD5;
use Time::Local;

use Cvs::Repository::Exception qw(:INTERNAL);
use Cvs::Repository::Revision;
use Cvs::Repository::Delta;
use Cvs::Repository::File;
use Cvs::Repository::DeltaCache;

##
# Version
use vars qw($VERSION);

$VERSION  = join('.',0,86,('$LastChangedRevision: 1251 $' =~ /^\$\s*LastChangedRevision:\s+(\d+)\s*\$$/));

##
# Shortcuts
use constant ST_TRUNK       => Cvs::Repository::Revision::TYPE_TRUNK;
use constant ST_BRANCH      => Cvs::Repository::Revision::TYPE_BRANCH;
use constant ST_BRANCHMAGIC => Cvs::Repository::Revision::TYPE_BRANCHMAGIC;
use constant ST_VENDOR      => Cvs::Repository::Revision::TYPE_VENDOR;
use constant ST_VENDORMAGIC => Cvs::Repository::Revision::TYPE_VENDORMAGIC;


##############################################################################
# Configuration variables
##
# Is in verbose mode?
my $CFG_VERBOSE = 0;
##
# Is in progress mode?
my $CFG_PROGRESS = 0;
##
# Timeout, when commits with one log message will be treated not as 'atomic', in
# seconds. Default value is 5 minutes (from cvs2svn.py).
my $CFG_TIMEOUT = 600;
##
# Ignore all vendor machinery
my $CFG_IGNOREVENDOR = 0;
##
# Ignore these branches (symbols)
my %CFG_IGNORESYMBOLS = ();
##
# Forced symbol parents
my %CFG_SYMBOLSPARENTS = ();
##
# Use symbol name heuristic: any symbol prefer parent which is longest prefix
# for this symbol
my $CFG_SYMHEUR = 0;
##
# Allows weak symbol resolution
my $CFG_SYMWEAK = 0;
##
# Do we need to store & load dumps?
my $CFG_LOADDUMPS  = 0;
my $CFG_SAVEDUMPS = 0;
##
# Do we need pre-allocate space for all files in repository?
my $CFG_PREALLOC = 0;
##
# Multi-project source repository?
my $CFG_ONEPROJECT = 1;
##
# Names of special catalogs
my @CFG_NAMES = ();
   $CFG_NAMES[ST_TRUNK]       = '%p/trunk';
   $CFG_NAMES[ST_BRANCH]      = '%p/tags/%n';
   $CFG_NAMES[ST_BRANCHMAGIC] = '%p/branches/%n';
   $CFG_NAMES[ST_VENDOR]      = '%p/vendor/%v/tags/%n';
   $CFG_NAMES[ST_VENDORMAGIC] = '%p/vendor/%v/trunk';
##
# Allow deletions after copy to make effective tags
my $CFG_DELPRECENT = 0;
##
# Memory for diff cache
my $CFG_DIFFMEM = 0;
##
# Memory for results cache
my $CFG_RESMEM = 0;
##
# Array of already-live paths
my @CFG_LIVE = ();
##
# Encoding of file names
my $CFG_ENC_FN = undef;
##
# Encoding of commit logs
my $CFG_ENC_LOG = undef;
##
# Name of author of all tags and branches, which have commits from different
# authors
my $CFG_TAGAUTHOR = '';
##
# Path to CVS repository. Could be directory inside real repository
my $CFG_CVSROOT = '';
##
# Name of output SVN dump
my $CFG_SVNDUMP = '';
##
# Mode of events
my %EVENTS = (
  'FileError'      => 'd',
  'DoubleFile'     => 'd',
  'UnnamedBranch'  => 'd',
  'LostBranch'     => 'd',
  'LostSymbol'     => 'd',
  'DoubleSymbol'   => 'd',
  'LostRevision'   => 'd',
  'TimeMismatch'   => 'd',
  'DiffSymType'    => 'd',
  'DiffSymParent'  => 'd',
  'NoSymParent'    => 'd',
  'InvalidSymName' => 'd',
  'InvalidSymRev'  => 'd',
  'DoubleRev'      => 'd',
  ''               => ''
);

##############################################################################
# Very global variables
##
# This is hash of all files in repository.
#   Key:   path in repository (as-is, with delimiters normalized to '/').
#   Value: Cvs::Repository::File instance, without deltatexts
my $REPO = {};
##
# Mapping between local (disk) file names & repository file names.
my $LN2RN = {};
##
# This is hash of all symbols (branches' names & tags) in repository
#   Key:   Symbol (tag or branch) name.
#   Value: One symbol description
#     Structure of one value:
#       {
#         'type'         => <'t'|'b'|'i'|'v'>
#         'files'        => {
#                             'parent' => [
#                                           [
#                                             <file as Cvs::Repository::File>,
#                                             <added as unixtime>,
#                                             <first changed as unixtime>
#                                           ],
#                                           ...
#                                         ],
#                             ...
#                           }
#         'date'         => <date of creation finish>
#         'firstchanged' => <date of first change>,
#       }
#     'type' is 't'ag, 'b'ranch, vendor branch ('i'mport), 'v'endor tag.
# NB! After pass 3 and before pass 4 finished it contains 'raw' information
#     in similar format, but 'files' contains ALL POSSIBLE parents, so any
#     file could be encountered many times.
my $SYMBOLS = {};
##
# Constants for symbol's element
use constant SE_FILE         => 0;
use constant SE_ADDED        => 1;
use constant SE_FIRSTCHANGED => 2;
##
# This is array of all "atomic" commits
#   Values: Sorted by commit date (normalized to timeout).
#     Structure of one value:
#       {
#         'date'        => <date as unixtime>,
#         'author'      => <author as string>
#         'log'         => <log message as string>,
#         'type'        => <'i'|'r'|'c'|'b'|'t'|'v'>
#         'content'     => [
#                            [
#                              <revision as Cvs::Repository::Revision>,
#                              <file as Cvs::Repository::File>
#                            ],
#                            ...
#                          ],
#         'tag'         => <tag as string>,
#         'branch'      => <branch as string>
#       }
#     'type' characters means: 'i'mport, copy to t'r'unk from import,
#     'c'hange, 'b'ranch, 't'ag or 'v'endor tag. If it is tagging or branch
#     creation, field 'tag' will be present. 'branch' could be omitted for 'c'hange
#     trunk commits. 'C'hange commits into branches will contain 'branch'
#     with branch name.
my $COMMITS = [];
##
# Constants for commits' element
use constant CE_REV  => 0;
use constant CE_FILE => 1;
##
# This is mapping between CVS revisions of files & Subversion repository
# revisions.
my $CVS2SVN = {
  't' => 'd',
  'c' => {},
  'h' => [],
  'p' => undef,
  '_' => []
};

##
# And this is array of leafs from $CVS2SVN tree
my @SVNFILES = ();
##
# Current Subversion repository revision
my $SVNREVISION = 1;
##
# State, which should be dumped
my $STATE = { 'pass' => 0, 'state' => [$REPO, $LN2RN, $SYMBOLS, $COMMITS], 'magic' => 0xDEADBEEF };
##
# Cache for check-outs
my $CACHE = undef;
##
# Warning callbacks for different events
my %CALLBACKS = (
  'FileError'      => \&warning_FileError,
  'DoubleFile'     => \&warning_DoubleFile,
  'UnnamedBranch'  => \&warning_UnnamedBranch,
  'LostBranch'     => \&warning_LostBranch,
  'LostSymbol'     => \&warning_LostSymbol,
  'DoubleSymbol'   => \&warning_DoubleSymbol,
  'LostRevision'   => \&warning_LostRevision,
  'TimeMismatch'   => \&warning_TimeMismatch,
  'DiffSymType'    => \&warning_DiffSymType,
  'DiffSymParent'  => \&warning_DiffSymParent,
  'NoSymParent'    => \&warning_NoSymParent,
  'InvalidSymName' => \&warning_InvalidSymName,
  'InvalidSymRev'  => \&warning_InvalidSymRev,
  'DoubleRev'      => \&warning_DoubleRev,
  ''               => ''
);

##
# Name of dump file
my $DUMPFILE = '';

##
# Time of all process start
my $WHOLE_START_TIME = time();
##
# Time of current op. start
my $STEP_START_TIME;

##
# Description of all passes. Every pass could be easily skipped
my @PASSES = (
  {
    'name'      => '',
    'skipcheck' => undef,
    'makepass'  => undef,
    'makeres'   => undef,
    'progress'  => undef
  },
  {
    'name'      => 'Load all RCS files from repository without content',
    'skipcheck' => undef,
    'makepass'  => \&pass1_LoadCVSRepository,
    'makeres'   => \&pass1_results,
    'progress'  => 1
  },
  {
    'name'      => 'Build tree of revisions & check symbols in each file',
    'skipcheck' => undef,
    'makepass'  => \&pass2_CompleteParsing,
    'makeres'   => \&pass2_results,
    'progress'  => 1
  },
  {
    'name'      => 'Collect symbols from files.',
    'skipcheck' => \&check_isSymbolsIgnored,
    'makepass'  => \&pass3_CollectSymbols,
    'makeres'   => \&pass3_results,
    'progress'  => 1
  },
  {
    'name'      => 'Normalize symbols over repository.',
    'skipcheck' => \&check_isSymbolsIgnored,
    'makepass'  => \&pass4_NormalizeSymbols,
    'makeres'   => \&pass4_results,
    'progress'  => 1
  },
  {
    'name'      => 'Group commits to \'atomic\' groups.',
    'skipcheck' => undef,
    'makepass'  => \&pass5_CollectCommits,
    'makeres'   => \&pass5_results,
    'progress'  => 1
  },
  {
    'name'      => 'Add tagging commits.',
    'skipcheck' => \&check_isSymbolsIgnored,
    'makepass'  => \&pass6_MakeTags,
    'makeres'   => \&pass6_results,
    'progress'  => 1
  },
  {
    'name'      => 'Sort commits by type and date.',
    'skipcheck' => undef,
    'makepass'  => \&pass7_SortCommits,
    'makeres'   => \&pass7_results,
    'progress'  => 0
  },
  {
    'name'      => 'Dump commits in SVN format.',
    'skipcheck' => undef,
    'makepass'  => \&pass8_DumpSVN,
    'makeres'   => \&pass8_results,
    'progress'  => 1
  }
);

##
# Make STDOUT not-buffered
$| = 1;

##
# Read options line
#
while(defined $ARGV[0] && $ARGV[0] =~ /^-/) {
  my $opt = shift @ARGV;
  if      ('-v' eq $opt) {
    ++$CFG_VERBOSE;
    &Help('Could not specify -v and -p in same time') unless !$CFG_PROGRESS;
  } elsif ('-p' eq $opt) {
    $CFG_PROGRESS = 1;
    &Help('Could not specify -v and -p in same time') unless !$CFG_VERBOSE;
  } elsif ('-t' eq $opt) {
    $CFG_TIMEOUT = shift @ARGV;
    &Help('Need timeout value') unless defined $CFG_TIMEOUT;
    &Help('Invalid timeout value, need NUM[s|m|h]: '.$CFG_TIMEOUT) if $CFG_TIMEOUT !~ s/^(\d+)(s|m|h)?$/$1/i;
    my $m = lc($2 || 's');
    if      ($m eq 'm') {
      $CFG_TIMEOUT *= 60;
    } elsif ($m eq 'h') {
      $CFG_TIMEOUT *= 3600;
    }
  } elsif ('-dcm' eq $opt) {
    $CFG_DIFFMEM = shift @ARGV;
    &Help('Need memory size value') unless defined $CFG_DIFFMEM;
    &Help('Invalid memory value, need NUM[b|k|m]: '.$CFG_DIFFMEM) if $CFG_DIFFMEM !~ s/^(\d+)(b|k|m)?$/$1/i;
    my $m = lc($2 || 'b');
    if      ($m eq 'k') {
      $CFG_DIFFMEM *= 1024;
    } elsif ($m eq 'm') {
      $CFG_DIFFMEM *= 1024*1024;
    }
  } elsif ('-rcm' eq $opt) {
    $CFG_RESMEM = shift @ARGV;
    &Help('Need memory size value') unless defined $CFG_RESMEM;
    &Help('Invalid memory value, need NUM[b|k|m]: '.$CFG_RESMEM) if $CFG_RESMEM !~ s/^(\d+)(b|k|m)?$/$1/i;
    my $m = lc($2 || 'b');
    if      ($m eq 'k') {
      $CFG_RESMEM *= 1024;
    } elsif ($m eq 'm') {
      $CFG_RESMEM *= 1024*1024;
    }
  } elsif ('-e' eq $opt || '-w' eq $opt || '-i' eq $opt) {
    my $mode = substr($opt,1,1);
    my $ids = shift @ARGV;
    &Help('Need events\' names for '.$opt) unless defined $ids;
    foreach my $id (split(/\s*,\s*/,$ids)) {
      &Help('Unknown event ID \''.$id.'\'') unless exists $EVENTS{$id};
      &Help('Event ID \''.$id.'\' could be specified only once') unless $EVENTS{$id} eq 'd';
      if      ($mode eq 'e') {
        $EVENTS{$id} = 1;
      } elsif ($mode eq 'w') {
        $EVENTS{$id} = $CALLBACKS{$id};
      } elsif ($mode eq 'i') {
        $EVENTS{$id} = 0;
      }
    }
  } elsif ('-is' eq $opt) {
    my $ids = shift @ARGV;
    &Help('Need symbols\' names for -is') unless defined $ids;
    foreach my $id (split(/\s*,\s*/,$ids)) {
      &Help('You could not ignore HEAD symbol') unless $id ne 'HEAD';
      $CFG_IGNORESYMBOLS{$id} = 1;
    }
  } elsif ('-sp' eq $opt) {
    my $pairs = shift @ARGV;
    &Help('Need symbols\' pairs for -sp') unless defined $pairs;
    foreach my $pair (split(/\s*,\s*/,$pairs)) {
      my ($child,$parent) = split(/\s*=\s*/,$pair);
      &Help('Invalid CHILD=PARENT element: \''.$pair.'\' for \'-sp\' option') unless defined $child && $child && defined $parent && $parent;
      &Help('CHILD \''.$child.'\' encountered more than once in \'-sp\' option') if exists $CFG_SYMBOLSPARENTS{$child};
      foreach my $c (split(/\./,$child)) {
        $CFG_SYMBOLSPARENTS{$c} = $parent;
      }
    }
  } elsif ('-iv' eq $opt) {
    $CFG_IGNOREVENDOR = 1;
  } elsif ('-ad' eq $opt) {
    $CFG_DELPRECENT = shift @ARGV;
    &Help('Need allowed percent for '.$opt) unless defined $CFG_DELPRECENT;
    &Help('Value for `-ad\' option should be number between 0 and 99') unless $CFG_DELPRECENT =~ /^\d\d?$/;
  } elsif ('-sh' eq $opt) {
    $CFG_SYMHEUR = 1;
  } elsif ('-aw' eq $opt) {
    $CFG_SYMWEAK = 1;
  } elsif ('-ld' eq $opt) {
    $CFG_LOADDUMPS = 1;
  } elsif ('-sd' eq $opt) {
    $CFG_SAVEDUMPS = 1;
  } elsif ('-m' eq $opt) {
    $CFG_PREALLOC = 1;
  } elsif ('-mp' eq $opt) {
    $CFG_ONEPROJECT = 0;
  } elsif ('-tn' eq $opt) {
    $CFG_NAMES[ST_TRUNK] = shift @ARGV;;
    &Help('Need trunk name value') unless defined $CFG_NAMES[ST_TRUNK];
  } elsif ('-bn' eq $opt) {
    $CFG_NAMES[ST_BRANCHMAGIC] = shift @ARGV;;
    &Help('Need branches name value') unless defined $CFG_NAMES[ST_BRANCHMAGIC];
  } elsif ('-gn' eq $opt) {
    $CFG_NAMES[ST_BRANCH] = shift @ARGV;;
    &Help('Need tags name value') unless defined $CFG_NAMES[ST_BRANCH];
  } elsif ('-vtn' eq $opt) {
    $CFG_NAMES[ST_VENDORMAGIC] = shift @ARGV;
    &Help('Need vendor trunk name value') unless defined $CFG_NAMES[ST_VENDORMAGIC];
  } elsif ('-vgn' eq $opt) {
    $CFG_NAMES[ST_VENDOR] = shift @ARGV;
    &Help('Need vendor tags name value') unless defined $CFG_NAMES[ST_VENDOR];
  } elsif ('-l' eq $opt) {
    my $paths = shift @ARGV;
    &Help('Need paths for -l') unless defined $paths;
    push @CFG_LIVE,split(/\s*,\s*/,$paths);
  } elsif ('-fcp' eq $opt) {
    $CFG_ENC_FN = shift @ARGV;;
    &Help('Need file name codepage value') unless defined $CFG_ENC_FN;
  } elsif ('-lcp' eq $opt) {
    $CFG_ENC_LOG = shift @ARGV;;
    &Help('Need log messages codepage value') unless defined $CFG_ENC_LOG;
  } else {
    &Help('Unknown option: '.$opt);
    # HELP never returns
  }
}

# Check parent settings: there should not be loops, nor ignored symbols.
&Help('Your ignore all symbols and give symbols-parents relations') unless !exists $CFG_IGNORESYMBOLS{'*'} || !keys %CFG_SYMBOLSPARENTS;
my %sp = %CFG_SYMBOLSPARENTS;
my $ch = 0;
while(keys %sp) {
  $ch = 0;
  foreach my $c (keys %sp) {
    if      (exists $CFG_IGNORESYMBOLS{$c}) {
      &Help("Symbol '$c' has user-selected parent & ignored in same time."),
    } elsif (exists $CFG_IGNORESYMBOLS{$sp{$c}}) {
      &Help("Symbol $sp{$c} is user-selected parent & ignored in same time."),
    }
    # If parent of this children is not a children
    # by itself, delete this link
    if      (!exists $sp{$sp{$c}}) {
      $ch = 1;
      delete $sp{$c};
    } elsif (!grep { $c eq $_ } values %sp) {
      # If child is not a parent for any other child, delete this link too
      $ch = 1;
      delete $sp{$c};
    }
  }
  last unless $ch;
}
&Help("Given set of symbols parent have cycles:\n".
      join("\n", map { "'$_' has '$sp{$_}' as parent" } keys %sp)) unless !keys %sp;


# Set default mode for all events
foreach my $e (keys %EVENTS) { $EVENTS{$e} = 'e' if $EVENTS{$e} eq 'd'; }

$CFG_TAGAUTHOR = shift @ARGV;
&Help('Need tagauthor parameter') unless defined $CFG_TAGAUTHOR;

$CFG_CVSROOT = shift @ARGV;
&Help('Need CVSROOT parameter') unless defined $CFG_CVSROOT;
$CFG_SVNDUMP = shift @ARGV;
&Help('Need svn dump parameter') unless defined $CFG_SVNDUMP;

# Normalize path on Win32
$CFG_CVSROOT =~ s#\\#/#g if $^O =~ /win32/i;
$CFG_CVSROOT .= '/' if substr($CFG_CVSROOT,-1,1) ne '/';
$CFG_CVSROOT =~  s#/+#/#g;

##
# Set project, if single-project config
if($CFG_ONEPROJECT) {
  $CFG_CVSROOT    =~ m#^.+?([^/]+)/$#;
  $CFG_ONEPROJECT =  $1;
}

##
# Normalize options about paths
foreach my $t (ST_TRUNK,ST_BRANCH,ST_BRANCHMAGIC,ST_VENDOR,ST_VENDORMAGIC) {
  $CFG_NAMES[$t] =~ s#\\#/#g if $^O =~ /win32/i;
  $CFG_NAMES[$t] .= '/'                if substr($CFG_NAMES[$t],-1,1) ne '/';
  $CFG_NAMES[$t]  = '/'.$CFG_NAMES[$t] if substr($CFG_NAMES[$t],1,1) ne '/';
  $CFG_NAMES[$t] =~  s#/+#/#g;
}

##
# Normalize live paths
@CFG_LIVE = map {
    s#\\#/#g if $^O =~ /win32/i;
    $_ = '/'.$_;
    s#/+#/#g;
  } @CFG_LIVE;

##
# Set preferred exceptions format
$Cvs::Repository::Exception::FORMAT = "%{|UNKNOWN EXCEPTION}m%{\nPlease, read about this event to avoid this error: }c%{\n   +++ }s";

##
# Generate dump name
if($CFG_LOADDUMPS || $CFG_SAVEDUMPS) {
  $DUMPFILE = 'cvs2svn.'.$CFG_CVSROOT.'.dmp';
  $DUMPFILE =~ s/[\\\/:]/_/g;
}

###
# Try to load dump & make all passes
if($CFG_LOADDUMPS) {
  print "--> Try to load dump from previous session... ";
  if(!-f $DUMPFILE) {
    print "No dump file\n";
  } else {
    my $st;
    $STEP_START_TIME = time();
    print ((stat($DUMPFILE))[7]," bytes ");
    eval { $st = Storable::retrieve($DUMPFILE); };
    $STEP_START_TIME = time() - $STEP_START_TIME;
    if      ($@) {
      print "Load failed, seems to format error: '$@'\n";
    } elsif (!defined $st) {
      print "Load failed, seems to I/O error\n";
    } elsif ($st->{'magic'} != $STATE->{'magic'}) {
      print "Load failed, internal format error\n";
    } else {
      print "Ok, loaded pass ",$st->{'pass'}," in $STEP_START_TIME seconds\n";
      $STATE = $st;
      ($REPO, $LN2RN, $SYMBOLS, $COMMITS) = @{$STATE->{'state'}};
      print "Files: ",scalar(keys %{$REPO}),", Symbols: ",scalar(keys %{$SYMBOLS}),", Commits: ",scalar(@{$COMMITS}),"\n";
    }
  }
}
for(my $pass = $STATE->{'pass'} + 1; $pass <= $#PASSES; ++$pass) {
  if(defined $PASSES[$pass]->{'skipcheck'} && $PASSES[$pass]->{'skipcheck'}->()) {
    print "--x Pass $pass: ",$PASSES[$pass]->{'name'}," SKIPPED.\n";
    next;
  }
  print "--> Pass $pass: ",$PASSES[$pass]->{'name'},"\n";
  $STEP_START_TIME = time();
  eval {$PASSES[$pass]->{'makepass'}->()};
  print "\n" if $CFG_PROGRESS && $PASSES[$pass]->{'progress'}; # Finish last line
  $STEP_START_TIME = time() - $STEP_START_TIME;
  if($@) {
    $WHOLE_START_TIME = time() - $WHOLE_START_TIME;
    print "\n" if $CFG_VERBOSE || $CFG_PROGRESS;
    print STDERR "--! Pass $pass: failed in $STEP_START_TIME (for $WHOLE_START_TIME total) seconds.\n";
    print STDERR "Last words was:\n";
    print STDERR "$@\n";
    exit(1);
  }

  if($CFG_SAVEDUMPS && $pass < $#PASSES) {
    my $STEP_START_TIME = time();
    print "---> Pass $pass: store results... ";
    $STATE->{'pass'} = $pass;
    if(Storable::store($STATE,$DUMPFILE.'.tmp')) {
      $STEP_START_TIME = time() - $STEP_START_TIME;
      unlink($DUMPFILE);
      rename($DUMPFILE.'.tmp',$DUMPFILE);
      print "Ok in $STEP_START_TIME seconds, ",(stat($DUMPFILE))[7]," bytes\n";
    } else {
      print "FAILED\n";
      unlink($DUMPFILE.'.tmp');
    }
  }

  my $res = '';
  if(defined $PASSES[$pass]->{'makeres'}) {
    $res = $PASSES[$pass]->{'makeres'}->();
    $res = ' '.$res if $res;
  }

  print "--< Pass $pass: done in $STEP_START_TIME seconds.",$res,"\n";
}
$WHOLE_START_TIME = time() - $WHOLE_START_TIME;
print "=== All passes finished in $WHOLE_START_TIME seconds.\n";

exit(0);

sub check_isSymbolsIgnored
{
  return exists $CFG_IGNORESYMBOLS{'*'};
}

sub pass1_LoadCVSRepository
{
  my $fc;
  if($CFG_PREALLOC || $CFG_PROGRESS) {
    print "Count number of files in repository... " if $CFG_VERBOSE;
    $fc = &pass1_helper_LoadDir($CFG_CVSROOT,'',1);
    # Pre-allocate space
    if($CFG_PREALLOC) {
      keys(%{$LN2RN}) = $fc;
      keys(%{$REPO})  = $fc;
    }
    print "$fc files was found\n" if $CFG_VERBOSE;
  }
  # Simple: recursive load of all files
  &pass1_helper_LoadDir($CFG_CVSROOT,'',0,$fc,0);
}

sub pass1_results
{
  return 'Loaded '.scalar(keys %{$REPO}).' files.';
}

sub pass2_CompleteParsing
{
  my $progress_all = scalar(keys %{$REPO});
  my $progress     = 0;

  print "Processed: $progress/$progress_all files\r" if $CFG_PROGRESS;
  foreach my $f (keys %{$REPO}) {
    print "Built internal structure for '$f'... " if $CFG_VERBOSE;

    eval { $REPO->{$f}->buildRevisionsTree(\%EVENTS); };
    condrethrow("Could not build revisions' tree for '$f'",\%EVENTS,'FileError',$f,1);
    if(exists $REPO->{$f}) {
      eval { $REPO->{$f}->checkDates(\%EVENTS); };
      condrethrow("Could not check delta tree for '$f'",\%EVENTS,'FileError',$f,1);
      eval { $REPO->{$f}->checkSymbols(\%EVENTS); };
      condrethrow("Could not check symbols for '$f'",\%EVENTS,'FileError',$f,1);
      # If file was not deleted, it processed Ok
      print "Ok\n" if $CFG_VERBOSE && exists $REPO->{$f};
    }
    # and now we should rename all invalid symbols
    if(exists $REPO->{$f}) {
      my $syms = $REPO->{$f}->symbols();
      foreach my $s (keys %{$syms}) {
        # Check symbol for invalid characters
        if($s =~ /[\\\/:\>\<\|~]/) {
          condthrow("Symbol '$s' in file '$f' contains invalid characters.",\%EVENTS,'InvalidSymName',$REPO->{$f},$REPO->{$f}->symbols($s),$s);
          my $new = $s;
          $new =~ s/([\\\/:\>\<\|~])/sprintf('%%%02x',ord($1))/eg;
          $syms->{$new} = $syms->{$s};
          delete $syms->{$s};
        }
      }
    }
    ##
    # Progress meter
    ++$progress;
    print "Processed: $progress/$progress_all files\r" if $CFG_PROGRESS;
  }
}

sub pass2_results
{
  return 'Processed '.scalar(keys %{$REPO}).' files.';
}

#
# This (with pass4) is most complex and sophisticated operation
#
# We have some problems here:
#  (a) We could not determine types of some tags, for example
#      tag could be set on trunk in one file and on vendor revision
#      on second, if second file wasn't changed after import.

#  (b) We could not determine parents of some tags, for example
#      tag could be set on branch in one file and on trunk
#      revision on second, if second file wasn't changed in this
#      branch.
#
#  This function collect FULL information about symbol and try
#  to determine it's type.
#
sub pass3_CollectSymbols
{
  # Types of tags: trunk -> tag, branch -> tag, magic -> branch, vendor tag, import
  my @tagtypes = ('t','t','b','v','i');
  my $files_count = scalar(keys %{$REPO});

  my $progress_all = scalar(keys %{$REPO});
  my $progress     = 0;

  print "Collect symbols from all files... " if $CFG_VERBOSE == 1;
  print "Processed: $progress/$progress_all files\r" if $CFG_PROGRESS;
  foreach my $f (keys %{$REPO}) {
    print "Collect symbols from '$f'... " if $CFG_VERBOSE > 1;

    my $syms = $REPO->{$f}->symbols();
    # For second processing
    my %psyms = ();
    # Speed up second processing: filter out
    # All non-magic symbols ONCE and find first changes on these magic symbols
    while(my ($sym, $rev) = each %{$syms}) {
      next unless $rev->isMagic() && !exists $CFG_IGNORESYMBOLS{$sym};
      my $d = $REPO->{$f}->deltas($rev->getFirstRevision());
      $psyms{$sym} = [$rev,$rev->getBP(),(defined $d)?$d->date():0x7FFFFFFF];
    }
    # Reset iterator
    scalar(values %{$syms});
    while(my ($sym, $rev) = each %{$syms}) {

      # Ignore vendor branches at start of processing
      # We could not ignore vendor tags on this stage: may be they will become
      # simple tags in future
      $CFG_IGNORESYMBOLS{$sym} = 1 if $CFG_IGNOREVENDOR && $rev->type() == ST_VENDORMAGIC;
      if(exists $CFG_IGNORESYMBOLS{$sym}) {
        print "IGNORE '$sym' " if $CFG_VERBOSE > 1;
        next;
      }

      # Ignore files, which is ADDED to branches, not BRANCHED
      if($rev->isMagic()) {
        my $d = $REPO->{$f}->deltas($psyms{$sym}->[1]); # Get delta by BP
        if($d->state() eq 'dead') {
          print "NOT BRANCHED in '$sym' " if $CFG_VERBOSE > 1;
          next;
        }
      }

      # Search for ALL possible parents of this symbol (tag/branch)
      my @parents = ();
      my @changed = ();
      my @add;

      if      ($rev->type() == ST_TRUNK || $rev->type() == ST_BRANCH) {
        # If it is trunk or branch revision, it is TAG.
        # There could be different cases with parent:
        # (a) This tag could be set on branch with revision of tag,
        #     for example, on HEAD if revision is 1.3 or on branch
        #     1.4.0.4 for revision 1.4.4.5
        # (b) This tag could be set on branch for which this revision
        #     is parent. So, tag with revision 1.2 could be tag on branch
        #     1.2.0.2 or 1.2.0.4, and so on.

        # (a) All names of this branch

        # Filter HERE to make consistent @changed
        @add = grep {$_ eq 'HEAD' || !exists $CFG_IGNORESYMBOLS{$_}} $REPO->{$f}->getBranchName($rev);
        push @parents,@add if @add && defined $add[0];
        # And add as many 'next revision' dates
        my $d;
        $d = $REPO->{$f}->deltas($rev->getNext());
        $d = (defined $d)?$d->date():0x7FFFFFFF;
        push @changed, map {$d} @parents;

        # (b) Names of all branches from this revision (include vendor ones)
        #     We could not check revision's branches, because some branches
        #     could have no files at all and be mentioned only in symbols.
        while(my ($s,$r) = each %psyms) {
          next unless $r->[1] == $rev && !exists $CFG_IGNORESYMBOLS{$s};

          push @parents, $s;
          push @changed, $r->[2];
        }
      } elsif ($rev->type() == ST_BRANCHMAGIC) {
        # It is creation of branch. Parent could be:
        #  (a) Branch point
        #      So, branch 1.5.0.6 could be created from HEAD (rev 1.5),
        #      and 1.6.6.0.2 could be created from 1.6.0.6
        #  (b) Any branch with same root (!). It is possible in case of
        #      branch-on-branch when file wasn't changed on first branch
        #      So, 1.5.0.6 could be created from 1.5.0.4 or 1.5.0.2 too.
        #  (c) Very special case: if branch point is vendor revision, add
        #      HEAD too.

        # (a) Branch point
        @add = grep {$_ eq 'HEAD' || !exists $CFG_IGNORESYMBOLS{$_}} $REPO->{$f}->getBranchName($rev->getBP());
        push @parents,@add if @add && defined $add[0];

        # (b) Sibling branches
        my $bp = $rev->getBP();
        while(my ($s,$r) = each %psyms) {
          next unless $r->[0] != $rev && $r->[1] == $bp && !exists $CFG_IGNORESYMBOLS{$s};
          push @parents, $s;
        }

        # (c) Need HEAD?
        push @parents,'HEAD' if $rev->getBP()->type() == ST_VENDOR;
      } elsif ($rev->type() == ST_VENDOR) {
        # This is vendor tag. Parent could be
        #  (a) Vendor branch
        #      So, tag for revision 1.1.1.2 could be import tag for vendor 1.1.1
        #  (b) Head: no changes after export was done
        #      So, tag on revision 1.1.1.1 could be, really, tag on HEAD.
        #  (c) All branches from this revision
        #      So, tag on 1.1.1.1 could be really tag on branch 1.1.1.1.0.2

        # (a) All names of vendor branch
        @add = grep {$_ eq 'HEAD' || !exists $CFG_IGNORESYMBOLS{$_}} $REPO->{$f}->getBranchName($rev);
        push @parents,@add if @add && defined $add[0];
        # And add as many 'next revision' dates
        my $d;
        $d = $REPO->{$f}->deltas($rev->getNext());
        $d = (defined $d)?$d->date():0x7FFFFFFF;
        push @changed, map {$d} @parents;

        # (b) Head
        push @parents, 'HEAD';
        push @changed, $d;

        # (c) All branches from this point
        while(my ($s,$r) = each %psyms) {
          next unless $r->[1] == $rev && !exists $CFG_IGNORESYMBOLS{$s};

          push @parents, $s;
          push @changed, $r->[2];
        }
      } elsif ($rev->type() == ST_VENDORMAGIC) {
        # This is Vendor Magic. Parent could be only one: HEAD
        push @parents, 'HEAD';
      }

      # check: should we skip this symbol as tag on skipped branch(es)?
      if(!@parents) {
        print "IGNORE '$sym' " if $CFG_VERBOSE > 1;
        # Speed up things
        delete $SYMBOLS->{$sym} if exists $SYMBOLS->{$sym};
        $CFG_IGNORESYMBOLS{$sym} = 1;
        next;
      }

      # Parent(s) found, Ok

      # Type of symbol
      my $type = $tagtypes[$rev->type()];
      
      # And add all this mess into symbol description.
      $SYMBOLS->{$sym} = {
      	'name'         => $sym,
      	'allfiles'     => 0,
      	'date'         => 0,
      	'firstchanged' => 0x7FFFFFFF } if !exists $SYMBOLS->{$sym};


      # Make shortcut -- Symbol Description
      my $sd = $SYMBOLS->{$sym};

      # Type of one tag should be consistent over repository
      # Only one exception allowed: vendor tag could become
      # simple tag.
      if(!exists $sd->{'type'}) {
        $sd->{'type'} = $type;
      } else {
        if      ($sd->{'type'} eq $type) {
          # No Op
        } elsif ($sd->{'type'} eq 't' && $type eq 'v') {
          # Make it TAG
          $type = 't';
        } elsif ($sd->{'type'} eq 'v' && $type eq 't') {
          # Make it TAG
          $sd->{'type'} = 't';
        } else {
          condthrow("Symbol '$sym' have type '$type' in '$f' and type '".$sd->{'type'}."' in other files",\%EVENTS,'DiffSymType',$f,$sym,$type,$sd->{'type'});
          # If we are here, create (or get) new symbol, with type suffix...
	      $SYMBOLS->{$sym.'.'.$type} = {
            'name'         => $sym,
            'type'         => $type,
            'allfiles'     => 0,
            'date'         => 0,
            'firstchanged' => 0x7FFFFFFF } if !exists $SYMBOLS->{$sym.'.',$type};
          $sd = $SYMBOLS->{$sym.'.'.$type};
        }
      }
      # Add file
      ++$sd->{'allfiles'};

      # Calculate date for this file on this symbol
      my $date = 0;
      my $fch  = 0x7FFFFFFF;
      if     ($sd->{'type'} eq 'b' || $sd->{'type'} eq 'i') {
        # Get revision of this file by BP. BP could be found at %psyms :)
        $date = $REPO->{$f}->deltas($psyms{$sym}->[1])->date();
        # And firstchanged could be found in psyms -- date of first revision of
        # this file on this branch
        my $d = $REPO->{$f}->deltas($rev->getFirstRevision());
        $fch = $d->date() if defined $d;
      } elsif($sd->{'type'} eq 't' || $sd->{'type'} eq 'v') {
        # Date is date of revision
        $date = $REPO->{$f}->deltas($rev)->date();
        # First changing is different and depends on parent...
      }

      $sd->{'files'} = {} unless exists $sd->{'files'};
      # Ok, type is normalized. Put this file with all possible parents
      foreach my $p (@parents) {
        # For tags first change depend on parent. And is +oo for HEAD tags
        # First change for tags is NEXT revision on it's branch
        # Each parent is branch.
        $fch  = shift @changed  unless $sd->{'type'} eq 'b' || $sd->{'type'} eq 'i';
        # We need to make big array: it speeds up processing
        $sd->{'files'}->{$p} = &helper_CreateArray($files_count) unless exists $sd->{'files'}->{$p};
        push @{$sd->{'files'}->{$p}}, [$REPO->{$f},$date,$fch];
        $sd->{'firstchanged'} = $fch  unless $fch  > $sd->{'firstchanged'};
        $sd->{'date'}         = $date unless $date < $sd->{'date'};
      }
    }
    print "Ok\n" if $CFG_VERBOSE > 1;
    ##
    # Progress meter
    ++$progress;
    print "Processed: $progress/$progress_all files\r" if $CFG_PROGRESS;
  }
  print "Ok\n" if $CFG_VERBOSE == 1;
  # Filter out all vendor tags
  if($CFG_IGNOREVENDOR) {
    print "Filter out all vendor tags... " if $CFG_VERBOSE;
    print "\n" if $CFG_VERBOSE > 1;
    foreach my $s (keys %{$SYMBOLS}) {
      print "Check $s... " if $CFG_VERBOSE > 1;
      if($SYMBOLS->{$s}->{'type'} eq 'v') {
        print "Delete\n" if $CFG_VERBOSE > 1;
        delete $SYMBOLS->{$s};
      } else {
        print "Ok\n" if $CFG_VERBOSE > 1;
      }
    }
    print "Ok\n" if $CFG_VERBOSE == 1;
  } else {
    $SYMBOLS->{'HEAD'} = {'type' => 'x'};
    print "Filter out all non-vendor parents from vendor tags... " if $CFG_VERBOSE;
    print "\n" if $CFG_VERBOSE > 1;
    foreach my $s (keys %{$SYMBOLS}) {
      next unless $SYMBOLS->{$s}->{'type'} eq 'v';
      print "Check $s... " if $CFG_VERBOSE > 1;
      foreach my $p (keys %{$SYMBOLS->{$s}->{'files'}}) {
        delete $SYMBOLS->{$s}->{'files'}->{$p} unless $SYMBOLS->{$p}->{'type'} eq 'i';
      }
      print "Ok\n" if $CFG_VERBOSE > 1;
    }
    print "Ok\n" if $CFG_VERBOSE == 1;
    delete $SYMBOLS->{'HEAD'};
  }
}

sub pass3_results
{
  return 'Found '.scalar(keys %{$SYMBOLS}).' symbols.';
}

#
# This (with pass3) is most complex and sophisticated operation
#
#  This function try to determine symbols' parents. Symbol could
#  have different parents for different files, but here are
#  conflict situations:
#   (a) Symbol have completely unrelated branches as parents
#   (b) Symbol is branch & tag in one time
#   (c) Symbol is vendor branch & something other in one time
#   ...
sub pass4_NormalizeSymbols
{
  # Store "raw" symbols
  my $symbols = $SYMBOLS;
  my $pass = 0;
  my $weak = 0;
  my @syms;

  my $progress_all = scalar(keys %{$symbols});

  # We will put normalized symbols here
  $SYMBOLS = {};

  # Add fictive symbol 'HEAD': only type, for parent's sorting
  $SYMBOLS->{'HEAD'} = {'type' => 't'};

  # Store for iterations-in-iterations
  @syms = keys %{$symbols};

  # Now we should build tree of revisions.
  # Why tree? Because we could get not-trivial ignores of tags & branches
  # We'll cache possible parents, but will proceed only when all possible parents
  # are resolved OR ignored OR manually set. If here is dead loop, we'll
  # ask user to break it.
  print "Processed: ",scalar(keys %{$SYMBOLS})-1,"/$progress_all symbols\r" if $CFG_PROGRESS;
  while(keys %{$symbols}) {
    ++$pass;
    print "Normalizing pass $pass... " if $CFG_VERBOSE;
    print "\n" if $CFG_VERBOSE > 1;
    # If this flag is 0 on exit from inner loop, we have problems
    my $changed = 0;
    while (my ($sym,$sd) = each %{$symbols}) {
      print "Try to add '$sym'... " if $CFG_VERBOSE > 1;

      if(&pass4_helper_CheckIgnoredParents($sd)) {
        print "Ignored, some files was lost with parents\n" if $CFG_VERBOSE > 1;
        $CFG_IGNORESYMBOLS{$sym} = 1;
        delete $symbols->{$sym};
        $changed = 1;
        next;
      }

      # May be, we could use user-selected parent?
      if     (defined ($sd->{'_pp'} = &pass4_helper_UserSelectedParent($sym,$sd,$SYMBOLS,$weak))) {
        if(!@{$sd->{'_pp'}}) {
          print "Skip for next pass, maybe user-selected parent\n" if $CFG_VERBOSE > 1;
          next;
        } else {
          print "Could use user-selected parent '",$sd->{'_pp'}->[0],"'... " if $CFG_VERBOSE > 1;
        }
      } elsif(defined($sd->{'_pp'} = &pass4_helper_OneParent($sym,$sd,$SYMBOLS,$weak))) {
        if(!@{$sd->{'_pp'}}) {
          print "Skip for next pass, maybe one parent\n" if $CFG_VERBOSE > 1;
          next;
        } else {
          print "Could use one parent '",$sd->{'_pp'}->[0],"'... " if $CFG_VERBOSE > 1;
        }
      } elsif(defined($sd->{'_pp'} = &pass4_helper_FullCoveringParent($sym,$sd,$SYMBOLS,$weak))) {
        if(!@{$sd->{'_pp'}}) {
          print "Skip for next pass, maybe full-covering parent\n" if $CFG_VERBOSE > 1;
          next;
        } else {
          print "Could use full-covering parent '",$sd->{'_pp'}->[0],"'... " if $CFG_VERBOSE > 1;
        }
      } elsif(defined($sd->{'_pp'} = &pass4_helper_CoveringSetOfParents($sym,$sd,$SYMBOLS,$weak))) {
        if(!@{$sd->{'_pp'}}) {
          print "Skip for next pass, maybe set of parents\n" if $CFG_VERBOSE > 1;
          next;
        } else {
          print "Could use full-covering set of parents '",join("','",@{$sd->{'_pp'}}),"'... " if $CFG_VERBOSE > 1;
        }
      } else {
        # No possible parents found: cond throw
        condthrow("Could not find single parent for '$sym', candidates was: '".join("','",keys %{$sd->{'files'}})."'",\%EVENTS,'NoSymParent',$sym);
        print "Ignored, no possible parents left\n" if $CFG_VERBOSE > 1;
        $CFG_IGNORESYMBOLS{$sym} = 1;
        # Delete this symbol
        delete $symbols->{$sym};
        # Go to next symbol
        $changed = 1;
        next;
      }
      print "Ok\n" if $CFG_VERBOSE > 1;
      $SYMBOLS->{$sym} = {
        'name'         => $sym,
        'type'         => $sd->{'type'},
        'date'         => $sd->{'date'},
        'firstchanged' => $sd->{'firstchanged'},
        'files'        => { map { $_ => $sd->{'files'}->{$_} } @{$sd->{'_pp'}} },
        'allfiles'     => $sd->{'allfiles'},
      };
      # And delete from un-processed
      delete $symbols->{$sym};
      $sd = undef;
      $changed = 1;
      # Don't apply filter, if symbol is vendor branch: it will be created differently and dates
      # are not valid
      next if $SYMBOLS->{$sym}->{'type'} eq 'i';
      # We could now filter out all impossible parents from not-added symbols
      # It should be done by time, when file is added to parent, and when -- to child
      foreach my $s (grep {exists $symbols->{$_} && exists $symbols->{$_}->{'files'}->{$sym}} @syms) {
        next unless $symbols->{$s}->{'date'} < $SYMBOLS->{$sym}->{'date'};
        my $oaf  = $symbols->{$s}->{'allfiles'};
        print "'$sym' is younger and could not be parent of '$s'.\n" if $CFG_VERBOSE > 1;
        delete $symbols->{$s}->{'files'}->{$sym};
        &pass4_helper_GetAllFiles($symbols->{$s});
        if($oaf != $symbols->{$s}->{'allfiles'}) {
          print "Ignored '$s', some files was lost with parent '$sym'\n" if $CFG_VERBOSE > 1;
          $CFG_IGNORESYMBOLS{$s} = 1;
          delete $symbols->{$s};
        }
        # Recalc date
        &pass4_helper_AddDate($symbols->{$s}) if exists $symbols->{$s};
      }
    }
    # Check: may be dead loop?
    if(!$changed) {
      if($CFG_SYMWEAK && !$weak) {
        print "Ok\n" if $CFG_VERBOSE == 1;
        print "Switching to WEAK mode: no symbols could be added in strict mode on this pass\n" if $CFG_VERBOSE;
        $weak = 1;
      } else {
        # Problems: this pass could add or ignore nothing :(
        throw("Could not add any symbols in tree on pass $pass. Symbols left: '".join("','",keys %{$symbols})."'. Please, use '-sp' options");
      }
    } else {
      print "Ok\n" if $CFG_VERBOSE == 1;
    }

    ##
    # Progress meter
    print "Processed: ",scalar(keys %{$SYMBOLS})-1,"/$progress_all symbols\r" if $CFG_PROGRESS;
  }

  # Undef processed
  undef $symbols;
  # Delete 'Head'
  delete $SYMBOLS->{'HEAD'};

  # Print all symbols in verbose mode
  if($CFG_VERBOSE) {
    my %types = (
      't' => 'tag',
      'b' => 'branch',
      'v' => 'vendor tag',
      'i' => 'vendor branch'
    );
    print "Symbols:\n";
    printf("  %-25s %-13s %-7s %-6s %s\n",'Name','Type','Parents','Files','Date');
    foreach my $s (sort keys %{$SYMBOLS}) {
      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($SYMBOLS->{$s}->{'date'});
      printf("  %-25s %-13s %7d %6d %02d/%02d/%04d %02d:%02d:%02d\n",
        $s,
        $types{$SYMBOLS->{$s}->{'type'}},
        scalar(keys %{$SYMBOLS->{$s}->{'files'}}),
        $SYMBOLS->{$s}->{'allfiles'},
        $mday,$mon + 1, $year + 1900,
        $hour,$min,$sec
      );
    }
  }
  # And dirty hack: change state
  undef $STATE->{'state'}->[2];
  $STATE->{'state'}->[2] = $SYMBOLS;
}

sub pass4_results
{
  return 'Normalized '.scalar(keys %{$SYMBOLS}).' symbols.';
}

sub pass5_CollectCommits
{
  # Process all files & all deltas in files
  my %CBA = ();
  my $groupscount = 0;
  my $filescount  = scalar(keys %{$REPO});

  my $progress_all = $filescount;
  my $progress     = 0;

  print "Collect all commits from all files by author and message... " if $CFG_VERBOSE == 1;
  print "Processed: $progress/$progress_all files\r" if $CFG_PROGRESS;
  foreach my $f (keys %{$REPO}) {
    print "Collect checkins from '$f'... " if $CFG_VERBOSE > 1;

    my $ds = $REPO->{$f}->deltas();
    foreach my $d (values %{$ds}) {
      # Skip dead 'root of tree' revisions: it is fictive commit!
      # This file will be used on branch, not on head
      next unless $d != $REPO->{$f}->tree() || $d->state() ne 'dead';
      # Check, is it dead-after-dead revision?
      next unless $d->state() ne 'dead' || $d->treePrev()->state() ne 'dead';
      # Check, is it unnamed branch?
      next unless $REPO->{$f}->getBranchName($d->rev());

      my $a = $d->author();
      my $l = $d->log();
      $CBA{$a} = {} unless exists $CBA{$a};
      $CBA{$a}->{$l} = &helper_CreateArray($filescount) unless exists $CBA{$a}->{$l};

      # Push special element: file & processed delta from it
      push @{$CBA{$a}->{$l}}, [$d,$REPO->{$f}];
      ++$groupscount;
    }
    print "Ok\n" if $CFG_VERBOSE > 1;
    ##
    # Progress meter
    ++$progress;
    print "Processed: $progress/$progress_all files\r" if $CFG_PROGRESS;
  }
  print "Ok\n" if $CFG_VERBOSE == 1;

  # Now we make @{$COMMITS} big enough. It is really speed-up processing
  &helper_ResizeArray($COMMITS,$groupscount * 1.5);

  # Ok, grouping done

  print "\n" if $CFG_PROGRESS;
  $progress     = 0;
  $progress_all = 0;
  map { $progress_all += scalar(keys %{$_}) } values %CBA if $CFG_PROGRESS;
  print "Processed: $progress/$progress_all commits groups\r" if $CFG_PROGRESS;
  foreach my $auth (keys %CBA) {
    print "Collect checkins for committer '$auth'... " if $CFG_VERBOSE;
    print "\n" if $CFG_VERBOSE > 1;

    foreach my $l (keys %{$CBA{$auth}}) {
      my $groups = 0;
      my $files = 0;
      if($CFG_VERBOSE > 1) {
        my $msg = $l;
        $msg =~ s/\r?\n/ /g;
        $msg =~ s/\s+/ /g;
        $msg = substr($msg,0,17).'...' if length($msg) > 20;
        print "  Check log message '$msg'... ";
      }
      my @commits = sort { $a->[0]->date() <=> $b->[0]->date() } @{$CBA{$auth}->{$l}};
      # Free memory
      $CBA{$auth}->{$l} = undef;
      while(@commits) {
        my @group = ( shift @commits );
        my $bd    = $group[0]->[0]->date();
        my $vnd   = $group[0]->[0]->rev()->isVendor();
        my $brn   = $group[0]->[1]->getBranchName($group[0]->[0]->rev()) || 'HEAD';
        my $cd    = $bd;
        my $i;
        my %filewas = ();

        $filewas{$group[0]->[1]} = 1;

        # Skip this commit if it is goes into skipped branch
        next if exists $CFG_IGNORESYMBOLS{$brn} or ($brn ne 'HEAD' and exists $CFG_IGNORESYMBOLS{'*'});

        ++$files;

        $i = 0;
        while($i < @commits) {
          # check all commits
          my $d  = $commits[$i]->[0];
          my $f  = $commits[$i]->[1];
          my $lb = $f->getBranchName($d->rev()) || 'HEAD';
          my $ingroup = 1;

          # File was not committed in this commit already
          $ingroup &= not exists $filewas{$f};
          # Commit timeout
          # Commit in same symbol
          $ingroup &= $lb eq $brn;
          # Commit of same type: vendor/not-vendor
          $ingroup &= $vnd == $d->rev()->isVendor();
          # SKIP this test if it is IMPORT
          $ingroup &= abs($bd - $d->date()) < $CFG_TIMEOUT unless $vnd;

          # No more checks
          if($ingroup) {
            $filewas{$f} = 1;
            push @group,$commits[$i];
            $cd = $d->date() if $cd > $d->date();
            # Remove from commits
            splice(@commits,$i,1);
            ++$files;
          } else {
            ++$i;
          }
        }

        my $type;
        if($vnd) {
          $type = 'i';
        } else {
          # Is this loading of 1.1 from branches?
          my $first = 1;
          my $vbs = '';
          # First revision in HEAD if it is root of tree and have vendor branch from it
          # This group is DEAD of it is HEAD and dead is ROOT of revisions tree.
          # We don't need to add DEAD groups at all -- it is files, added to branch
          foreach my $g (@group) {
            $first &= $g->[0] == $g->[1]->tree();
            if($first && $g->[0]->treeBranches()) {
              my $hv = 0;
              my @brs = $g->[0]->treeBranches();
              foreach my $b (@brs) {
                # TODO: Work with multiple vendors
                if($b->rev()->isVendor()) {
                  $hv = 1;
                  $vbs = $vbs || $g->[1]->symbols($b->rev()->getMagic());
                }
                last if $hv;
              }
              $first &= $hv;
            } else {
              # No branches: we could not skip this!
              $first = 0;
            }
            last unless $first;
          }
          # If YES, skip this commit, we don't need version 1.1 after import
          # But if vendor branch is ignored, don't skip this commit
          if($first && defined $vbs && $vbs && !exists $CFG_IGNORESYMBOLS{$vbs} && !exists $CFG_IGNORESYMBOLS{'*'}) {
            $type = '';
          } else {
            $type = 'c';
          }
        }
        # Skip `VENDOR -> 1.1' commit, it will be created as additional to import
        next unless $type;
        # Group is ready
        push @{$COMMITS}, {
          'date'    => $cd,
          'author'  => $auth,
          'log'     => $l,
          'type'    => $type,
          'content' => [ map { [$_->[0]->rev(), $_->[1]] } @group ],
          'branch'  => $brn
        };
        ++$groups;
        # And add one more commit: copy to trunk, if it was import.
        # Copied to trunk should be ALL files, which have '1.2' revision
        # created later, than this import (or never)
        if('i' eq $type) {
          # Filter out all needed files
          @group = grep {
                          my $nxt = $_->[1]->tree()->treeNext();
                          (!defined $nxt) || ($nxt->date() > $_->[0]->date());
                        } @group;
          if(@group) {
            push @{$COMMITS}, {
              'date'    => $cd,
              'author'  => $auth,
              'log'     => "Commit generated by refinecvs to copy imported files to trunk",
              'type'    => 'r',
              'content' => [ map { [$_->[0]->rev(), $_->[1]] } @group ],
              'branch'  => $brn
            };
            ++$groups;
          }
        }
      }
      print "Ok, $groups groups with $files files found\n" if $CFG_VERBOSE > 1;
      ##
      # Progress meter
      ++$progress;
      print "Processed: $progress/$progress_all commits groups\r" if $CFG_PROGRESS;
    }
    print "Ok\n" if $CFG_VERBOSE == 1;
  }

  return;
}

sub pass5_results
{
  return 'Found '.scalar(@{$COMMITS}).' commits.';
}

sub pass6_MakeTags
{
  my %logtypes = ('t' => 'tag', 'b' => 'branch', 'v' => 'vendor tag', 'i' => 'vendor branch');

  my $progress_all = scalar(keys %{$SYMBOLS});
  my $progress     = 0;

  print "Processed $progress/$progress_all symbols\r" if $CFG_PROGRESS;
  foreach my $s (keys %{$SYMBOLS}) {
    print "Add commits for symbol '$s'... " if $CFG_VERBOSE;
    # Strip tag type.
    my ($sname) = split(/\./,$s);

    my $logtype  = $logtypes{$SYMBOLS->{$s}->{'type'}};
    my $auth = '';

    # Now we should one or more commits from each symbol.
    # For tags here will be one commit, and for branches
    # one ore more: files could be added in many passes.

    # If date of last file added is great than first file
    # on branch was changed, make multiple commits from
    # this branch
    if($SYMBOLS->{$s}->{'type'} eq 'i') {
      # No work for vendor branch creation.
      # Import commit will do all work for us
      print "Vendor branch, skipped... " if $CFG_VERBOSE;
    } else {
      print "$logtype... " if $CFG_VERBOSE;
      # Split symbol to many add-commits: before first change, before second, etc...
      my @byadd    = ();
      my @bychange = ();
      my $commits = 0;

      # Iterate over all files and put it into array as special structures
      foreach my $p (keys %{$SYMBOLS->{$s}->{'files'}}) {
        foreach my $f (@{$SYMBOLS->{$s}->{'files'}->{$p}}) {
          my $d;
          if($SYMBOLS->{$s}->{'type'} eq 'b') {
            $d = $f->[SE_FILE]->deltas($f->[SE_FILE]->symbols($sname)->getBP());
          } else {
            $d = $f->[SE_FILE]->deltas($f->[SE_FILE]->symbols($sname));
          }
          if($SYMBOLS->{$s}->{'type'} eq 'b' || $SYMBOLS->{$s}->{'type'} eq 't') {
            # Special variant: BRANCH or TAG, vendor revision, and vendor branch is IGNORED
            $d = $f->[SE_FILE]->tree() unless !$d->rev()->isVendor() || !(exists $CFG_IGNORESYMBOLS{$f->[SE_FILE]->getBranchName($d->rev())} || exists $CFG_IGNORESYMBOLS{'*'});
          }
          # Skip files from dead revision. It should not be here, but...
          next if $d->state() eq 'dead';
          my $nf = {'f' => $f, 'd' => $d, 'p' => $p, 'a' => 0};
          push @byadd,   $nf;
          push @bychange,$nf;
        }
      }
      # Ok, sort array by date & by first change
      @byadd    = sort {
                         $a->{'f'}->[SE_ADDED] <=> $b->{'f'}->[SE_ADDED] ||
                         $a->{'f'}->[SE_FILE] cmp $b->{'f'}->[SE_FILE]
                       } @byadd;
      @bychange = sort {
                         $a->{'f'}->[SE_FIRSTCHANGED] <=> $b->{'f'}->[SE_FIRSTCHANGED] ||
                         $a->{'f'}->[SE_FILE] cmp $b->{'f'}->[SE_FILE]
                       } @bychange;

      my $firstpass = 1;
      while(@byadd) {
        my %c;
        my $f;
        my $date;
        # Add all files, which could be added before nearest change
        $auth = $byadd[0]->{'d'}->author();
        while(($f = shift @byadd) && $f->{'f'}->[SE_ADDED] <= $bychange[0]->{'f'}->[SE_FIRSTCHANGED]) {
          # Mark as added
          $f->{'a'} = 1;
          # Add
          $c{$f->{'p'}} = &helper_CreateArray(scalar(@byadd)) unless exists $c{$f->{'p'}};
          # Push file description
          push @{$c{$f->{'p'}}},[$f->{'d'}->rev(), $f->{'f'}->[SE_FILE]];

          $date = $f->{'f'}->[SE_ADDED];
          $auth = '' unless $auth eq $f->{'d'}->author();
          # We don't should remove these files from changed array
        }
        $auth = $CFG_TAGAUTHOR unless $auth;
        # Put not added file back
        unshift @byadd,$f if defined $f;
        # Skip added file from "changed" array, they don't should interrupt process now.
        @bychange = grep { !$_->{'a'} } @bychange;
        # Test: was some files added?
        throw("File '".$LN2RN->{$bychange[0]->{'f'}->[SE_FILE]->name()}."' was changed (".$bychange[0]->{'f'}->[SE_FIRSTCHANGED].") on '$s' before adding (".$bychange[0]->{'f'}->[SE_ADDED].")") if !keys %c;;
        # And add new commit
        my $log;
        if($firstpass) {
          $log = "Commit generated by refinecvs to create $logtype '$s'";
          $firstpass = 0;
        } else {
          $log = "Commit generated by refinecvs to add files to $logtype '$s'";
        }
        ++$commits;
        if(1 == scalar(keys %c)) {
          push @{$COMMITS}, {
            'date'    => $date,
            'author'  => $auth,
            'log'     => $log,
            'type'    => $SYMBOLS->{$s}->{'type'},
            'content' => [ @{(values %c)[0]} ],
            'tag'     => $sname,
            'branch'  => (keys %c)[0]
          };
        } else {
          push @{$COMMITS}, {
            'date'    => $date,
            'author'  => $auth,
            'log'     => $log,
            'type'    => $SYMBOLS->{$s}->{'type'},
            'content' => { %c },
            'tag'     => $sname,
          };
        }
      }
      print "Created in $commits commit(s)... " if $CFG_VERBOSE;
    }
    print "Ok\n" if $CFG_VERBOSE;
    ##
    # Progress meter
    ++$progress;
    print "Processed $progress/$progress_all symbols\r" if $CFG_PROGRESS;
  }
  # Remove ALL files from symbol, we will need memory in future
  foreach my $s (keys %{$SYMBOLS}) {
    # Store parents
    $SYMBOLS->{$s}->{'prn'} = [keys %{$SYMBOLS->{$s}->{'files'}}];
    foreach my $p (keys %{$SYMBOLS->{$s}->{'files'}}) {
      # Delete files array
      $#{$SYMBOLS->{$s}->{'files'}->{$p}} = -1;
      $SYMBOLS->{$s}->{'files'}->{$p} = undef;
      delete $SYMBOLS->{$s}->{'files'}->{$p};
    }
  }
}

sub pass6_results
{
  return 'Result is '.scalar(@{$COMMITS}).' commits.';
}

sub pass7_SortCommits
{
  print "Sort commits by dates..." if $CFG_VERBOSE;
  @{$COMMITS} = sort pass7_helper_CompareCommits @{$COMMITS};
  print " Ok\n" if $CFG_VERBOSE;

  print "Start to check revisions sequence..." if $CFG_VERBOSE == 1;
  
  my $checked_commits   = &helper_CreateArray($#{$COMMITS} + 1);
  my %revisions_checked = ();
  my %revisions_waited  = ();
  my $disorder_commits  = 0;
  
  my $progress     = 0;
  my $progress_all = 0;
  
  # First of all, we need to store
  # all revisions, which will be used
  # in branching, tagging and 'copy to trunk' processes
  print "Build blocking map..." if $CFG_VERBOSE > 1;
  $progress     = 0;
  $progress_all = scalar(@{$COMMITS});
  print "Build blocking map: $progress/$progress_all commits\r" if $CFG_PROGRESS;
  foreach my $c (@{$COMMITS}) {
    # 'change' commits doesn't block any revisions  
    next if $c->{'type'} eq 'c';
    # 'import' commits doesn't block any revisions
    next if $c->{'type'} eq 'i';
    
    # All other types: 'b'ranch, 't'ag and copy to t'r'unk blocks
    # used revisions
    foreach my $rev_file (@{$c->{'content'}}) {
      my ($rev,$file) = @{$rev_file};
      my $rn          = $LN2RN->{$file->name()};
      $revisions_waited{$rn}         = {} unless exists $revisions_waited{$rn};
      $revisions_waited{$rn}->{$rev} = 0  unless exists $revisions_waited{$rn}->{$rev};
      ++$revisions_waited{$rn}->{$rev}; # How many commits aith this revision
    }
    ++$progress;
    print "Build blocking map: $progress/$progress_all commits\r" if $CFG_PROGRESS;
  }
  print "\n" if $CFG_PROGRESS;
  print " Ok\n" if $CFG_VERBOSE > 1;

  # Now get commits one by one and check them
  print "Check commits sequence..." if $CFG_VERBOSE > 1;
  $progress     = 0;
  $progress_all = scalar(@{$COMMITS});
  print "Check commits sequence: $progress/$progress_all commits ready\r" if $CFG_PROGRESS;
  while(@{$COMMITS}) {
    my $commit = 0;
    # Find first situable commit
    while($commit < @{$COMMITS}) {
      my $c = $COMMITS->[$commit];
      my $r = 1; #Ready?
      
      if     ($c->{'type'} eq 'c') { # This is change
        foreach my $rev_file (@{$c->{'content'}}) {
          my ($crv,$file) = @{$rev_file};
          my $rn  = $LN2RN->{$file->name()};

          # Branch of this revision
          my $cbr = $crv->getMagic() || 'HEAD';
          
          # Previous revision (this change need this one!)
          my $prv = $file->deltas($crv)->treePrev(); $prv &&= $prv->rev();
          # Previous branch
          my $pbr = $prv? ($prv->getMagic() || 'HEAD') : '';
          
          
          # Check: no such file here and we NEED previous revision
          if($prv && ! (exists $revisions_checked{$rn} && exists $revisions_checked{$rn}->{$pbr})) {
            $r = 0;
            last;
          }
          # Check: file is here, but here is OTHER previous revision
          if($prv && $revisions_checked{$rn}->{$pbr} != $prv) {
            $r = 0;
            last;
          }
          # Check: may be, current revision is blocked?
          if($prv && exists $revisions_waited{$rn}->{$prv} && $revisions_waited{$rn}->{$prv}) {
            $r = 0;
            last;
          }
          # Everything is ok
        }
      } elsif($c->{'type'} eq 'i') { # This is change-or-add
        foreach my $rev_file (@{$c->{'content'}}) {
          my ($crv,$file) = @{$rev_file};
          my $rn  = $LN2RN->{$file->name()};

          # Branch of this revision
          my $cbr = $crv->getMagic() || 'HEAD';
          
          # Previous revision (this change need this one!)
          my $prv = $file->deltas($crv)->treePrev()->rev();
          # Previous branch
          my $pbr = $prv->getMagic() || 'HEAD';
          
          # First case: it is INITIAL import (1.1.1.1 and like this)
          if     ($prv == $file->tree()->rev()) {
            # Nothing can stop this
          } else {
            # Check as simple chnage: we need previous import!
            if(!(exists $revisions_checked{$rn} && exists $revisions_checked{$rn}->{$pbr}) || $revisions_checked{$rn}->{$pbr} != $prv) {
              $r = 0;
              last;
            }
            if(exists $revisions_waited{$rn}->{$prv} && $revisions_waited{$rn}->{$prv}) {
              $r = 0;
              last;
          }
          }
        }
      } elsif($c->{'type'} eq 'b' || $c->{'type'} eq 't' || $c->{'type'} eq 'r') { # This is access
        foreach my $rev_file (@{$c->{'content'}}) {
          my ($rev,$file) = @{$rev_file};
          my $rn          = $LN2RN->{$file->name()};
          my $brn         = $rev->getMagic() || 'HEAD';
          if(!(exists $revisions_checked{$rn} && exists $revisions_checked{$rn}->{$brn}) || $revisions_checked{$rn}->{$brn} != $rev) {
            $r = 0;
            last;
          }
        }
      }
      if($r) {
        last;
      } else {
        ++$commit;
      }
    }
    if($commit < @{$COMMITS}) {
      my $c = $COMMITS->[$commit];
      push @{$checked_commits}, $c;
      splice(@{$COMMITS},$commit,1);
      # And process this commit
      if     ($c->{'type'} eq 'c' || $c->{'type'} eq 'i') { # This is change
        foreach my $rev_file (@{$c->{'content'}}) {
          my ($rev,$file) = @{$rev_file};
          my $rn          = $LN2RN->{$file->name()};
          my $brn         = $rev->getMagic() || 'HEAD';
          
          $revisions_checked{$rn}         = {} unless exists $revisions_checked{$rn};
          $revisions_checked{$rn}->{$brn} = $rev;
        }
      } elsif($c->{'type'} eq 'b' || $c->{'type'} eq 't') { # This is unblock
        foreach my $rev_file (@{$c->{'content'}}) {
          my ($rev,$file) = @{$rev_file};
          my $rn          = $LN2RN->{$file->name()};
          --$revisions_waited{$rn}->{$rev};
        }
      } elsif($c->{'type'} eq 'r'                       ) { # What is this?
        foreach my $rev_file (@{$c->{'content'}}) {
          my ($rev,$file) = @{$rev_file};
          my $rn          = $LN2RN->{$file->name()};
          my $brn         = $rev->getMagic() || 'HEAD';
          
          $revisions_checked{$rn} = {} unless exists $revisions_checked{$rn};
          # First case: it is INITIAL import (1.1.1.1 and like this)
          if     ($file->deltas($rev)->treePrev() == $file->tree()) {
            # Fill branch, etc
            $revisions_checked{$rn}->{$brn} = $rev;
            # Fill head
            $revisions_checked{$rn}->{'HEAD'} = $file->tree()->rev();
          } else {
            # Fill branch only
            $revisions_checked{$rn}->{$brn} = $rev;
          }
          $revisions_checked{$rn}         = {} unless exists $revisions_checked{$rn};
          $revisions_checked{$rn}->{$brn} = $rev;
        }
      }
      ++$disorder_commits if $commit;
    } else {
      throw("Can not find situable commit. There are serious problem with revision orders\n");
    }
    ++$progress;
    print "Check commits sequence: $progress/$progress_all commits ready\r" if $CFG_PROGRESS;
  }
  print "\n" if $CFG_PROGRESS;
  print " Ok\n" if $CFG_VERBOSE > 1;
  if($disorder_commits) {
    print "$disorder_commits commits was reordered\n"  if $CFG_VERBOSE;
  } else {
    print "Date-time order of commits is Ok\n"  if $CFG_VERBOSE;
  }
  $COMMITS = $checked_commits;
}

sub pass7_results
{
  return '';
}

sub pass8_DumpSVN
{
  my $progress_all = scalar(@{$COMMITS});
  my $progress     = 0;

  $CACHE = Cvs::Repository::DeltaCache->New($CFG_DIFFMEM, $CFG_DIFFMEM, $CFG_RESMEM, $CFG_RESMEM) if $CFG_DIFFMEM || $CFG_RESMEM;

  print "Preload live directories in repository... " if $CFG_VERBOSE;
  print "\n" if $CFG_VERBOSE > 1;
  foreach my $p (@CFG_LIVE) {
    print "  Preload '$p'... " if $CFG_VERBOSE > 1;
    my @svnname = grep {$_} split(m-/-,$p);
    # Mark as dir
    push @svnname,'/';
    &dir_MarkAsAdded(\@svnname);
    print "Ok\n" if $CFG_VERBOSE > 1;
  }
  print "Ok\n" if $CFG_VERBOSE == 1;

  local *DUMP;
  open(DUMP,'> '.$CFG_SVNDUMP) or throw("Could not open '$CFG_SVNDUMP' for writing");
  binmode(DUMP);

  my %HELPERS = (
    'i' => \&pass8_helper_ImportOrChange,
    'r' => \&pass8_helper_CopyToTrunk,
    'c' => \&pass8_helper_ImportOrChange,
    't' => \&pass8_helper_TagOrBranch,
    'b' => \&pass8_helper_TagOrBranch,
    'v' => \&pass8_helper_VendorTag
  );

  # Make UUID
  my $UUID = Digest::MD5::md5_hex($CFG_CVSROOT);
  $UUID =~ s/^([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{12})$/$1-$2-$3-$4-$5/;
  # Make dump header
  print DUMP "SVN-fs-dump-format-version: 2\n",
             "UUID: $UUID\n",
             "\n";

  # Make all commits. It is not so simple task
  print "Processed $progress/$progress_all commits\r" if $CFG_PROGRESS;
  foreach my $commit (@{$COMMITS}) {
    # Different types of commits required quite different processing
    if($CFG_VERBOSE) {
      my $msg = $commit->{'log'};
      $msg =~ s/\r?\n/ /g;
      $msg =~ s/\s+/ /g;
      $msg = substr($msg,0,47).'...' if length($msg) > 50;
      print "Make revision from '$msg'...";
      print "\n" if $CFG_VERBOSE > 1;
    }

    # Prepare properties
    my $props = '';
    # Add author
    $props = pass8_helper_MakeProps(
      'author',$commit->{'author'},
      'log',&helper_encodeLogMessage($commit->{'log'}),
      'date',&pass8_helper_MakeDate($commit->{'date'}));

    # Prepare revision header
    print DUMP "Revision-number: $SVNREVISION\n",
                "Prop-content-length: ",length($props),"\n",
                "Content-length: ",length($props),"\n",
                "\n",
                $props,
                "\n";

    # Make revision content
    $HELPERS{$commit->{'type'}}->($commit,*DUMP{IO});

    # Make all last lifespans longer
    foreach my $l (@SVNFILES) {
      ++$l->[$#{$l}]->[2];
    }

    print "Ok, revision $SVNREVISION is ready\n" if $CFG_VERBOSE;

    # Increment lifespan of all latest revisions
    ++$SVNREVISION;
    ##
    # Progress meter
    ++$progress;
    print "Processed $progress/$progress_all commits\r" if $CFG_PROGRESS;
  }

  close(DUMP);
}

sub pass8_results
{
  return '' unless defined $CACHE;
  my ($dh,$dr,$da,$dd,$dc,$rh,$rr,$ra,$rd,$rc) = $CACHE->getStat();
  my $p;
  my $res = '';
  if($CACHE->diffsEnabled()) {
    $p = $dr?int($dh*100/$dr):0;
    $res .= sprintf('Diff cache hits/reqs/%%/adds/dels: %d/%d %d%% %d %d',$dh,$dr,$p,$da,$dd);
  }
  if($CACHE->resultsEnabled()) {
    $res .= ' ' if $CACHE->diffsEnabled();
    $p = $rr?int($rh*100/$rr):0;
    $res .= sprintf('Res cache hits/reqs/%%/adds/dels: %d/%d %d%% %d %d',$rh,$rr,$p,$ra,$rd);
  }
  return $res;
}

sub Help
{
  my $error = shift;
  print STDERR 'Error: ',$error,"\n" if defined $error && $error;
  print<<END_OF_HELP;
refinecvs.pl version $VERSION
Copyright (c) 2003 Lev Serebryakov <lev\@serebryakov.spb.ru>
Distributed under terms of BSD license. See LICENSE file.

Syntax: refinecvs.pl [-v|-p] [-sd] [-ld] [-m] [-dcm SIZE] [-rcm SIZE]
                     [-t timeout] [-ad PRECENT] [-mp] [-l PATH1,PATH2...]
                     [-tn NAME] [-bn NAME] [-gn NAME] [-vtn NAME] [-vgn NAME]
                     [-sh] [-aw] [-iv] [-is SYM1,SYM2,...]
                     [-sp CHILD1=PARENT1,CHILD2.CHILD3=PARENT2,...]
                     [-e ID1,ID2,...] [-w ID1,ID2,...] [-i ID1,ID2,...]
                     tagauthor CVSROOT svn-dump

     -v           -- Verbose output. Go to stdout (errors to stderr).
                     This option could be repeated to get more verbose
                     output.
                     This option could not be used with `-p'.

     -p           -- Progress output. Go to stdout (errors to stderr).
                     When progress output is turned on, every pass
                     will print progress meter, but will not be print
                     any internal information, as in verbose mode.
                     This option could not be used with `-v'.

     -sd          -- Save dump of internal state after each pass.
                     File will be named after path to repository.
                     Only one dump file will be created, it will be updated
                     after each pass.

     -ld          -- Load dump of internal state before work. Dump must be
                     prepared with `-sd' option. If load was successful,
                     script will start from next pass, not from begin.

     -m           -- Pre-allocate memory for big objects. It slightly slow
                     down first pass but could improve performance for all
                     other passes.

     -dcm SIZE    -- Set size of diffs memory cache. Default is 0, so, cache
                     for diffs is disabled. Cache is working with MRU strategy.
                     If this value is too small, performance will be lost.
                     This cache used when SVN dump is produced to store diffs
                     from RCS files.
                     Size can be given as number with optional suffix `b', `k',
                     or `m' (bytes, kilobytes, megabytes). Default is bytes.

     -rcm SIZE    -- Set size of results memory cache. Default is 0, so, cache
                     for results is disabled. Cache is working with MRU
                     strategy. If this value is too small, performance will be
                     lost.
                     This cache used when SVN dump is produced to store checked
                     out revisions from RCS files.
                     Size can be given as number with optional suffix `b', `k',
                     or `m' (bytes, kilobytes, megabytes). Default is bytes.

     -t tiemout   -- Time between checkins, when two checkins with same log
                     message will not treated as one "atomic" checkin.
                     Default is 600 seconds (10 minutes). Could be given as
                     number with optional `s' (default), `m' or `h' suffix
                     (seconds, minutes or hours).

     -fcp         -- Codepage of filenames in repository.
                     Default is UTF-8 compatible (ASCII ot UTF-8 itself).

     -lcp         -- Codepage of commit messages in repository.
                     Default is UTF-8 compatible (ASCII ot UTF-8 itself).

     -ad PERCENT  -- Allow deletions when create branches & tags. By default,
                     whole directories are copied in one operation, only if
                     all content of directory should be branched/tagged. This
                     option allows to use copy-and-delete algorithm for
                     branching/tagging. If percent of files, which disallow
                     to copy directory as whole, less, than given percent, then
                     directory will be copied and these files will be deleted
                     from target path.
                     Default value is 0, so no deletions will be performed.

     -mp          -- Multi-project source (CVS) repository. By default.
                     cvs2svn thinks, that you give one project for conversion,
                     named as last part of path to CVS repository.
                     If you want to convert repository with multiple projects,
                     you should give this option. CVSROOT "project" will be
                     ignored in any case.

     -l PATH1,PATH2,...
                  -- When SVN dump is generating, given paths will not be
                     created, but used as they presents in repository
                     already.
                     This option allows you to create dumps, which will
                     contains files in already live directories.
                     For example, if you use `trunk/projX' repository
                     layout, this option allows you to add projects
                     to live repository.

     -tn NAME     -- Name for `trunk' directory in SVN repository.
                     This is template. `%p' will be replaced with
                     project's name (see `-mp' option).
                     Default is `%p/trunk'.

     -bn NAME     -- Name for `branches' directory in SVN repository.
                     This is template. `%p' will be replaced with
                     project's name (see `-mp' option), and `%n' will
                     be replaced with branche's name.
                     Default is `%p/branches/%n'.

     -gn NAME     -- Name for `tags' directory in SVN repository.
                     This is template. `%p' will be replaced with
                     project's name (see `-mp' option), and `%n' will
                     be replaced with tag's name.
                     Default is `%p/tags/%n'.

     -vtn NAME    -- Name for `vendor trunk' directory in SVN repository,
                     whcih will hold latest vendor's sources.
                     This is template. `%p' will be replaced with
                     project's name (see `-mp' option), and `%v' will
                     be replaced with vendor's name.
                     Default is `%p/vendor/%n/trunk'.

     -vgn NAME    -- Name for `vendor tags' directory in SVN repository,
                     which will hold tagged vendor's imports.
                     This is template. `%p' will be replaced with
                     project's name (see `-mp' option), `%v' will
                     be replaced with vendor's name and `%n' will be
                     replaced with tag's (release's) name.
                     Default is `%p/vendor/%n/tags'.

     -sh          -- Use special heuristic to resolve situation when symbol
                     (branch or tag) have many parents. If this heuristic is
                     turned on, no DiffSymParent event (see below) raised,
                     but used parent, which is longest prefix of this symbol
                     name. So, if RELEASE_4_0_0 have parents RELEASE_4,
                     RELEASE_4_0 and HEAD, RELEASE_4_0 will be selected
                     without any error or warning. But if here is no parent,
                     which is prefix (so, maximum prefix length is 0),
                     DiffSymParent event will be raised in any case.

     -aw          -- Allow weak symbols normalizing. If this key is passed,
                     more attempts to resolve symbols tree will be performed,
                     after strict passes weak one will be performed.

     -iv          -- Ignore all vendors machinery: vendor branches, imports,
                     etc.

     -is SYM1,... -- Ignore these symbols. If symbol is branch name,
                     whole branch will be ignored. If symbol is tag name,
                     tag will not be created. For example, you could want
                     to ignore vendor branch in most simple cases.
                     You could specify '*' as symbol name, and all symbols
                     (and all branches, etc) will be ignored.

     -sp CHILD1=PARENT1,CHILD2.CHILD3=PARENT2,...
                  -- Set parents for some symbols by hands. If given parent
                     is not full-covering parent, error is generated. Such
                     error could not be ignored.
                     You could set one parent for multiple children in once.
                     Name of trunk is HEAD.

     -e, -w, -i   -- Make some events to be errors (default), warnings
                     or nothing (ignore them). Every event could be used
                     only one time. After any of this options you should
                     list events, separated by commas. Here is list of
                     events with meanings:

                     DoubleFile -- Event occurs when one file is present in
                        directory and its `Attic'. It is very strange
                        situation, which CVS could not process well by itself.
                        If you ignore (or warning) this event, file from
                        `Attic' will be ignored.

                     FileError -- Composite event. If parsing or processing
                         of file failed in some way (some of file's events
                         rise error), whole process could be stopped (error),
                         or file could be skipped with message (warning) or
                         without one (ignore).

                     InvalidSymRev -- One of the symbols in file have invalid
                         (unparsable) revision. When it is not error, symbol
                         will be ignored for this file.

                     InvalidSymName -- One of the symbols in file have invalid
                         name. When this error is ignored, all invalid
                         characters will be replaced with %XX as it is done in
                         URLs.

                     DoubleRev -- One of the symbols in file reference two or
                         more different revisions. When it is not error, symbol
                         will be ignored for this file.

                     UnnamedBranch -- When trees of branches is builded for
                         each file, program checks, that each branch have
                         symbol (name). It is strange to have branches
                         without symbols. But if you want to simple ignore
                         such revisions, use this event name.

                     LostBranch -- Each branch in file should have branch
                         point, and this revision should exists in file.
                         It seems, that file have serious damage, if here
                         is branch, which branch point could not be found.
                         You should think twice before make this event to be
                         warning or ignore it.

                     LostSymbol -- Each symbol (tag or branch tag) inside
                         file should reference existing revision. Normally,
                         symbol with invalid revision is error.

                     DoubleSymbol -- Normally, each branch in file have one
                         and only one name (symbol). If here are two
                         or more symbols for one branch, it is strange.
                         But in most cases you could ignore this error
                         without any problems.

                     LostRevision -- All revisions in file are linked into
                         tree. If some revision doesn't have place in such
                         tree, it seems, that file is damaged.

                     TimeMismatch -- Revision has time less than previous
                         one. Time will be fixed, if this event will be set
                         to 'warning', and, please, doesn't ignore this
                         event.

                     DiffSymType -- When all symbols are collected from
                         files to global table, one symbol should have
                         one meaning (tag or branch) in each file. If
                         one symbol has different meanings in different
                         files, it is error. Such error could be ignored
                         with or without message.
                         If this error is ignored, script will create
                         fictive symbols (with type added to name),
                         and strip type when create directories in
                         SVN repository.

                     DiffSymParent -- Each symbol must have a parent.
                         If parents are different in different files,
                         this error is raised. If you ignore this warning,
                         first parent will be selected.

                     NoSymParent -- Each symbol must have a parent.
                         If parents are different in different files,
                         and script could not build set of parents,
                         which will cover all files, this error is raised.
                         If you ignore this error, symbol will be ignored.

     tagauthor    -- Author of all tags and branches, which consists of
                     revisions from different authors.

     CVSROOT      -- Path to CVS repository to convert. It could be path
                     to whole CVS repository (CVSROOT "project" will be
                     skipped automatically) or one directory (project) in
                     CVS repository.

     svn-dump     -- Name of output file, for "svnadmin load" command.
END_OF_HELP
  exit(-1) if defined $error && $error;
  exit(0);
}

sub pass1_helper_LoadDir {
  my ($root,$showroot,$countonly,$total,$fc) = @_;
  local *DIR;

  $countonly = 0 unless defined $countonly;
  $fc = 0 unless defined $fc;

  print "Entering into '$showroot'\n" if $CFG_VERBOSE &&  !$countonly && $showroot && substr($root,-6) ne 'Attic/';
  opendir(DIR,$root) or throw("Could not open directory '$root'");
  while(my $f = readdir(DIR)) {
    if      (-f $root.$f && $f =~ /,v$/) {
      ++$fc;
      next if $countonly;

      if(exists $REPO->{$showroot.$f}) {
        condthrow("File '$f' presents in '$showroot' and '$showroot"."Attic'",\%EVENTS,'DoubleFile',$showroot,$f);
        # Skip if NOW we load file from attic
        next unless substr($root,-6) ne 'Attic/';
      }

      print "Processed ",$fc-1,"/$total files\r" if $CFG_PROGRESS;
      print "Parse '$showroot$f'... " if $CFG_VERBOSE;

      $LN2RN->{$root.$f} = $showroot.$f;
      my $rcs;
      eval { $rcs = Cvs::Repository::File->New($root.$f,\%EVENTS); };
      # Skip this file in case of any error
      if($@) {
        condrethrow("Could not parse file $showroot$f",\%EVENTS,'FileError',$showroot.$f);
      } else {
        print "Ok\n" if $CFG_VERBOSE;
        $REPO->{$showroot.$f} = $rcs;
      }
    } elsif (-d $root.$f && $f !~ /^\.\.?$/ && $f ne 'CVSROOT' && $f ne 'CVS') {
      my $nsr = $showroot;
      $nsr .= $f.'/' if $f ne 'Attic';
      $fc = pass1_helper_LoadDir($root.$f.'/',$nsr,$countonly,$total,$fc);
    }
  }
  closedir(DIR);
  return $fc;
}

sub pass4_helper_AddDate
{
  my ($sd) = @_;
  $sd->{'date'}         = 0;
  $sd->{'firstchanged'} = 0x7FFFFFFF;
  foreach my $p (keys %{$sd->{'files'}}) {
    foreach my $f (@{$sd->{'files'}->{$p}}) {
      $sd->{'date'}         = $f->[1] unless $f->[1] < $sd->{'date'};
      $sd->{'firstchanged'} = $f->[2] unless $f->[2] > $sd->{'firstchanged'};
    }
  }
}

sub pass4_helper_GetAllFiles
{
  my ($sd) = @_;
  my %filewas = ();
  $sd->{'allfiles'} = 0;
  foreach my $a (values %{$sd->{'files'}}) {
    foreach my $f (@{$a}) {
      if(!exists $filewas{$f->[SE_FILE]->name()}) {
        $filewas{$f->[SE_FILE]->name()} = 1;
        ++$sd->{'allfiles'};
      }
    }
  }
}

sub pass4_helper_CheckIgnoredParents
{
  my ($sd) = @_;
  my $dele = 0;
  my $oaf = $sd->{'allfiles'};
  foreach my $p (keys %{$sd->{'files'}}) {
    if(exists $CFG_IGNORESYMBOLS{$p}) {
      delete $sd->{'files'}->{$p};
      $dele = 1;
    }
  }
  &pass4_helper_GetAllFiles($sd) if $dele;
  return 1 if $sd->{'allfiles'} ne $oaf;
  &pass4_helper_AddDate($sd) if $dele;
  return 0;
}

sub pass4_helper_UserSelectedParent
{
  my ($sym,$sd,$resolved,$weak) = @_;
  $weak = 0 unless defined $weak;

  return undef unless exists $CFG_SYMBOLSPARENTS{$sym};

  my $p = $CFG_SYMBOLSPARENTS{$sym};
  # Check: may be parent was ignored?
  return undef if exists $CFG_IGNORESYMBOLS{$p};
  # May be, not ready?
  return [] unless exists $resolved->{$p} || $weak;
  # Check: is this symbol COULD be parent?
  throw("User selected parent '$p' for '$sym' could not be parent at all.") unless exists $sd->{'files'}->{$p};
  # Check: is it full-covering parent?
  throw("User selected parent '$p' for '$sym' is not full-covering one.") unless $sd->{'allfiles'} == @{$sd->{'files'}->{$p}};
  return [ $CFG_SYMBOLSPARENTS{$sym} ];
}

sub pass4_helper_OneParent
{
  my ($sym,$sd,$resolved,$weak) = @_;
  $weak = 0 unless defined $weak;
  return undef if keys %{$sd->{'files'}} != 1;
  my $p = (keys %{$sd->{'files'}})[0];
  return [] unless exists $resolved->{$p};
  return [ $p ];
}

sub pass4_helper_FullCoveringParent
{
  my ($sym,$sd,$resolved,$weak) = @_;
  $weak = 0 unless defined $weak;

  my @fullp = ();

  # Try to find parents, which cover all files, if it is possible
  while(my ($p,$a) =  each %{$sd->{'files'}}) {
    push @fullp,$p if $sd->{'allfiles'} == @{$a};
  }

  return undef unless @fullp;

  # Filter out all not-resolved parents.
  # If we found one, return [] -- next time
  if($weak) {
    @fullp = grep { exists $resolved->{$_} } @fullp;
    return [] if !@fullp;
  } else {
    return [] if grep { !exists $resolved->{$_} } @fullp;
  }

  # If this is simple branch or tag, try to filter out vendor branches as parents
  if($sd->{'type'} eq 't' || $sd->{'type'} eq 'b') {
    my @nonvendor = grep { $resolved->{$_}->{'type'} ne 'i' && $resolved->{$_}->{'type'} ne 'v' } @fullp;
    @fullp = @nonvendor if @nonvendor;
  }

  # Sort them by priority of their types
  # Branch have most priority and head -- less
  my %prio = ('b' => 0, 'i' => 1, 'v' => 2, 't' => 3);
  @fullp = sort {$prio{$resolved->{$a}->{'type'}} <=> $prio{$resolved->{$b}->{'type'}}} @fullp;

  # If symbol heuristic is ON, filter out all parents, for which symbol is prefix
  @fullp = grep {$sym ne substr($_,0,length($sym))} @fullp if $CFG_SYMHEUR;

  # Try to filter-out all revision, which is not longest prefix
  if((1 < grep {$_ ne 'HEAD'} @fullp) && $CFG_SYMHEUR) { # HEAD could be skipped :)
    my @prefixes = sort {length($b)<=>length($a)} grep {$_ eq substr($sym,0,length($_))} @fullp;
    @fullp = ( $prefixes[0] ) if @prefixes;
  }

  if(1 < grep {$_ ne 'HEAD'} @fullp) { # HEAD could be skipped :)
    condthrow("Symbol '$sym' have many full parents: '".join("', '",@fullp)."'.\nMay be, you should use '-sp' option.",\%EVENTS,'DiffSymParent',$sym,@fullp);
  }
  return [ $fullp[0] ];
}

sub pass4_helper_CoveringSetOfParents
{
  my ($sym,$sd,$resolved,$weak) = @_;
  $weak = 0 unless defined $weak;

  my @covering = ();

  # Try to find minimal set of parents, which covers all files
  # without intersections... If here is no such case, fail to process
  # this symbol

  # Check, may be this symbol have Intersect Matrix
  my %ima = ();
  if(exists $sd->{'_ima'}) {
    %ima = %{$sd->{'_ima'}};
  } else {
    # Build intersect matrix
    foreach my $p1 (keys %{$sd->{'files'}}) {
      my %p1c = map { $LN2RN->{$_->[SE_FILE]->name()} => 1 } @{$sd->{'files'}->{$p1}};
      $ima{$p1} = {};
      foreach my $p2 (keys %{$sd->{'files'}}) {
        if      (exists $ima{$p2} && exists $ima{$p2}->{$p1}) {
          $ima{$p1}->{$p2} = $ima{$p2}->{$p1};
        } elsif ($p1 eq $p2) {
          $ima{$p1}->{$p2} = 1;
        } else {
          $ima{$p1}->{$p2} = 0;
          foreach my $f (map { $LN2RN->{$_->[SE_FILE]->name()} => 1 } @{$sd->{'files'}->{$p2}}) {
            if(exists $p1c{$f}) {
              $ima{$p1}->{$p2} = 1;
              last;
            }
          }
        }
      }
    }
    $sd->{'_ima'} = { %ima };
  }

  # Matrix is build. Now try to create such set
  my %wasp = ();
  foreach my $p1 (keys %{$sd->{'files'}}) {
    $wasp{$p1} = 1;
    @covering = ($p1);
    # Filter newly-ignored parents here
    foreach my $p2 (grep {!exists $CFG_IGNORESYMBOLS{$_}} keys %{$ima{$p1}}) {
      if(!$ima{$p1}->{$p2}) {
        # If no intersection and $p2 was seen -- skip this combination as probed
        if(exists $wasp{$p2}) {
          # Mark "failed"
          @covering = ();
          # Break inner loop
          last;
        }
        push @covering,$p2;
      }
    }
    # Check for coverage with found set
    my $filesleft = $sd->{'allfiles'};
    foreach my $p (@covering) {
      $filesleft -= @{$sd->{'files'}->{$p}};
    }
    last unless $filesleft;
    @covering= ();
  }
  # If array is not empty, it contains full coverage
  return undef unless @covering;
  return [] if grep { !exists $resolved->{$_} } @covering;
  return [ @covering ];
}

sub pass7_helper_CompareCommits($$)
{
 my ($c1,$c2) = @_;
 my $r = 0;
 my %oporder = ('i' => 0, 'v' => 1, 'r' => 2, 'c' => 3, 'b' => 4, 't' => 5 );
 my $inv = 1;

 $r = $c1->{'date'} <=> $c2->{'date'};
 return $r unless !$r;

 # If first commit is on tree, created with second, second go LATER
 # And vice versa
 if      ($c2->{'type'} eq 'b') {
   return  1 if &pass7_helper_OnTree($c2->{'tag'},$c1);
 } elsif ($c1->{'type'} eq 'b') {
   return -1 if &pass7_helper_OnTree($c1->{'tag'},$c2);
 }
 # Not special case, use operations order
 $r = $oporder{$c1->{'type'}} <=> $oporder{$c2->{'type'}};
 return $r unless !$r;
 # And they are equal. Check who is upper on tree
 return &pass7_helper_CompareBranches($c1, $c2);
}

sub pass7_helper_OnTree
{
  my ($root,$c) = @_;

  return 0 unless !exists $c->{'branch'} || 'HEAD' ne $c->{'branch'};

  if(exists $c->{'branch'}) {
    return &pass7_helper_OnTree_worker($root,$c->{'branch'});
  } else {
    return &pass7_helper_OnTree_worker($root,grep {$_ ne 'HEAD'} keys %{$c->{'content'}});
  }
}

sub pass7_helper_OnTree_worker
{
  my ($root,@leaves) = @_;
  return 1 if $root eq 'HEAD';
  # Ok, check, is leaves is rooted at $root
  while(my $l = shift @leaves) {
    return 1 if $l eq $root;
    # Push all parents :)
    push @leaves, grep {$_ ne 'HEAD'} @{$SYMBOLS->{$l}->{'prn'}};
  }
  return 0;
}

sub pass7_helper_CompareBranches
{
  my ($c1,$c2) = @_;
  my (@b1, @b2);

  if(exists $c1->{'branch'}) {
    @b1 = ($c1->{'branch'});
  } else {
    @b1 = keys %{$c1->{'content'}};
  }
  if(exists $c2->{'branch'}) {
    @b2 = ($c2->{'branch'});
  } else {
    @b2 = keys %{$c2->{'content'}};
  }

  # Ok, check is $c1 is upper on tree, that $c2
  foreach my $b1 (@b1) {
    return -1 unless !pass7_helper_OnTree_worker($b1,@b2);
  }
  foreach my $b2 (@b2) {
    return  1 unless !pass7_helper_OnTree_worker($b2,@b1);
  }
  return 0;
}

sub pass8_helper_ImportOrChange
{
  my ($commit, $DUMP) = @_;
  my $svnname;
  my $props;
  my $content;
  my ($type,$name,$vend);
  my $logop;

  # This is import of files or change in some branch.
  # Really, these files should be added to vendor branch.
  # Easy work, yes. Really easy.

  # check, we should have 'branch' value
  if      ($commit->{'type'} eq 'c') {
    $logop = 'Add or change';
    $vend = '';
    if(exists $commit->{'branch'} && $commit->{'branch'} && $commit->{'branch'} ne 'HEAD') {
      $type = ST_BRANCHMAGIC;
      $name = $commit->{'branch'};
    } else {
      $type = ST_TRUNK;
      $name = 'HEAD'; # For future use
    }
  } elsif ($commit->{'type'} eq 'i') {
    $logop = 'Import';
    throw("Invalid 'import' commit: no 'branch'") unless exists $commit->{'branch'} && $commit->{'branch'};
    $type = ST_VENDORMAGIC;
    $name = '';
    $vend = $commit->{'branch'};
  } else {
    throw("Invalid commit type for 'ImportOrChange()' function");
  }

  foreach my $file (@{$commit->{'content'}}) {
    my $r = $file->[0];
    my $f = $file->[1];
    my $n = $LN2RN->{$f->name()};
    $svnname = &dir_cvs2svn($n,$type,$name,$vend);

    print "  $logop '/",join('/',@{$svnname}),"'... " if $CFG_VERBOSE > 1;

    my $svnfile;
    my $dead = 0;
    if($f->deltas($r)->state() eq 'dead') {
      print "Delete... " if $CFG_VERBOSE > 1;
      $svnfile = &dir_Delete($svnname,$DUMP);
      $dead     = 1;
      $content  = '';
    } else {
      $svnfile = &dir_AddOrChange($svnname,$DUMP);
      $content  = $f->checkOut($CACHE,$r);
    }

    # Special processing: `.cvsignore'
    if(!defined($svnfile)) {
      print ".cvsignore -> svn:ignore... " if $CFG_VERBOSE > 1;
      my $change = 0;
      if($dead) {
        # Check -- is path live? :)
        my $d = undef;
        # Get directory (we need to copy array or we destroy cache)
        $svnname = [ @{$svnname} ];
        $svnname->[$#{$svnname}] = '/';
        $d = &dir_IsLive($svnname);
        # Is directory alive?
        if(defined $d) {
          $change = 1;
          pop @{$svnname};
          # Print header for directory properties changed
          print $DUMP "Node-path: /",&helper_encodeNodePath(join('/',@{$svnname})),"\n",
                      "Node-kind: dir\n",
                      "Node-action: change\n";
        }
      } else {
        $change = 1;
        # Header is printed already
      }
      # If path is live
      if($change) {
        # Don't add this file as file, and dir_AddOrChange knows about this.
        # Set properties
        $props = &pass8_helper_MakeProps('ignore',$content);
        # Ok, print them
        print $DUMP "Prop-content-length: ",length($props),"\n",
                    "Content-length: ",length($props),"\n",
                    "\n",
                    $props,
                    "\n";
      }
      $dead = 1;
    }

    if (!$dead) {
      # Make properties
      $props   = &pass8_helper_MakeFileProps($f);
      # Checkout content
      throw("Could not check out revision '$r' for file '$n'") unless defined $content;
      # Print headers
      my ($pcl,$tcl) = (length($props),length($content));
      print $DUMP "Prop-content-length: ",$pcl,"\n",
                  "Text-content-length: ",$tcl,"\n",
                  "Text-content-md5: ",Digest::MD5::md5_hex($content),"\n",
                  "Content-length: ",$tcl+$pcl,"\n",
                  "\n",
                  $props,
                  $content,
                  "\n";
      # And add mapping for this file
      push @{$svnfile->{'h'}},[$r, $SVNREVISION, $SVNREVISION-1]; # -1 because after this change it will be incremented
    } elsif(defined $svnfile) { # Mark dead & defined files: dead & undefined is '.cvsignore'
      # Marked in &dir_Delete();
    }
    print "Ok, CVS revision $r is committed.\n" if $CFG_VERBOSE > 1;
    undef $content;
  }
  # Ok, everything is done
}

sub pass8_helper_CopyToTrunk
{
  my ($commit, $DUMP) = @_;

  throw("Invalid commit type for 'CopyToTrunk()' function") unless $commit->{'type'} eq 'r';
  throw("Invalid commit: no 'branch' field in 'ctr' commit") unless exists $commit->{'branch'} && $commit->{'branch'};

  print "  Copy imported sources to trunk.\n" if $CFG_VERBOSE > 1;

  # Make source structure
  my @src;
  my $svnname;
  foreach my $f (@{$commit->{'content'}}) {
    $svnname = &dir_cvs2svn($LN2RN->{$f->[1]->name()},ST_VENDORMAGIC,'',$commit->{'branch'});
    push @src,[$f->[0],$f->[1],$svnname,&dir_GetLiveOne($svnname)];
  }
  &pass8_helper_CopyTree($DUMP,\@src,ST_TRUNK,'','');
}

sub pass8_helper_VendorTag
{
  my ($commit, $DUMP) = @_;

  throw("Invalid commit type for 'VendorTag()' function") unless $commit->{'type'} eq 'v';
  throw("Invalid commit: no 'tag' field in 'vendor tag' commit") unless exists $commit->{'tag'} && $commit->{'tag'};

  my @src;
  my $svnname;
  if(exists $commit->{'branch'}) {
    print "  Create vendor tag '",$commit->{'tag'},"' for vendor '",$commit->{'branch'},"'.\n" if $CFG_VERBOSE > 1;
    foreach my $f (@{$commit->{'content'}}) {
      $svnname = &dir_cvs2svn($LN2RN->{$f->[1]->name()},ST_VENDORMAGIC,'',$commit->{'branch'});
      push @src,[$f->[0],$f->[1],$svnname,&dir_GetLiveOne($svnname)];
    }
    &pass8_helper_CopyTree($DUMP,\@src,ST_VENDOR,$commit->{'tag'},$commit->{'branch'});
  } else {
    # Multi-branch source
    while(my ($p,$c) = each %{$commit->{'content'}}) {
      print "  Create vendor tag '",$commit->{'tag'},"' for '$p'\n" if $CFG_VERBOSE > 1;
      @src = ();
      foreach my $f (@{$c}) {
        $svnname = &dir_cvs2svn($LN2RN->{$f->[1]->name()},ST_VENDORMAGIC,'',$p);
        push @src,[$f->[0],$f->[1],$svnname,&dir_GetLiveOne($svnname)];
      }
      &pass8_helper_CopyTree($DUMP,\@src,ST_VENDOR,$commit->{'tag'},$p);
    }
  }
}

sub pass8_helper_TagOrBranch
{
  my ($commit, $DUMP) = @_;

  throw("Invalid commit type for 'TagOrBranch()' function") unless $commit->{'type'} eq 't' || $commit->{'type'} eq 'b';
  throw("Invalid commit: no 'tag' field in 'tag or branch' commit") unless exists $commit->{'tag'} && $commit->{'tag'};

  print "  Create ",($commit->{'type'} eq 't')?"tag":"branch"," '",$commit->{'tag'},"'\n" if $CFG_VERBOSE > 1;

  my @src;
  my $stype;
  my $svnname;
  if(exists $commit->{'branch'}) {
    $stype = ($commit->{'branch'} eq 'HEAD')?ST_TRUNK:ST_BRANCHMAGIC;
    foreach my $f (@{$commit->{'content'}}) {
      $svnname = &dir_cvs2svn($LN2RN->{$f->[1]->name()},$stype,$commit->{'branch'},'');
      push @src,[$f->[0],$f->[1],$svnname,&dir_GetLiveOne($svnname)];
    }
  } else {
    # Multi-branch source
    while(my ($p,$c) = each %{$commit->{'content'}}) {
      $stype = ($p eq 'HEAD')?ST_TRUNK:ST_BRANCHMAGIC;
      foreach my $f (@{$c}) {
        $svnname = &dir_cvs2svn($LN2RN->{$f->[1]->name()},$stype,$p,'');
        push @src,[$f->[0],$f->[1],$svnname,&dir_GetLiveOne($svnname)];
      }
    }
  }
  # Ok, source is ready

  # Make source structure
  &pass8_helper_CopyTree($DUMP,\@src,($commit->{'type'} eq 'b')?ST_BRANCHMAGIC:ST_BRANCH,$commit->{'tag'},'');
}

#
# Copy tree in optimal way
# This is working horse for Tagging and branching operations
#
use constant CS_REV     => 0;
use constant CS_FILE    => 1;
use constant CS_SVNNAME => 2;
use constant CS_TREE    => 3;
use constant CS_TONAME  => 4;

sub pass8_helper_CopyTree
{
  my ($DUMP,$source,$dtype,$dname,$dvend) = @_;

  my %was = ();
  my @parents  = ();
  my @tocopy   = ();
  my @todel    = ();
  my %needdead = ();
  my %islive   = ();
  my $deepcmn = undef;
  my $deepsrc = undef;
  my $deepdir = undef;

  print "    Determine best strategy for tree copy.\n" if $CFG_VERBOSE > 1;

  # Prepare first version of tree elements to copy.
  # All individual files for first try.
  # Put only SVN paths, SVN tree elements & add gaps
  #
  # Also, collect all parents to additional place
  foreach my $e (@{$source}) {
    # construct destination name
    $e->[CS_TONAME] = &dir_cvs2svn($LN2RN->{$e->[CS_FILE]->name()},$dtype,$dname,$dvend);
    # check for ".cvsignore"
    next if !defined $e->[CS_TREE];
    # Check, is it live BEFORE copying or not
    $islive{$e->[CS_TREE]} = 1 unless !&dir_IsLive($e->[CS_TONAME]);
    
    # Mark needed file with gap
    # If it is copy from revision, which is root of delta tree
    # We allow to find not this tree, but newest vendor revision
    if($e->[CS_REV] == $e->[CS_FILE]->tree()->rev()) {
      $e->[CS_TREE]->{'_'} = [ &dir_FindFileSpanForRoot($e->[CS_TREE],$e->[CS_FILE]) ];
    } else {
      $e->[CS_TREE]->{'_'} = [ &dir_FindFileSpan($e->[CS_TREE],$e->[CS_REV]) ];
    }
    # Push to @tocopy
    push @tocopy, [$e->[CS_SVNNAME],$e->[CS_TREE],$e->[CS_TONAME]];

    my $p = $e->[CS_TREE]->{'p'};
    # Add parent, if was not added yet.
    next if exists $was{"$p"};

    # Make svnpath for parent (to and from)
    my $svnsdir = [ @{$tocopy[$#tocopy]->[0]} ]; $svnsdir->[$#{$svnsdir}] = '/';
    my $svnddir = [ @{$tocopy[$#tocopy]->[2]} ]; $svnddir->[$#{$svnddir}] = '/';
    push @parents,[$svnsdir, $p, $svnddir];
    $was{"$p"} = 1;

    # And try to find deepest common for all destinations & source directory
    my $i;
    $deepcmn = [ @{$svnsdir} ] unless defined $deepcmn;
    for($i = 0; $i <= $#{$deepcmn} && $i <= $#{$svnsdir} && $i <= $#{$svnddir}; ++$i) {
      last unless $deepcmn->[$i] eq $svnsdir->[$i] && $deepcmn->[$i] eq $svnddir->[$i];
    }
    $#{$deepcmn} = $i - 1;
    # And deepest common for sources only
    $deepsrc = [ @{$svnsdir} ] unless defined $deepsrc;
    for($i = 0; $i <= $#{$deepsrc} && $i <= $#{$svnsdir}; ++$i) {
      last unless $deepsrc->[$i] eq $svnsdir->[$i];
    }
    $#{$deepsrc} = $i - 1;
  }
  push @{$deepcmn},'/' unless $#{$deepcmn} > -1 && $deepcmn->[$#{$deepcmn}] eq '/';
  push @{$deepsrc},'/' unless $#{$deepsrc} > -1 && $deepsrc->[$#{$deepsrc}] eq '/';
  # Take longest path
  if($#{$deepcmn} < $#{$deepsrc}) {
    $deepdir = &dir_GetLiveOne($deepsrc);
    throw("Copy from non-existent path '/",join('/',@{$deepsrc})."'") unless defined $deepdir;
  } else {
    $deepdir = &dir_GetLiveOne($deepcmn);
    throw("Copy from non-existent path '/",join('/',@{$deepcmn})."'") unless defined $deepdir;
  }

  print "    To copy: ",scalar(@{$source})," file(s) in ",scalar(@parents)," directorie(s)\n" if $CFG_VERBOSE > 1;
  # Process all parents & mark all not-marked children with 'dead' gaps
  foreach my $p (@parents) {
    while (my ($nm,$c) =  each %{$p->[1]->{'c'}}) {
      # Already marked
      next if defined $c->{'_'};
      # Don't mark directory, which is in @parents
      # It will be marked later with intersect of children
      next if exists $was{"$c"};
      # Mark all other elements
      $c->{'_'} = [ &dir_FindDeadSpan($c) ];
      $needdead{"$c"} = 1;
    }
  }
  # Ok, everything is properly marked

  # Process all parents, look at children & try to collapse them
  while(my $p = shift @parents) {
    print "      Process parent '/",join('/',@{$p->[0]}[0..$#{$p->[0]}-1]),"' (",scalar(keys %{$p->[1]->{'c'}})," children)... " if $CFG_VERBOSE > 1;

    # Check: may be destination path exists?
    my $d;
    eval { $d = &dir_GetLiveOne($p->[2]); };
    if(defined $d) {
      print "Destination path exists, copy all children\n" if $CFG_VERBOSE > 1;
      next;
    }

    my $skip  = 0;
    my $todel = 0; # For deletion process
    # Find intersection of all children's spans
    my @spans = (1, $SVNREVISION - 1); # This is maximal allowed variant
    my $files = scalar(grep {$_->{'t'} eq 'f'} values %{$p->[1]->{'c'}});

    while (my ($nm,$c) =  each %{$p->[1]->{'c'}}) {
      # Check: may be children is not-processed parent?
      # Skip this parent, if it contains not processed ones
      if(exists $was{"$c"} && $was{"$c"} == 1) {
        print "Skip for next pass\n" if $CFG_VERBOSE > 1;
        # Put to back
        push @parents, $p;
        # And mark to skip
        $skip = 1;
      }
      last if $skip;
      # If this file is live any case, skip it: it will not be copied.
      next unless !exists $islive{$c};
      # We could allow bad dead files
      if($CFG_DELPRECENT && exists $needdead{"$c"}) {
        my @newspans = &intervals_intersect(\@spans,$c->{'_'});
        # And check
        if(!@newspans) {
          # Check percentage
          ++$todel;
          if($todel * 100 / $files <= $CFG_DELPRECENT) {
            my $dp = [ @{$p->[2]} ];
            $dp->[$#{$dp}] = $nm;
            push @todel,[undef,$c,$dp];
          } else {
            @spans = ();
          }
        } else {
          @spans = @newspans;
        }
      } else {
        @spans = &intervals_intersect(\@spans,$c->{'_'});
      }
      # don't check more, everything is bad
      last unless @spans;
    }
    # Reset iterator
    scalar(values(%{$p->[1]->{'c'}}));

    # If skip, don't do anything, go to next parent
    next if $skip;
    # Ok, if @spans is proper, set it to this dir, add it's parent & remove it from parents
    # Mark as processed dir in any case
    $was{$p->[1]} = 2;
    if(@spans) {
      if($todel) {
        print "Copy as one element, with $todel deletion(s)\n" if $CFG_VERBOSE > 1;
      } else {
        print "Copy as one element\n" if $CFG_VERBOSE > 1;
      }
      # Add proper span
      $p->[1]->{'_'} = [ @spans ];
      # Filter out all children from tocopy
      @tocopy = grep { $_->[1]->{'p'} ne $p->[1] } @tocopy;
      # Add to copy
      push @tocopy, $p;
      # Add parent, if not here yet & it we is not root of copy
      if($p->[1] ne $deepdir && !exists $was{$p->[1]->{'p'}}) {
        my $svnsname = [ @{$p->[0]} ]; --$#{$svnsname}; $svnsname->[$#{$svnsname}] = '/';
        my $svndname = [ @{$p->[2]} ]; --$#{$svndname}; $svndname->[$#{$svndname}] = '/';
        push @parents, [$svnsname, $p->[1]->{'p'}, $svndname];
        # Mark as added, but not processed
        $was{$p->[1]->{'p'}} = 1;
        # And mark all dead childrens of parent, surely!
        while (my ($nm,$c) =  each %{$p->[1]->{'p'}->{'c'}}) {
          # Already marked
          next if defined $c->{'_'};
          # Don't mark directory, which is in @parents
          # It will be marked later with intersect of children
          next if exists $was{"$c"};
          # Mark all other elements
          $c->{'_'} = [ &dir_FindDeadSpan($c) ];
          $needdead{"$c"} = 1;
        }
      }
    } else {
      #  Mark as not copiable
      $p->[1]->{'_'} = [];
      print "Copy all children\n" if $CFG_VERBOSE > 1;
    }
  }
  print "    To copy: ",scalar(@tocopy)," element(s)\n" if $CFG_VERBOSE > 1;

  # Copy each in proper order -- from root to leafs to avoid unnecessary directories
  # creation
  foreach my $f (sort { &tree_cmp($a->[2],$b->[2]) } @tocopy) {
    # If file is live, don't make copy operation
    next unless !exists $islive{$f->[2]};
    my $t = &dir_AddOrChange($f->[2],$DUMP,0); # No properties for last directories!
    # Skip .cvsignore
    next unless defined $t;
    # And add copy statements
    my $p = $f->[0];
    --$#{$p} if $p->[$#{$p}] eq '/';
    print $DUMP "Node-copyfrom-path: /",join('/',@{$p}),"\n",
                "Node-copyfrom-rev: ",$f->[1]->{'_'}->[$#{$f->[1]->{'_'}}],"\n",
                "\n";

    print "    Copy '/",join('/',@{$p}),"'\n" if $CFG_VERBOSE > 1;
  }
  # Add them all without printing to dump!
  # But make .cvsignore changes too.
  foreach my $e (sort { &tree_cmp($a->[CS_TONAME],$b->[CS_TONAME]) } @{$source}) {
    # If it is file and not '.cvsignore', add
    if($e->[CS_TONAME]->[$#{$e->[CS_TONAME]}] ne '.cvsignore') {
      # And very bad situation: we need to CHANGE file manually, because change+copy doesn't work :(
      if($islive{$e->[CS_TREE]}) {
        my $d = $e->[CS_FILE]->deltas($e->[CS_REV]);
        # Strange?
        if($d->state() eq 'dead') {
          print "    Really delete '/",join('/',@{$e->[CS_TONAME]}),"'... " if $CFG_VERBOSE > 1;
          &dir_Delete($e->[CS_TONAME],$DUMP);
        } else {
          print "    Really change '/",join('/',@{$e->[CS_TONAME]}),"'... " if $CFG_VERBOSE > 1;

          my $f = &dir_AddOrChange($e->[CS_TONAME],$DUMP);
          my $content = $e->[CS_FILE]->checkOut($CACHE,$e->[CS_REV]);
          my $props = &pass8_helper_MakeFileProps($e->[CS_FILE]);
          throw "Could not check out revision '".$e->[CS_REV]."' from file '".$LN2RN->{$e->[CS_FILE]->name()}."'" unless defined $content;
          my ($pcl,$tcl) = (length($props),length($content));
          print $DUMP "Prop-content-length: ",$pcl,"\n",
                      "Text-content-length: ",$tcl,"\n",
                      "Text-content-md5: ",Digest::MD5::md5_hex($content),"\n",
                      "Content-length: ",$tcl+$pcl,"\n",
                      "\n",
                      $props,
                      $content,
                      "\n";
          # And put it!
          push @{$f->{'h'}},[$e->[CS_REV],$SVNREVISION,$SVNREVISION-1];
        }
        print "Ok\n" if $CFG_VERBOSE > 1;
      } else {
        my $d = &dir_MarkAsAdded($e->[CS_TONAME]);
        # Add revision for files.
        push @{$d->{'h'}},[$e->[CS_REV],$SVNREVISION,$SVNREVISION-1] if $d->{'t'} eq 'f';
      }
    } else {
      my $props;

      # Mark as added, for directory adding to our tree
      &dir_MarkAsAdded($e->[CS_TONAME]);

      # Add header for properties changes
      &dir_AddOrChange($e->[CS_TONAME],$DUMP);
      # If it is .cvsignore, change it's directory properties

      $props = $e->[CS_FILE]->checkOut($CACHE,$e->[CS_REV]);
      $props = &pass8_helper_MakeProps('ignore',$props);

      print $DUMP "Prop-content-length: ",length($props),"\n",
                  "Content-length: ",length($props),"\n",
                  "\n",
                  $props,
                  "\n";

    }
  }
  #
  # Add & delete all deleted :)
  foreach my $e (sort { &tree_cmp($a->[2],$b->[2]) } @todel) {
    my $d = &dir_MarkAsAdded($e->[2]);
    # And delete it!
    &dir_Delete($e->[2],$DUMP);
  }

  # After all work, clean '_' parameters
  &dir_CleanTraverse($CVS2SVN);
}

sub pass8_helper_MakeProps
{
  my $props = '';
  my $s;
  while(@_) {
    $s = shift @_;
    $s = 'svn:'.$s unless $s =~ /:/;
    $props .= 'K '.length($s)."\n";
    $props .= $s."\n";
    $s = shift @_;
    $props .= 'V '.length($s)."\n";
    $props .= $s."\n";
  }
  $props .= "PROPS-END\n";
  return $props;
}

sub pass8_helper_MakeFileProps
{
  my ($file) = @_;
  my @props = ();
  # Ok, check expansion
  my $expand = $file->expand() || 'kv';
  if      ('kv' eq $expand || 'kvl' eq $expand) {
    push @props, ('keywords','LastChangedDate LastChangedRevision LastChangedBy HeadURL Id');
  } elsif ('k' eq $expand || 'o' eq $expand || 'v' eq $expand) {
    # Skip this: no substitution, no binary
  } elsif ('b' eq $expand) {
    # Binary
    push @props, ('media-type','application/octet-stream');
  } else {
    # Unknown mode -- skip
  }
  # Check executableness
  push @props,('executable','yes') if $file->exec();
  return &pass8_helper_MakeProps(@props);
}

sub pass8_helper_MakeDate
{
  #   0     1    2     3     4    5     6     7
  # ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday)
  my @gmt = gmtime($_[0]);
  return sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000000Z",$gmt[5]+1900,$gmt[4]+1,$gmt[3],$gmt[2],$gmt[1],$gmt[0]);
}

sub helper_ResizeArray
{
  return unless $CFG_PREALLOC;
  $#{$_[0]} = $_[1] || 256;
  @{$_[0]} = ();
}

sub helper_CreateArray
{
  my $r = [];
  return $r unless $CFG_PREALLOC;
  $#{$r} = $_[0] || 256;
  @{$r} = ();
  return $r;
}

##
# Encoding helpers
sub helper_encodeNodePath
{
  return $_[0] unless $CFG_ENC_FN;
  Encode::from_to($_[0],$CFG_ENC_FN,'utf8');
  return $_[0];
}

sub helper_encodeLogMessage
{
  return $_[0] unless $CFG_ENC_LOG;
  Encode::from_to($_[0],$CFG_ENC_LOG,'utf8');
  return $_[0];
}

##
# Really, this is "static" variable for dir_cvs2svn
my %__c2s_cache = ();

sub dir_cvs2svn
{
  my ($cvs, $type, $name, $vend) = @_;

  # Return cached variant
  return $__c2s_cache{$cvs.'\x00'.$type.'\x00'.$name.'\x00'.$vend} if exists $__c2s_cache{$cvs.'\x00'.$type.'\x00'.$name.'\x00'.$vend};

  # Ok, make such path
  my @cvs = split(m-/-,$cvs);
  # Add special `directory' element
  if(substr($cvs,-1,1) eq '/') {
    push @cvs,'/';
  } else {
    # Remove `,v'
    $cvs[$#cvs] = substr($cvs[$#cvs],0,length($cvs[$#cvs]) - 2);
  }
  my $proj;
  if($CFG_ONEPROJECT) {
    $proj = $CFG_ONEPROJECT;
  } else {
    # Skip first element from CVS name
    $proj = shift @cvs;
  }
  my $svn = $CFG_NAMES[$type];
  # Make replacements
  $svn =~ s/%p/$proj/g;
  $svn =~ s/%n/$name/g unless ST_TRUNK == $type || ST_VENDORMAGIC == $type;
  $svn =~ s/%v/$vend/g unless ST_TRUNK == $type || ST_BRANCH == $type || ST_BRANCHMAGIC == $type;
  return $__c2s_cache{$cvs.'\x00'.$type.'\x00'.$name.'\x00'.$vend} = [grep {$_} split(m-/-,$svn), @cvs];
}

# Return current state (revision, 'D' or 'L') for element
sub dir_GetStatus
{
  return $_[0]->{'h'}->[$#{$_[0]->{'h'}}]->[0];
}

# Return state (revision, 'D' or 'L') for element at given SVN revision
sub dir_GetStatusAt
{
  my ($e,$r) = @_;
  # Get history
  $e = $e->{'h'};
  # 'D' if we BEFORE start of history
  return 'D' if $r < $e->[0]->[1];
  # Find in history
  foreach my $i (@{$e}) {
    return $i->[0] if $r >= $i->[1] && $i <= $i->[2];
  }
  # Return last element
  return $e->[$#{$e}]->[0];
}

# Find SVN revisions span for file by CVS revision
sub dir_FindFileSpan
{
  my ($f,$r) = @_;
  $f = $f->{'h'};
  foreach my $i (@{$f}) {
    next if !ref($i->[0]);
    return ($i->[1],$i->[2]) if $i->[0] == $r;
  }
  return ();
}

sub dir_FindFileSpanForRoot
{
  my ($f,$rf) = @_;
  my @span;

  @span = &dir_FindFileSpan($f,$rf->tree()->rev());
  return @span unless !@span;
  # Try to find for all revisions
  foreach my $d (sort { $b->rev() <=> $a->rev() } grep { $_->rev()->isVendor() } $rf->tree()->treeBranches()) {
    # Find tip of vendor branch
    while($d->treeNext()) {
      $d = $d->treeNext();
    }
    # And try to find revisions from vendor branch
    while($d != $rf->tree()) {
      @span = &dir_FindFileSpan($f,$d->rev());
      return @span unless !@span;
      $d = $d->treePrev();
    }
  }
  return ();
}

# Find SVN revisions span for directory or file when it was DEAD
sub dir_FindDeadSpan
{
  my ($e,$r) = @_;
  my @d = ();
  $e = $e->{'h'};
  foreach my $i (@{$e}) {
    next if ref($i->[0]);
    next if 'L' eq $i->[0];
    push @d, ($i->[1],$i->[2]);
  }
  return @d;
}

# Is directory dead because all children are dead? Not recursive!
sub dir_BecomeDead
{
  my ($d) = @_;
  my $havelife = 0;
  my $s;
  foreach my $c (values %{$d->{'c'}}) {
    $s = &dir_GetStatus($c);
    $havelife |= (ref($s) ne '' || 'L' eq $s);
    last if $havelife;
  }
  return !$havelife;
}

sub dir_AddOrChange
{
  my ($svn, $DUMP, $props_for_dir) = @_;
  # Check all path: does it exists?
  my $d = $CVS2SVN;
  my $elem = '';
  my $p    = '';
  my $add  = 0;

  $props_for_dir = 1 unless defined $props_for_dir;

  for(my $i = 0; $i < $#{$svn}; ++$i) {
    $elem = $svn->[$i];
    $p .= '/'.$elem;

    # Is directory exists?
    if(!exists $d->{'c'}->{$elem}) {
      $add = 1;
      $d->{'c'}->{$elem} = {
        't' => 'd',
        'h' => [['L', $SVNREVISION, $SVNREVISION - 1]],
        'c' => {},
        'p' => $d,
        # For tree traversal
        '_' => undef
      };
      $d = $d->{'c'}->{$elem};
      # For increments of last lifespan
      push @SVNFILES,$d->{'h'};
    } else {
      $d = $d->{'c'}->{$elem};
      # Ok, check type
      throw("Conflicting type of element '$elem' in path '/".join('/',@{$svn})."'") unless $d->{'t'} eq 'd';
      # Revive, if needed
      if('D' eq &dir_GetStatus($d)) {
        $add = 1;
        push @{$d->{'h'}},['L', $SVNREVISION, $SVNREVISION - 1];
      }
    }
    if($add) {
      # Add directory in path
      # Add empty properties, unless it is last directory
      # without file & we asked so
      ################################
      # Print Node Change
      print $DUMP "Node-path: ",&helper_encodeNodePath($p),"\n",
                  "Node-kind: dir\n",
                  "Node-action: add\n";
      print $DUMP "Prop-content-length: 10\n",
                  "Content-length: 10\n",
                  "\n",
                  "PROPS-END\n",
                  "\n\n"
                  unless !$props_for_dir && $svn->[$#{$svn}] eq '/' && $i == $#{$svn}-1; # For LAST dir only
      #
      ################################

      # CHANGE last directory if it is TARGET
    } elsif (!$props_for_dir && $svn->[$#{$svn}] eq '/' && $i == $#{$svn}-1) {
      print $DUMP "Node-path: ",&helper_encodeNodePath($p),"\n",
                  "Node-kind: dir\n",
                  "Node-action: change\n";
    }
  }

  $elem = $svn->[$#{$svn}];
  # Last element could be file name or '/' -- directory
  return $d unless $elem ne '/';
  # Special processing for '.cvsignore' in any case
  if('.cvsignore' eq $elem) {
    ################################
    # Print Node Change
    print $DUMP "Node-path: ",&helper_encodeNodePath($p),"\n",
                "Node-kind: dir\n",
                "Node-action: change\n"
                if $props_for_dir;
    #
    ################################
    return undef;
  }

  $p .= '/'.$elem;
  ################################
  # Print Node Change
  print $DUMP "Node-path: ",&helper_encodeNodePath($p),"\n",
              "Node-kind: file\n";
  #
  ################################

  $add = '';
  if(!exists $d->{'c'}->{$elem}) {
    $d->{'c'}->{$elem} = {
      't' => 'f',
      'h' => [],
      'p' => $d,
      # For tree traversal
      '_' => undef
    };
    $d = $d->{'c'}->{$elem};
    # Speed up all leafs life incrementing
    push @SVNFILES,$d->{'h'};
    $add = 'add';
  } else {
    $d = $d->{'c'}->{$elem};
    throw("Conflicting type in path '$p'") unless $d->{'t'} eq 'f';
    # Check: is state 'D'ead or true revision?
    if(!ref(&dir_GetStatus($d))) {
      $add = 'add';
    } else {
      $add = 'change';
    }
  }
  ################################
  # Print Node Change
  print DUMP "Node-action: $add\n";
  #
  ################################
  return $d;
}

sub dir_GetLiveOne
{
  my ($svn) = @_;
  # Check all path: does it exists?
  my $d    = $CVS2SVN;
  my $elem = '';
  my $p    = '';

  for(my $i = 0; $i < $#{$svn}; ++$i) {
    $elem = $svn->[$i];
    $p .= '/'.$elem;
    throw("Could not retrieve inexistent path '/".join('/',@{$svn})."'") unless exists $d->{'c'}->{$elem};
    $d = $d->{'c'}->{$elem};
    throw("Conflicting type of element '$elem' in path '/".join('/',@{$svn})."'") unless $d->{'t'} eq 'd';
    throw("Could not retrieve dead path '/".join('/',@{$svn})."'") unless 'L' eq &dir_GetStatus($d);
  }

  $elem = $svn->[$#{$svn}];
  # Last element could be file name or '/' -- directory
  return $d unless $elem ne '/';

  # Special processing for '.cvsignore' in any case
  # It should be converted
  return undef unless '.cvsignore' ne $elem;

  throw("Could not retrieve inexistent path '/".join('/',@{$svn})."'") unless exists $d->{'c'}->{$elem};

  $d = $d->{'c'}->{$elem};
  throw("Conflicting type in path '$p'") unless $d->{'t'} eq 'f';
  return $d;
}

sub dir_IsLive
{
  my ($svn) = @_;
  # Check all path: does it exists?
  my $d    = $CVS2SVN;
  my $elem = '';
  my $p    = '';

  for(my $i = 0; $i < $#{$svn}; ++$i) {
    $elem = $svn->[$i];
    $p .= '/'.$elem;
    return undef unless exists $d->{'c'}->{$elem};
    $d = $d->{'c'}->{$elem};
    return undef unless $d->{'t'} eq 'd';
    return undef unless 'L' eq &dir_GetStatus($d);
  }

  $elem = $svn->[$#{$svn}];
  # Last element could be file name or '/' -- directory
  return $d unless $elem ne '/';

  # Special processing for '.cvsignore' in any case
  # It should be converted
  return undef unless '.cvsignore' ne $elem;

  return undef unless exists $d->{'c'}->{$elem};

  $d = $d->{'c'}->{$elem};
  return undef unless $d->{'t'} eq 'f';
  return $d;
}


sub dir_MarkAsAdded
{
  my ($svn) = @_;
  # Check all path: does it exists?
  my $d = $CVS2SVN;
  my $elem = '';
  my $p    = '';
  my $add  = 0;

  for(my $i = 0; $i < $#{$svn}; ++$i) {
    $elem = $svn->[$i];
    $p .= '/'.$elem;

    # Is directory exists?
    if(!exists $d->{'c'}->{$elem}) {
      $d->{'c'}->{$elem} = {
        't' => 'd',
        'h' => [['L', $SVNREVISION, $SVNREVISION - 1]],
        'c' => {},
        'p' => $d,
        # For tree traversal
        '_' => undef
      };
      $d = $d->{'c'}->{$elem};
      # For increments of last lifespan
      push @SVNFILES,$d->{'h'};
    } else {
      $d = $d->{'c'}->{$elem};
      # Ok, check type
      throw("Conflicting type of element '$elem' in path '/".join('/',@{$svn})."'") unless $d->{'t'} eq 'd';
      # Revive, if needed
      if('D' eq &dir_GetStatus($d)) {
        push @{$d->{'h'}},['L', $SVNREVISION, $SVNREVISION - 1];
      }
    }
  }

  $elem = $svn->[$#{$svn}];
  # Last element could be file name or '/' -- directory
  return $d unless $elem ne '/';
  # Special processing for '.cvsignore' in any case
  return undef if '.cvsignore' eq $elem;

  $p .= '/'.$elem;
  if(!exists $d->{'c'}->{$elem}) {
    $d->{'c'}->{$elem} = {
      't' => 'f',
      'h' => [],
      'p' => $d,
      # For tree traversal
      '_' => undef
    };
    $d = $d->{'c'}->{$elem};
    # Speed up all leafs life incrementing
    push @SVNFILES,$d->{'h'};
    $add = 'add';
  } else {
    $d = $d->{'c'}->{$elem};
  }
  return $d;
}

sub dir_Delete
{
  my ($svn, $DUMP) = @_;

  my $d = $CVS2SVN;
  my $elem = '';
  my $p    = '';
  my $ci   = 0;
  my $f    = undef;

  # Traverse tree to inner-most directory.
  for(my $i = 0; $i < $#{$svn}; ++$i) {
    $elem = $svn->[$i];
    $p .= '/'.$elem;
    throw("Could not delete inexistent path '/".join('/',@{$svn})."'") unless exists $d->{'c'}->{$elem};
    $d = $d->{'c'}->{$elem};
    throw("Conflicting type of element '$elem' in path '/".join('/',@{$svn})."'") unless $d->{'t'} eq 'd';
    # Don't check this, if file is .cvsignore, beacuse path could be dead already
    # For this path: deleted on previous file deletion
    if($svn->[$#{$svn}] ne '.cvsignore') {
      throw("Could not delete dead path '/".join('/',@{$svn})."'") unless 'L' eq &dir_GetStatus($d);
    } else {
      # Simple return '.cvsignore' flag
      return undef unless 'L' eq &dir_GetStatus($d);
    }
  }

  $elem = $svn->[$#{$svn}];
  # Is this file?
  $f = $d unless $elem ne '/';
  if($elem ne '/' && $elem ne '.cvsignore') {
    $p .= '/'.$elem;
    throw("Could not delete inexistent file '/".join('/',@{$svn})."'") unless exists $d->{'c'}->{$elem};
    $d = $d->{'c'}->{$elem};
    throw("Conflicting type of element '$elem' in path '/".join('/',@{$svn})."'") unless $d->{'t'} eq 'f';
    ################################
    # Print Node change
    print $DUMP "Node-path: ",&helper_encodeNodePath($p),"\n",
                "Node-action: delete\n",
                "\n\n";
    #
    ################################
    # Mark as dead HERE
    push @{$d->{'h'}},['D',$SVNREVISION,$SVNREVISION-1];

    $f = $d;
    # Step up, to parent directory
    $d = $d->{'p'};
    $p =~ s#/[^/]+$##;
  }

  while($d != $CVS2SVN && &dir_BecomeDead($d)) {
    ################################
    # Print Node change
    print $DUMP "Node-path: ",&helper_encodeNodePath($p),"\n",
                "Node-action: delete\n",
                "\n\n";
    #
    ################################
    push @{$d->{'h'}},['D',$SVNREVISION,$SVNREVISION-1];
    $d = $d->{'p'};
    # And split path.
    $p =~ s#/[^/]+$##;
  }
  # Ok, everything is deleted
  return $f;
}

sub dir_CleanTraverse
{
  my ($root) = @_;
  $root->{'_'} = undef;
  foreach my $c (values %{$root->{'c'}}) {
    $c->{'_'} = undef;
    &dir_CleanTraverse($c) if $c->{'t'} eq 'd';
  }
}

sub tree_cmp($$)
{
  my ($t1,$t2) = @_;
  my ($i,$r);
  # Compare as two refs -- speed ups things
  return 0 unless $t1 ne $t2;
  for($i = 0; $i <= $#{$t1} && $i <= $#{$t2}; ++$i) {
    # Special case: '/' is less than anything
    return -1 if $t1->[$i] eq '/' && $t2->[$i] ne '/';
    return  1 if $t1->[$i] ne '/' && $t2->[$i] eq '/';
    $r = $t1->[$i] cmp $t2->[$i];
    return $r if $r;
  }
  # Ok, return which is longer
  return $#{$t1} - $#{$t2};
}

sub intervals_intersect
{
  my ($a,$b) = @_;
  my ($ia,$ib) = (0,0);
  my ($t,$e);
  my @r = ();

  return () unless @{$a} && @{$b};

  # First array must be leftmost one
  ($a,$b) = ($b,$a) if $a->[0] > $b->[0];

  # While here is intervals in both arrays
  while ($ia < $#{$a} && $ib < $#{$b}) {
    # Go to close end in first array
    ++$ia;
    # While close end is ON LEFT from open end in second array, skip it
	while (($e = $a->[$ia++]) < $b->[$ib]) {
	    ++$ia;
		return @r unless $ia < $#{$a};
	}
	# Here we have OPEN end in $a in any case (first ++ will works always)
	# If close end in first array is ON RIGHT from close end in second array,
	# whole interval from second array
	if($e > $b->[$ib+1]) {
      push @r,($b->[$ib],$b->[$ib+1]);
      $ib += 2;
    # Make intersection & swap arrays
	} else {
      push @r,($b->[$ib],$e);
      ($a,$b) = ($b,$a);
      ($ia,$ib) = ($ib,$ia);
	}
  }
  return @r;
}

sub warning_FileError
{
  my ($cb, $name) = @_;
  print STDERR "Warning: file '$name' was skipped\n";
  return 1;
}

sub warning_DoubleFile
{
  my ($cb, $root, $f) = @_;
  print STDERR "Warning: File '$f' presents in '$root' and in 'Attic'.\n".
               "         Attic's version was skipped.\n";
  return 1;
}

sub warning_UnnamedBranch
{
  my ($cb, $f, $r) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains revision $r from untagged branch.\n".
               "         Revisions from this branch will be lost.\n";
  return 1;
}

sub warning_LostBranch
{
  my ($cb, $f, $r, $s) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains branch '$s' ($r) without branch point.\n".
               "         Revisions from this branch will be lost.\n";
  return 1;
}

sub warning_LostSymbol
{
  my ($cb, $f, $r, $s) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains symbol '$s' ($r) but no delta for this revision.\n".
               "         Symbol will be ignored.\n";
  return 1;
}

sub warning_DoubleSymbol
{
  my ($cb, $f, $r) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains branch '$r' tagged more than once.\n";
  return 1;
}

sub warning_InvalidSymRev
{
  my ($cb, $f, $r, $s) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains symbol '$s' with invalid revision '$r'.\n".
               "         Symbol will be ignored.\n";
  return 1;
}

sub warning_InvalidSymName
{
  my ($cb, $f, $r, $s) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains symbol '$s', that violates RCS specification.\n".
               "         Symbol will be used with invalid character replaced with %XX.\n";
  return 1;
}

sub warning_DoubleRev
{
  my ($cb, $f, $r, $s) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains symbol '$s' which marks more than one revision.\n".
               "         Symbol will be ignored.\n";
  return 1;
}

sub warning_LostRevision
{
  my ($cb, $f, $r) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains revision '$r' which is not in revision tree.\n".
               "         Delta will be ignored.\n";
  return 1;
}

sub warning_DiffSymType
{
  my ($cb, $f, $s, $t1, $t2) = @_;
  print STDERR "Warning: File '$f' contains symbol '$s' which have type '$t1', conflicting with '$t2' in other files.\n".
               "         Symbol will be appended with type.\n";
  return 1;
}

sub warning_DiffSymParent
{
  my ($cb, $s, @p) = @_;
  print STDERR "Warning: Symbol '$s' could have any of these parents: '",join("','",@p),"'\n".
               "         First one is selected.\n";
  return 1;
}

sub warning_NoSymParent
{
  my ($cb, $s, @p) = @_;
  print STDERR "Warning: Symbol '$s' contains files from these parents: '",join("','",@p),"'\n".
               "         But script can not find set of parents, which contains every file only once.\n".
               "         Symbol is ignored.\n";
  return 1;
}

sub warning_TimeMismatch
{
  my ($cb, $f, $cd, $pd) = @_;
  print STDERR "Warning: File '",$LN2RN->{$f->name()},"' contains two revisions '",$pd->rev(),"' (by ".$pd->author().") and '",$cd->rev(),"' (by ".$cd->author().")\n".
               "         with times goes backawrd: ", &warning_TimeMismatch_dt($pd->date())," and ", &warning_TimeMismatch_dt($cd->date()),"\n";
  return 1;
}

sub warning_TimeMismatch_dt
{
  #   0     1    2     3     4    5     6     7
  # ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday)
  my @gmt = gmtime($_[0]);
  return sprintf("%04d/%02d/%02d %02d:%02d:%02d",$gmt[5]+1900,$gmt[4]+1,$gmt[3],$gmt[2],$gmt[1],$gmt[0]);
}
