# (Be in -*- python -*- mode.)
#
# ====================================================================
# Copyright (c) 2000-2009 CollabNet.  All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.  The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals.  For exact contribution history, see the revision
# history and logs, available at http://cvs2svn.tigris.org/.
# ====================================================================

"""This module manages cvs2svn run options."""


import sys
import optparse
import datetime
import codecs

from cvs2svn_lib.version import VERSION
from cvs2svn_lib import config
from cvs2svn_lib.common import warning_prefix
from cvs2svn_lib.common import error_prefix
from cvs2svn_lib.common import FatalError
from cvs2svn_lib.common import normalize_svn_path
from cvs2svn_lib.log import Log
from cvs2svn_lib.context import Ctx
from cvs2svn_lib.run_options import not_both
from cvs2svn_lib.run_options import RunOptions
from cvs2svn_lib.run_options import ContextOption
from cvs2svn_lib.run_options import IncompatibleOption
from cvs2svn_lib.run_options import authors
from cvs2svn_lib.man_writer import ManWriter
from cvs2svn_lib.project import Project
from cvs2svn_lib.svn_output_option import DumpfileOutputOption
from cvs2svn_lib.svn_output_option import ExistingRepositoryOutputOption
from cvs2svn_lib.svn_output_option import NewRepositoryOutputOption
from cvs2svn_lib.revision_manager import NullRevisionRecorder
from cvs2svn_lib.revision_manager import NullRevisionExcluder
from cvs2svn_lib.rcs_revision_manager import RCSRevisionReader
from cvs2svn_lib.cvs_revision_manager import CVSRevisionReader
from cvs2svn_lib.checkout_internal import InternalRevisionRecorder
from cvs2svn_lib.checkout_internal import InternalRevisionExcluder
from cvs2svn_lib.checkout_internal import InternalRevisionReader
from cvs2svn_lib.symbol_strategy import TrunkPathRule
from cvs2svn_lib.symbol_strategy import BranchesPathRule
from cvs2svn_lib.symbol_strategy import TagsPathRule


short_desc = 'convert a cvs repository into a subversion repository'

synopsis = """\
.B cvs2svn
[\\fIOPTION\\fR]... \\fIOUTPUT-OPTION CVS-REPOS-PATH\\fR
.br
.B cvs2svn
[\\fIOPTION\\fR]... \\fI--options=PATH\\fR
"""

long_desc = """\
Create a new Subversion repository based on the version history stored in a
CVS repository. Each CVS commit will be mirrored in the Subversion
repository, including such information as date of commit and id of the
committer.
.P
\\fICVS-REPOS-PATH\\fR is the filesystem path of the part of the CVS
repository that you want to convert.  It is not possible to convert a
CVS repository to which you only have remote access; see the FAQ for
more information.  This path doesn't have to be the top level
directory of a CVS repository; it can point at a project within a
repository, in which case only that project will be converted.  This
path or one of its parent directories has to contain a subdirectory
called CVSROOT (though the CVSROOT directory can be empty).
.P
Multiple CVS repositories can be converted into a single Subversion
repository in a single run of cvs2svn, but only by using an
\\fB--options\\fR file.
"""

files = """\
A directory called \\fIcvs2svn-tmp\\fR (or the directory specified by
\\fB--tmpdir\\fR) is used as scratch space for temporary data files.
"""

see_also = [
  ('cvs', '1'),
  ('svn', '1'),
  ('svnadmin', '1'),
  ]


class SVNRunOptions(RunOptions):
  def _get_output_options_group(self):
    group = RunOptions._get_output_options_group(self)

    group.add_option(IncompatibleOption(
        '--svnrepos', '-s', type='string',
        action='store',
        help='path where SVN repos should be created',
        man_help=(
            'Write the output of the conversion into a Subversion repository '
            'located at \\fIpath\\fR.  This option causes a new Subversion '
            'repository to be created at \\fIpath\\fR unless the '
            '\\fB--existing-svnrepos\\fR option is also used.'
            ),
        metavar='PATH',
        ))
    self.parser.set_default('existing_svnrepos', False)
    group.add_option(IncompatibleOption(
        '--existing-svnrepos',
        action='store_true',
        help='load into existing SVN repository (for use with --svnrepos)',
        man_help=(
            'Load the converted CVS repository into an existing Subversion '
            'repository, instead of creating a new repository.  (This option '
            'should be used in combination with '
            '\\fB-s\\fR/\\fB--svnrepos\\fR.)  The repository must either be '
            'empty or contain no paths that overlap with those that will '
            'result from the conversion.  Please note that you need write '
            'permission for the repository files.'
            ),
        ))
    group.add_option(IncompatibleOption(
        '--fs-type', type='string',
        action='store',
        help=(
            'pass --fs-type=TYPE to "svnadmin create" (for use with '
            '--svnrepos)'
            ),
        man_help=(
            'Pass \\fI--fs-type\\fR=\\fItype\\fR to "svnadmin create" when '
            'creating a new repository.'
            ),
        metavar='TYPE',
        ))
    self.parser.set_default('bdb_txn_nosync', False)
    group.add_option(IncompatibleOption(
        '--bdb-txn-nosync',
        action='store_true',
        help=(
            'pass --bdb-txn-nosync to "svnadmin create" (for use with '
            '--svnrepos)'
            ),
        man_help=(
            'Pass \\fI--bdb-txn-nosync\\fR to "svnadmin create" when '
            'creating a new BDB-style Subversion repository.'
            ),
        ))
    self.parser.set_default('create_options', [])
    group.add_option(IncompatibleOption(
        '--create-option', type='string',
        action='append', dest='create_options',
        help='pass OPT to "svnadmin create" (for use with --svnrepos)',
        man_help=(
            'Pass \\fIopt\\fR to "svnadmin create" when creating a new '
            'Subversion repository (can be specified multiple times to '
            'pass multiple options).'
            ),
        metavar='OPT',
        ))
    group.add_option(IncompatibleOption(
        '--dumpfile', type='string',
        action='store',
        help='just produce a dumpfile; don\'t commit to a repos',
        man_help=(
            'Just produce a dumpfile; don\'t commit to an SVN repository. '
            'Write the dumpfile to \\fIpath\\fR.'
            ),
        metavar='PATH',
        ))

    group.add_option(ContextOption(
        '--dry-run',
        action='store_true',
        help=(
            'do not create a repository or a dumpfile; just print what '
            'would happen.'
            ),
        man_help=(
            'Do not create a repository or a dumpfile; just print the '
            'details of what cvs2svn would do if it were really converting '
            'your repository.'
            ),
        ))

    # Deprecated options:
    self.parser.set_default('dump_only', False)
    group.add_option(IncompatibleOption(
        '--dump-only',
        action='callback', callback=self.callback_dump_only,
        help=optparse.SUPPRESS_HELP,
        man_help=optparse.SUPPRESS_HELP,
        ))
    group.add_option(IncompatibleOption(
        '--create',
        action='callback', callback=self.callback_create,
        help=optparse.SUPPRESS_HELP,
        man_help=optparse.SUPPRESS_HELP,
        ))

    return group

  def _get_conversion_options_group(self):
    group = RunOptions._get_conversion_options_group(self)

    self.parser.set_default('trunk_base', config.DEFAULT_TRUNK_BASE)
    group.add_option(IncompatibleOption(
        '--trunk', type='string',
        action='store', dest='trunk_base',
        help=(
            'path for trunk (default: %s)'
            % (config.DEFAULT_TRUNK_BASE,)
            ),
        man_help=(
            'Set the top-level path to use for trunk in the Subversion '
            'repository. The default is \\fI%s\\fR.'
            % (config.DEFAULT_TRUNK_BASE,)
            ),
        metavar='PATH',
        ))
    self.parser.set_default('branches_base', config.DEFAULT_BRANCHES_BASE)
    group.add_option(IncompatibleOption(
        '--branches', type='string',
        action='store', dest='branches_base',
        help=(
            'path for branches (default: %s)'
            % (config.DEFAULT_BRANCHES_BASE,)
            ),
        man_help=(
            'Set the top-level path to use for branches in the Subversion '
            'repository.  The default is \\fI%s\\fR.'
            % (config.DEFAULT_BRANCHES_BASE,)
            ),
        metavar='PATH',
        ))
    self.parser.set_default('tags_base', config.DEFAULT_TAGS_BASE)
    group.add_option(IncompatibleOption(
        '--tags', type='string',
        action='store', dest='tags_base',
        help=(
            'path for tags (default: %s)'
            % (config.DEFAULT_TAGS_BASE,)
            ),
        man_help=(
            'Set the top-level path to use for tags in the Subversion '
            'repository. The default is \\fI%s\\fR.'
            % (config.DEFAULT_TAGS_BASE,)
            ),
        metavar='PATH',
        ))
    group.add_option(ContextOption(
        '--no-prune',
        action='store_false', dest='prune',
        help='don\'t prune empty directories',
        man_help=(
            'When all files are deleted from a directory in the Subversion '
            'repository, don\'t delete the empty directory (the default is '
            'to delete any empty directories).'
            ),
        ))
    group.add_option(ContextOption(
        '--no-cross-branch-commits',
        action='store_false', dest='cross_branch_commits',
        help='prevent the creation of cross-branch commits',
        man_help=(
            'Prevent the creation of commits that affect files on multiple '
            'branches at once.'
            ),
        ))

    return group

  def _get_extraction_options_group(self):
    group = RunOptions._get_extraction_options_group(self)

    self.parser.set_default('use_internal_co', False)
    group.add_option(IncompatibleOption(
        '--use-internal-co',
        action='store_true',
        help=(
            'use internal code to extract revision contents '
            '(fastest but disk space intensive) (default)'
            ),
        man_help=(
            'Use internal code to extract revision contents.  This '
            'is up to 50% faster than using \\fB--use-rcs\\fR, but needs '
            'a lot of disk space: roughly the size of your CVS repository '
            'plus the peak size of a complete checkout of the repository '
            'with all branches that existed and still had commits pending '
            'at a given time.  This option is the default.'
            ),
        ))
    self.parser.set_default('use_cvs', False)
    group.add_option(IncompatibleOption(
        '--use-cvs',
        action='store_true',
        help=(
            'use CVS to extract revision contents (slower than '
            '--use-internal-co or --use-rcs)'
            ),
        man_help=(
            'Use CVS to extract revision contents.  This option is slower '
            'than \\fB--use-internal-co\\fR or \\fB--use-rcs\\fR.'
            ),
        ))
    self.parser.set_default('use_rcs', False)
    group.add_option(IncompatibleOption(
        '--use-rcs',
        action='store_true',
        help=(
            'use RCS to extract revision contents (faster than '
            '--use-cvs but fails in some cases)'
            ),
        man_help=(
            'Use RCS \'co\' to extract revision contents.  This option is '
            'faster than \\fB--use-cvs\\fR but fails in some cases.'
            ),
        ))

    return group

  def _get_environment_options_group(self):
    group = RunOptions._get_environment_options_group(self)

    group.add_option(ContextOption(
        '--svnadmin', type='string',
        action='store', dest='svnadmin_executable',
        help='path to the "svnadmin" program',
        man_help=(
            'Path to the \\fIsvnadmin\\fR program.  (\\fIsvnadmin\\fR is '
            'needed when the \\fB-s\\fR/\\fB--svnrepos\\fR output option is '
            'used.)'
            ),
        metavar='PATH',
        ))

    return group

  def callback_dump_only(self, option, opt_str, value, parser):
    parser.values.dump_only = True
    Log().error(
        warning_prefix +
        ': The --dump-only option is deprecated (it is implied '
        'by --dumpfile).\n'
        )

  def callback_create(self, option, opt_str, value, parser):
    Log().error(
        warning_prefix +
        ': The behaviour produced by the --create option is now the '
        'default;\n'
        'passing the option is deprecated.\n'
        )

  def callback_manpage(self, option, opt_str, value, parser):
    f = codecs.getwriter('utf_8')(sys.stdout)
    ManWriter(
        parser,
        section='1',
        date=datetime.date.today(),
        source='Version %s' % (VERSION,),
        manual='User Commands',
        short_desc=short_desc,
        synopsis=synopsis,
        long_desc=long_desc,
        files=files,
        authors=authors,
        see_also=see_also,
        ).write_manpage(f)
    sys.exit(0)

  def process_extraction_options(self):
    """Process options related to extracting data from the CVS repository."""

    ctx = Ctx()
    options = self.options

    not_both(options.use_rcs, '--use-rcs',
             options.use_cvs, '--use-cvs')

    not_both(options.use_rcs, '--use-rcs',
             options.use_internal_co, '--use-internal-co')

    not_both(options.use_cvs, '--use-cvs',
             options.use_internal_co, '--use-internal-co')

    if options.use_rcs:
      ctx.revision_recorder = NullRevisionRecorder()
      ctx.revision_excluder = NullRevisionExcluder()
      ctx.revision_reader = RCSRevisionReader(options.co_executable)
    elif options.use_cvs:
      ctx.revision_recorder = NullRevisionRecorder()
      ctx.revision_excluder = NullRevisionExcluder()
      ctx.revision_reader = CVSRevisionReader(options.cvs_executable)
    else:
      # --use-internal-co is the default:
      ctx.revision_recorder = InternalRevisionRecorder(compress=True)
      ctx.revision_excluder = InternalRevisionExcluder()
      ctx.revision_reader = InternalRevisionReader(compress=True)

  def process_output_options(self):
    """Process the options related to SVN output."""

    ctx = Ctx()
    options = self.options

    if options.dump_only and not options.dumpfile:
      raise FatalError("'--dump-only' requires '--dumpfile' to be specified.")

    if not options.svnrepos and not options.dumpfile and not ctx.dry_run:
      raise FatalError("must pass one of '-s' or '--dumpfile'.")

    not_both(options.svnrepos, '-s',
             options.dumpfile, '--dumpfile')

    not_both(options.dumpfile, '--dumpfile',
             options.existing_svnrepos, '--existing-svnrepos')

    not_both(options.bdb_txn_nosync, '--bdb-txn-nosync',
             options.existing_svnrepos, '--existing-svnrepos')

    not_both(options.dumpfile, '--dumpfile',
             options.bdb_txn_nosync, '--bdb-txn-nosync')

    not_both(options.fs_type, '--fs-type',
             options.existing_svnrepos, '--existing-svnrepos')

    if (
          options.fs_type
          and options.fs_type != 'bdb'
          and options.bdb_txn_nosync
          ):
      raise FatalError("cannot pass --bdb-txn-nosync with --fs-type=%s."
                       % options.fs_type)

    if options.svnrepos:
      if options.existing_svnrepos:
        ctx.output_option = ExistingRepositoryOutputOption(options.svnrepos)
      else:
        ctx.output_option = NewRepositoryOutputOption(
            options.svnrepos,
            fs_type=options.fs_type, bdb_txn_nosync=options.bdb_txn_nosync,
            create_options=options.create_options)
    else:
      ctx.output_option = DumpfileOutputOption(options.dumpfile)

  def add_project(
        self,
        project_cvs_repos_path,
        trunk_path=None, branches_path=None, tags_path=None,
        initial_directories=[],
        symbol_transforms=None,
        symbol_strategy_rules=[],
        ):
    """Add a project to be converted.

    Most arguments are passed straight through to the Project
    constructor.  SYMBOL_STRATEGY_RULES is an iterable of
    SymbolStrategyRules that will be applied to symbols in this
    project."""

    if trunk_path is not None:
      trunk_path = normalize_svn_path(trunk_path, allow_empty=True)
    if branches_path is not None:
      branches_path = normalize_svn_path(branches_path, allow_empty=False)
    if tags_path is not None:
      tags_path = normalize_svn_path(tags_path, allow_empty=False)

    initial_directories = [
        path
        for path in [trunk_path, branches_path, tags_path]
        if path
        ] + [
        normalize_svn_path(path)
        for path in initial_directories
        ]

    symbol_strategy_rules = list(symbol_strategy_rules)

    # Add rules to set the SVN paths for LODs depending on whether
    # they are the trunk, tags, or branches:
    if trunk_path is not None:
      symbol_strategy_rules.append(TrunkPathRule(trunk_path))
    if branches_path is not None:
      symbol_strategy_rules.append(BranchesPathRule(branches_path))
    if tags_path is not None:
      symbol_strategy_rules.append(TagsPathRule(tags_path))

    id = len(self.projects)
    project = Project(
        id,
        project_cvs_repos_path,
        initial_directories=initial_directories,
        symbol_transforms=symbol_transforms,
        )

    self.projects.append(project)
    self.project_symbol_strategy_rules.append(symbol_strategy_rules)

  def clear_projects(self):
    """Clear the list of projects to be converted.

    This method is for the convenience of options files, which may
    want to import one another."""

    del self.projects[:]
    del self.project_symbol_strategy_rules[:]

  def process_options(self):
    # Consistency check for options and arguments.
    if len(self.args) == 0:
      self.usage()
      sys.exit(1)

    if len(self.args) > 1:
      Log().error(error_prefix + ": must pass only one CVS repository.\n")
      self.usage()
      sys.exit(1)

    cvsroot = self.args[0]

    self.process_extraction_options()
    self.process_output_options()
    self.process_symbol_strategy_options()
    self.process_property_setter_options()

    # Create the default project (using ctx.trunk, ctx.branches, and
    # ctx.tags):
    self.add_project(
        cvsroot,
        trunk_path=self.options.trunk_base,
        branches_path=self.options.branches_base,
        tags_path=self.options.tags_base,
        symbol_transforms=self.options.symbol_transforms,
        symbol_strategy_rules=self.options.symbol_strategy_rules,
        )


