# (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 contains classes to set common cvs2xxx run options."""

# pylint: disable=unused-argument

import sys
import re
import optparse
from optparse import OptionGroup
import datetime
import codecs
import time

from cvs2hg_lib.version import VERSION
from cvs2hg_lib import config
from cvs2hg_lib.common import error_prefix
from cvs2hg_lib.common import FatalError
from cvs2hg_lib.man_writer import ManWriter
from cvs2hg_lib.log import logger
from cvs2hg_lib.context import Ctx
from cvs2hg_lib.man_writer import ManOption
from cvs2hg_lib.pass_manager import InvalidPassError
from cvs2hg_lib.revision_manager import NullRevisionCollector
from cvs2hg_lib.rcs_revision_manager import RCSRevisionReader
from cvs2hg_lib.cvs_revision_manager import CVSRevisionReader
from cvs2hg_lib.checkout_internal import InternalRevisionCollector
from cvs2hg_lib.checkout_internal import InternalRevisionReader
from cvs2hg_lib.symbol_strategy import AllBranchRule
from cvs2hg_lib.symbol_strategy import AllExcludedRule
from cvs2hg_lib.symbol_strategy import AllTagRule
from cvs2hg_lib.symbol_strategy import BranchIfCommitsRule
from cvs2hg_lib.symbol_strategy import ExcludeRegexpStrategyRule
from cvs2hg_lib.symbol_strategy import ForceBranchRegexpStrategyRule
from cvs2hg_lib.symbol_strategy import ForceTagRegexpStrategyRule
from cvs2hg_lib.symbol_strategy import ExcludeTrivialImportBranchRule
from cvs2hg_lib.symbol_strategy import HeuristicStrategyRule
from cvs2hg_lib.symbol_strategy import UnambiguousUsageRule
from cvs2hg_lib.symbol_strategy import HeuristicPreferredParentRule
from cvs2hg_lib.symbol_strategy import SymbolHintsFileRule
from cvs2hg_lib.symbol_transform import ReplaceSubstringsSymbolTransform
from cvs2hg_lib.symbol_transform import RegexpSymbolTransform
from cvs2hg_lib.symbol_transform import NormalizePathsSymbolTransform
from cvs2hg_lib.property_setters import AutoPropsPropertySetter
from cvs2hg_lib.property_setters import CVSBinaryFileDefaultMimeTypeSetter
from cvs2hg_lib.property_setters import CVSBinaryFileEOLStyleSetter
from cvs2hg_lib.property_setters import CVSRevisionNumberSetter
from cvs2hg_lib.property_setters import DefaultEOLStyleSetter
from cvs2hg_lib.property_setters import EOLStyleFromMimeTypeSetter
from cvs2hg_lib.property_setters import ExecutablePropertySetter
from cvs2hg_lib.property_setters import DescriptionPropertySetter
from cvs2hg_lib.property_setters import KeywordsPropertySetter
from cvs2hg_lib.property_setters import MimeMapper
from cvs2hg_lib.property_setters import SVNBinaryFileKeywordsPropertySetter


usage = """\
Usage: %prog --options OPTIONFILE
       %prog [OPTION...] --hgrepos HG-OUTPUT CVS-REPOS-PATH"""

description = """\
Convert a CVS repository into a Mercurial repository, including history.
"""


class IncompatibleOption(ManOption):
    """A ManOption that is incompatible with the --options option.

    Record that the option was used so that error checking can later be
    done."""

    def __init__(self, *args, **kw):
        ManOption.__init__(self, *args, **kw)

    def take_action(self, action, dest, opt, value, values, parser):
        oio = parser.values.options_incompatible_options
        if opt not in oio:
            oio.append(opt)
        return ManOption.take_action(
            self, action, dest, opt, value, values, parser
        )


class ContextOption(ManOption):
    """A ManOption that stores its value to Ctx."""

    def __init__(self, *args, **kw):
        if kw.get('action') not in self.STORE_ACTIONS:
            raise ValueError('Invalid action: %s' % (kw['action'],))

        self.__compatible_with_option = kw.pop('compatible_with_option', False)
        self.__action = kw.pop('action')
        try:
            self.__dest = kw.pop('dest')
        except KeyError:
            opt = args[0]
            if not opt.startswith('--'):
                raise ValueError
            self.__dest = opt[2:].replace('-', '_')
        if 'const' in kw:
            self.__const = kw.pop('const')

        kw['action'] = 'callback'
        kw['callback'] = self.__callback

        ManOption.__init__(self, *args, **kw)

    def __callback(self, option, opt_str, value, parser):
        if not self.__compatible_with_option:
            oio = parser.values.options_incompatible_options
            if opt_str not in oio:
                oio.append(opt_str)

        action = self.__action
        dest = self.__dest

        if action == "store":
            setattr(Ctx(), dest, value)
        elif action == "store_const":
            setattr(Ctx(), dest, self.__const)
        elif action == "store_true":
            setattr(Ctx(), dest, True)
        elif action == "store_false":
            setattr(Ctx(), dest, False)
        elif action == "append":
            getattr(Ctx(), dest).append(value)
        elif action == "count":
            setattr(Ctx(), dest, getattr(Ctx(), dest, 0) + 1)
        else:
            raise RuntimeError("unknown action %r" % self.__action)

        return 1


class IncompatibleOptionsException(FatalError):
    pass


# Options that are not allowed to be used with --trunk-only:
SYMBOL_OPTIONS = [
    '--symbol-transform',
    '--symbol-hints',
    '--force-branch',
    '--force-tag',
    '--exclude',
    '--keep-trivial-imports',
    '--symbol-default',
    '--no-cross-branch-commits',
]


class SymbolOptionsWithTrunkOnlyException(IncompatibleOptionsException):
    def __init__(self):
        IncompatibleOptionsException.__init__(
            self,
            'The following symbol-related options cannot be used together\n'
            'with --trunk-only:\n'
            '    %s'
            % ('\n    '.join(SYMBOL_OPTIONS),)
        )


def not_both(opt1val, opt1name, opt2val, opt2name):
    """Raise an exception if both opt1val and opt2val are set."""
    if opt1val and opt2val:
        raise IncompatibleOptionsException(
            "cannot pass both '%s' and '%s'." % (opt1name, opt2name,)
        )


class RunOptions(object):
    """A place to store meta-options that are used to start the conversion."""

    # Components of the man page.  Attributes set to None here must be set
    # by subclasses; others may be overridden/augmented by subclasses if
    # they wish.
    short_desc = None
    synopsis = None
    long_desc = None
    files = None
    authors = [
        u"C. Michael Pilato <cmpilato@collab.net>",
        u"Greg Stein <gstein@lyra.org>",
        u"Branko \u010cibej <brane@xbc.nu>",
        u"Blair Zajac <blair@orcaware.com>",
        u"Max Bowsher <maxb@ukf.net>",
        u"Brian Fitzpatrick <fitz@red-bean.com>",
        u"Tobias Ringstr\u00f6m <tobias@ringstrom.mine.nu>",
        u"Karl Fogel <kfogel@collab.net>",
        u"Erik H\u00fclsmann <e.huelsmann@gmx.net>",
        u"David Summers <david@summersoft.fay.ar.us>",
        u"Michael Haggerty <mhagger@alum.mit.edu>",
    ]
    see_also = None

    def __init__(self, progname, cmd_args, pass_manager):
        """Process the command-line options, storing run options to SELF.

        PROGNAME is the name of the program, used in the usage string.
        CMD_ARGS is the list of command-line arguments passed to the
        program.  PASS_MANAGER is an instance of PassManager, needed to
        help process the -p and --help-passes options."""

        self.progname = progname
        self.cmd_args = cmd_args
        self.pass_manager = pass_manager
        self.start_pass = 1
        self.end_pass = self.pass_manager.num_passes
        self.profiling = False

        self.projects = []

        # A list of one list of SymbolStrategyRules for each project:
        self.project_symbol_strategy_rules = []

        parser = self.parser = optparse.OptionParser(
            usage=usage,
            description=self.get_description(),
            add_help_option=False,
        )
        # A place to record any options used that are incompatible with
        # --options:
        parser.set_default('options_incompatible_options', [])

        # Populate the options parser with the options, one group at a
        # time:
        parser.add_option_group(self._get_options_file_options_group())
        parser.add_option_group(self._get_output_options_group())
        parser.add_option_group(self._get_conversion_options_group())
        parser.add_option_group(self._get_symbol_handling_options_group())
        parser.add_option_group(self._get_extraction_options_group())
        parser.add_option_group(self._get_environment_options_group())
        parser.add_option_group(self._get_partial_conversion_options_group())
        parser.add_option_group(self._get_information_options_group())

        (self.options, self.args) = parser.parse_args(args=self.cmd_args)

        # Now the log level has been set; log the time when the run started:
        logger.verbose(
            time.strftime(
                'Conversion start time: %Y-%m-%d %I:%M:%S %Z',
                time.localtime(logger.start_time)
            )
        )

        if self.options.options_file_found:
            # Check that no options that are incompatible with --options
            # were used:
            self.verify_option_compatibility()
        else:
            # --options was not specified.  So do the main initialization
            # based on other command-line options:
            self.process_options()

        # Check for problems with the options:
        self.check_options()

    def get_description(self):
        return description

    def _get_options_file_options_group(self):
        group = OptionGroup(
            self.parser, 'Configuration via options file'
            )
        self.parser.set_default('options_file_found', False)
        group.add_option(ManOption(
            '--options', type='string',
            action='callback', callback=self.callback_options,
            help=(
                'read the conversion options from PATH.  This '
                'method allows more flexibility than using '
                'command-line options.  See documentation for info'
            ),
            man_help=(
                'Read the conversion options from \\fIpath\\fR instead of from '
                'the command line.  This option allows far more conversion '
                'flexibility than can be achieved using the command-line alone. '
                'See the documentation for more information.  Only the following '
                'command-line options are allowed in combination with '
                '\\fB--options\\fR: \\fB-h\\fR/\\fB--help\\fR, '
                '\\fB--help-passes\\fR, \\fB--version\\fR, '
                '\\fB-v\\fR/\\fB--verbose\\fR, \\fB-q\\fR/\\fB--quiet\\fR, '
                '\\fB-p\\fR/\\fB--pass\\fR/\\fB--passes\\fR, \\fB--dry-run\\fR, '
                '\\fB--profile\\fR, \\fB--trunk-only\\fR, \\fB--encoding\\fR, '
                'and \\fB--fallback-encoding\\fR. '
                'Options are processed in the order specified on the command '
                'line.'
            ),
            metavar='PATH',
        ))
        return group

    def _get_output_options_group(self):
        group = OptionGroup(self.parser, 'Output options')
        return group

    def _get_conversion_options_group(self):
        group = OptionGroup(self.parser, 'Conversion options')
        group.add_option(ContextOption(
            '--trunk-only',
            action='store_true',
            compatible_with_option=True,
            help='convert only trunk commits, not tags nor branches',
            man_help=(
                'Convert only trunk commits, not tags nor branches.'
            ),
        ))
        group.add_option(ManOption(
            '--encoding', type='string',
            action='callback', callback=self.callback_encoding,
            help=(
                'encoding for paths and log messages in CVS repos.  '
                'If option is specified multiple times, encoders '
                'are tried in order until one succeeds.  See '
                'http://docs.python.org/lib/standard-encodings.html '
                'for a list of standard Python encodings.'
            ),
            man_help=(
                'Use \\fIencoding\\fR as the encoding for filenames, log '
                'messages, and author names in the CVS repos.  This option '
                'may be specified multiple times, in which case the encodings '
                'are tried in order until one succeeds.  Default: ascii.  See '
                'http://docs.python.org/lib/standard-encodings.html for a list '
                'of other standard encodings.'
            ),
            metavar='ENC',
        ))
        group.add_option(ManOption(
            '--fallback-encoding', type='string',
            action='callback', callback=self.callback_fallback_encoding,
            help='If all --encodings fail, use lossy encoding with ENC',
            man_help=(
                'If none of the encodings specified with \\fB--encoding\\fR '
                'succeed in decoding an author name or log message, then fall '
                'back to using \\fIencoding\\fR in lossy \'replace\' mode. '
                'Use of this option may cause information to be lost, but at '
                'least it allows the conversion to run to completion.  This '
                'option only affects the encoding of log messages and author '
                'names; there is no fallback encoding for filenames.  (By '
                'using an \\fB--options\\fR file, it is possible to specify '
                'a fallback encoding for filenames.)  Default: disabled.'
            ),
            metavar='ENC',
        ))
        group.add_option(ContextOption(
            '--retain-conflicting-attic-files',
            action='store_true',
            help=(
                'if a file appears both in and out of '
                'the CVS Attic, then leave the attic version in a '
                'SVN directory called "Attic"'
            ),
            man_help=(
                'If a file appears both inside an outside of the CVS attic, '
                'retain the attic version in an SVN subdirectory called '
                '\'Attic\'.  (Normally this situation is treated as a fatal '
                'error.)'
            ),
        ))
        group.add_option(ContextOption(
            '--username', type='string',
            action='store',
            help='username for cvs2hg-synthesized commits',
            man_help=(
                'Set the default username to \\fIname\\fR when cvs2svn needs '
                'to generate a commit for which CVS does not record the '
                'original username. This happens when a branch or tag is '
                'created. The default is to use no author at all for such '
                'commits.'
            ),
            metavar='NAME',
        ))

        return group

    def _get_symbol_handling_options_group(self):
        group = OptionGroup(self.parser, 'Symbol handling')
        self.parser.set_default('symbol_transforms', [])
        group.add_option(IncompatibleOption(
            '--symbol-transform', type='string',
            action='callback', callback=self.callback_symbol_transform,
            help=(
                'transform symbol names from P to S, where P and S '
                'use Python regexp and reference syntax '
                'respectively.  P must match the whole symbol name'
            ),
            man_help=(
                'Transform RCS/CVS symbol names before entering them into '
                'Subversion. \\fIpattern\\fR is a Python regexp pattern that '
                'is matches against the entire symbol name; \\fIreplacement\\fR '
                'is a replacement using Python\'s regexp reference syntax. '
                'You may specify any number of these options; they will be '
                'applied in the order given on the command line.'
            ),
            metavar='P:S',
        ))
        self.parser.set_default('symbol_strategy_rules', [])
        group.add_option(IncompatibleOption(
            '--symbol-hints', type='string',
            action='callback', callback=self.callback_symbol_hints,
            help='read symbol conversion hints from PATH',
            man_help=(
                'Read symbol conversion hints from \\fIpath\\fR.  The format of '
                '\\fIpath\\fR is the same as the format output by '
                '\\fB--write-symbol-info\\fR, namely a text file with four '
                'whitespace-separated columns: \\fIproject-id\\fR, '
                '\\fIsymbol\\fR, \\fIconversion\\fR, and '
                '\\fIparent-lod-name\\fR.  \\fIproject-id\\fR is the numerical '
                'ID of the project to which the symbol belongs, counting from '
                '0. \\fIproject-id\\fR can be set to \'.\' if '
                'project-specificity is not needed.  \\fIsymbol-name\\fR is the '
                'name of the symbol being specified.  \\fIconversion\\fR '
                'specifies how the symbol should be converted, and can be one '
                'of the values \'branch\', \'tag\', or \'exclude\'. If '
                '\\fIconversion\\fR is \'.\', then this rule does not affect '
                'how the symbol is converted.  \\fIparent-lod-name\\fR is the '
                'name of the symbol from which this symbol should sprout, or '
                '\'.trunk.\' if the symbol should sprout from trunk.  If '
                '\\fIparent-lod-name\\fR is omitted or \'.\', then this rule '
                'does not affect the preferred parent of this symbol. The file '
                'may contain blank lines or comment lines (lines whose first '
                'non-whitespace character is \'#\').'
            ),
            metavar='PATH',
        ))
        self.parser.set_default('symbol_default', 'heuristic')
        group.add_option(IncompatibleOption(
            '--symbol-default', type='choice',
            choices=['heuristic', 'strict', 'branch', 'tag', 'exclude'],
            action='store',
            help=(
                'specify how ambiguous symbols are converted.  '
                'OPT is "heuristic" (default), "strict", "branch", '
                '"tag" or "exclude"'
            ),
            man_help=(
                'Specify how to convert ambiguous symbols (those that appear in '
                'the CVS archive as both branches and tags).  \\fIopt\\fR must '
                'be \'heuristic\' (decide how to treat each ambiguous symbol '
                'based on whether it was used more often as a branch/tag in '
                'CVS), \'strict\' (no default; every ambiguous symbol has to be '
                'resolved manually using \\fB--force-branch\\fR, '
                '\\fB--force-tag\\fR, or \\fB--exclude\\fR), \'branch\' (treat '
                'every ambiguous symbol as a branch), \'tag\' (treat every '
                'ambiguous symbol as a tag), or \'exclude\' (do not convert '
                'ambiguous symbols).  The default is \'heuristic\'.'
            ),
            metavar='OPT',
        ))
        group.add_option(IncompatibleOption(
            '--force-branch', type='string',
            action='callback', callback=self.callback_force_branch,
            help='force symbols matching REGEXP to be branches',
            man_help=(
                'Force symbols whose names match \\fIregexp\\fR to be branches. '
                '\\fIregexp\\fR must match the whole symbol name.'
            ),
            metavar='REGEXP',
        ))
        group.add_option(IncompatibleOption(
            '--force-tag', type='string',
            action='callback', callback=self.callback_force_tag,
            help='force symbols matching REGEXP to be tags',
            man_help=(
                'Force symbols whose names match \\fIregexp\\fR to be tags. '
                '\\fIregexp\\fR must match the whole symbol name.'
            ),
            metavar='REGEXP',
        ))
        group.add_option(IncompatibleOption(
            '--exclude', type='string',
            action='callback', callback=self.callback_exclude,
            help='exclude branches and tags matching REGEXP',
            man_help=(
                'Exclude branches and tags whose names match \\fIregexp\\fR '
                'from the conversion.  \\fIregexp\\fR must match the whole '
                'symbol name.'
            ),
            metavar='REGEXP',
        ))
        self.parser.set_default('keep_trivial_imports', False)
        group.add_option(IncompatibleOption(
            '--keep-trivial-imports',
            action='store_true',
            help=(
                'do not exclude branches that were only used for '
                'a single import (usually these are unneeded)'
            ),
            man_help=(
                'Do not exclude branches that were only used for a single '
                'import. (By default such branches are excluded because they '
                'are usually created by the inappropriate use of \\fBcvs '
                'import\\fR.)'
            ),
        ))

        return group

    def _get_extraction_options_group(self):
        group = OptionGroup(self.parser, 'Extraction options')

        return group

    def _add_use_internal_co_option(self, group):
        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.'
            ),
        ))

    def _add_use_cvs_option(self, group):
        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.'
            ),
        ))

    def _add_use_rcs_option(self, group):
        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.'
            ),
        ))

    def _get_environment_options_group(self):
        group = OptionGroup(self.parser, 'Environment options')
        group.add_option(ContextOption(
            '--tmpdir', type='string',
            action='store',
            help=(
                'directory to use for temporary data files '
                '(default "test-temporary-tmp-dir")'
            ),
            man_help=(
                'Set the \\fIpath\\fR to use for temporary data. Default '
                'is a directory called \\fItest-temporary-tmp-dir\\fR under the current '
                'directory.'
            ),
            metavar='PATH',
        ))
        self.parser.set_default('co_executable', config.CO_EXECUTABLE)
        group.add_option(IncompatibleOption(
            '--co', type='string',
            action='store', dest='co_executable',
            help='path to the "co" program (required if --use-rcs)',
            man_help=(
                'Path to the \\fIco\\fR program.  (\\fIco\\fR is needed if the '
                '\\fB--use-rcs\\fR option is used.)'
            ),
            metavar='PATH',
        ))
        self.parser.set_default('cvs_executable', config.CVS_EXECUTABLE)
        group.add_option(IncompatibleOption(
            '--cvs', type='string',
            action='store', dest='cvs_executable',
            help='path to the "cvs" program (required if --use-cvs)',
            man_help=(
                'Path to the \\fIcvs\\fR program.  (\\fIcvs\\fR is needed if the '
                '\\fB--use-cvs\\fR option is used.)'
            ),
            metavar='PATH',
        ))

        return group

    def _get_partial_conversion_options_group(self):
        group = OptionGroup(self.parser, 'Partial conversions')
        group.add_option(ManOption(
            '--pass', type='string',
            action='callback', callback=self.callback_passes,
            help='execute only specified PASS of conversion',
            man_help=(
                'Execute only pass \\fIpass\\fR of the conversion. '
                '\\fIpass\\fR can be specified by name or by number (see '
                '\\fB--help-passes\\fR).'
            ),
            metavar='PASS',
        ))
        group.add_option(ManOption(
            '--passes', '-p', type='string',
            action='callback', callback=self.callback_passes,
            help=(
                'execute passes START through END, inclusive (PASS, '
                'START, and END can be pass names or numbers)'
            ),
            man_help=(
                'Execute passes \\fIstart\\fR through \\fIend\\fR of the '
                'conversion (inclusive). \\fIstart\\fR and \\fIend\\fR can be '
                'specified by name or by number (see \\fB--help-passes\\fR). '
                'If \\fIstart\\fR or \\fIend\\fR is missing, it defaults to '
                'the first or last pass, respectively. For this to work the '
                'earlier passes must have been completed before on the '
                'same CVS repository, and the generated data files must be '
                'in the temporary directory (see \\fB--tmpdir\\fR).'
            ),
            metavar='[START]:[END]',
        ))

        return group

    def _get_information_options_group(self):
        group = OptionGroup(self.parser, 'Information options')
        group.add_option(ManOption(
            '--version',
            action='callback', callback=self.callback_version,
            help='print the version number',
            man_help='Print the version number.',
        ))
        group.add_option(ManOption(
            '--help', '-h',
            action="help",
            help='print this usage message and exit with success',
            man_help='Print the usage message and exit with success.',
        ))
        group.add_option(ManOption(
            '--help-passes',
            action='callback', callback=self.callback_help_passes,
            help='list the available passes and their numbers',
            man_help=(
                'Print the numbers and names of the conversion passes and '
                'exit with success.'
            ),
        ))
        group.add_option(ManOption(
            '--man',
            action='callback', callback=self.callback_manpage,
            help='write the manpage for this program to standard output',
            man_help=(
                'Output the unix-style manpage for this program to standard '
                'output.'
            ),
        ))
        group.add_option(ManOption(
            '--verbose', '-v',
            action='callback', callback=self.callback_verbose,
            help='verbose (may be specified twice for debug output)',
            man_help=(
                'Print more information while running. This option may be '
                'specified twice to output voluminous debugging information.'
            ),
        ))
        group.add_option(ManOption(
            '--quiet', '-q',
            action='callback', callback=self.callback_quiet,
            help='quiet (may be specified twice for very quiet)',
            man_help=(
                'Print less information while running. This option may be '
                'specified twice to suppress all non-error output.'
            ),
        ))
        group.add_option(ContextOption(
            '--write-symbol-info', type='string',
            action='store', dest='symbol_info_filename',
            help='write information and statistics about CVS symbols to PATH.',
            man_help=(
                'Write to \\fIpath\\fR symbol statistics and information about '
                'how symbols were converted during CollateSymbolsPass.'
            ),
            metavar='PATH',
        ))
        group.add_option(ContextOption(
            '--skip-cleanup',
            action='store_true',
            help='prevent the deletion of intermediate files',
            man_help='Prevent the deletion of temporary files.',
        ))
        prof = 'cProfile'
        try:
            import cProfile
        except ImportError as e:
            prof = 'hotshot'
            group.add_option(ManOption(
                '--profile',
                action='callback', callback=self.callback_profile,
                help='profile with \'' + prof + '\' (into file cvs2svn.' + prof + ')',
                man_help=(
                    'Profile with \'' + prof + '\' (into file \\fIcvs2svn.' + prof + '\\fR).'
                ),
            ))

        return group

    def callback_options(self, option, opt_str, value, parser):
        parser.values.options_file_found = True
        self.process_options_file(value)

    def callback_encoding(self, option, opt_str, value, parser):
        ctx = Ctx()

        try:
            ctx.cvs_author_decoder.add_encoding(value)
            ctx.cvs_log_decoder.add_encoding(value)
            ctx.cvs_filename_decoder.add_encoding(value)
        except LookupError as e:
            raise FatalError(str(e))

    def callback_fallback_encoding(self, option, opt_str, value, parser):
        ctx = Ctx()

        try:
            ctx.cvs_author_decoder.set_fallback_encoding(value)
            ctx.cvs_log_decoder.set_fallback_encoding(value)
            # Don't use fallback_encoding for filenames.
        except LookupError as e:
            raise FatalError(str(e))

    def callback_help_passes(self, option, opt_str, value, parser):
        self.pass_manager.help_passes()
        sys.exit(0)

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

    def callback_version(self, option, opt_str, value, parser):
        sys.stdout.write(
            '%s version %s\n' % (self.progname, VERSION)
            )
        sys.exit(0)

    def callback_verbose(self, option, opt_str, value, parser):
        logger.increase_verbosity()

    def callback_quiet(self, option, opt_str, value, parser):
        logger.decrease_verbosity()

    def callback_passes(self, option, opt_str, value, parser):
        if value.find(':') >= 0:
            start_pass, end_pass = value.split(':')
            self.start_pass = self.pass_manager.get_pass_number(start_pass, 1)
            self.end_pass = self.pass_manager.get_pass_number(
                end_pass, self.pass_manager.num_passes
                )
        else:
            self.end_pass = \
                self.start_pass = \
                self.pass_manager.get_pass_number(value)

    def callback_profile(self, option, opt_str, value, parser):
        self.profiling = True

    def callback_symbol_hints(self, option, opt_str, value, parser):
        parser.values.symbol_strategy_rules.append(SymbolHintsFileRule(value))

    def callback_force_branch(self, option, opt_str, value, parser):
        parser.values.symbol_strategy_rules.append(
            ForceBranchRegexpStrategyRule(value)
        )

    def callback_force_tag(self, option, opt_str, value, parser):
        parser.values.symbol_strategy_rules.append(
            ForceTagRegexpStrategyRule(value)
        )

    def callback_exclude(self, option, opt_str, value, parser):
        parser.values.symbol_strategy_rules.append(
            ExcludeRegexpStrategyRule(value)
        )

    def callback_cvs_revnums(self, option, opt_str, value, parser):
        Ctx().revision_property_setters.append(CVSRevisionNumberSetter())

    def callback_symbol_transform(self, option, opt_str, value, parser):
        [pattern, replacement] = value.split(":")
        try:
            parser.values.symbol_transforms.append(
                RegexpSymbolTransform(pattern, replacement)
            )
        except re.error:
            raise FatalError("'%s' is not a valid regexp." % (pattern,))

    # Common to SVNRunOptions, HgRunOptions (GitRunOptions and
    # BzrRunOptions do not support --use-internal-co, so cannot use this).
    def process_all_extraction_options(self):
        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_collector = NullRevisionCollector()
            ctx.revision_reader = RCSRevisionReader(options.co_executable)
        elif options.use_cvs:
            ctx.revision_collector = NullRevisionCollector()
            ctx.revision_reader = CVSRevisionReader(options.cvs_executable)
        else:
            # --use-internal-co is the default:
            ctx.revision_collector = InternalRevisionCollector(compress=True)
            ctx.revision_reader = InternalRevisionReader(compress=True)

    def process_symbol_strategy_options(self):
        """Process symbol strategy-related options."""

        ctx = Ctx()
        options = self.options

        # Add the standard symbol name cleanup rules:
        self.options.symbol_transforms.extend([
            ReplaceSubstringsSymbolTransform('\\','/'),
            # Remove leading, trailing, and repeated slashes:
            NormalizePathsSymbolTransform(),
            ])

        if ctx.trunk_only:
            if options.symbol_strategy_rules or options.keep_trivial_imports:
                raise SymbolOptionsWithTrunkOnlyException()

        else:
            if not options.keep_trivial_imports:
                options.symbol_strategy_rules.append(ExcludeTrivialImportBranchRule())

            options.symbol_strategy_rules.append(UnambiguousUsageRule())
            if options.symbol_default == 'strict':
                pass
            elif options.symbol_default == 'branch':
                options.symbol_strategy_rules.append(AllBranchRule())
            elif options.symbol_default == 'tag':
                options.symbol_strategy_rules.append(AllTagRule())
            elif options.symbol_default == 'heuristic':
                options.symbol_strategy_rules.append(BranchIfCommitsRule())
                options.symbol_strategy_rules.append(HeuristicStrategyRule())
            elif options.symbol_default == 'exclude':
                options.symbol_strategy_rules.append(AllExcludedRule())
            else:
                assert False

            # Now add a rule whose job it is to pick the preferred parents of
            # branches and tags:
            options.symbol_strategy_rules.append(HeuristicPreferredParentRule())

    def process_property_setter_options(self):
        """Process the options that set SVN properties."""

        ctx = Ctx()
        options = self.options

        ctx.file_property_setters.append(CVSBinaryFileEOLStyleSetter())

        ctx.file_property_setters.append(CVSBinaryFileDefaultMimeTypeSetter())

        ctx.file_property_setters.append(SVNBinaryFileKeywordsPropertySetter())

        ctx.file_property_setters.append(ExecutablePropertySetter())

        ctx.file_property_setters.append(DescriptionPropertySetter())

    def process_options(self):
        """Do the main configuration based on command-line options.

        This method is only called if the --options option was not
        specified."""

        raise NotImplementedError()

    def check_options(self):
        """Check the the run options are OK.

        This should only be called after all options have been processed."""

        # Convenience var, so we don't have to keep instantiating this Borg.
        ctx = Ctx()

        if not self.start_pass <= self.end_pass:
            raise InvalidPassError(
                'Ending pass must not come before starting pass.')

        if not ctx.dry_run and ctx.output_option is None:
            raise FatalError('No output option specified.')

        if ctx.output_option is not None:
            ctx.output_option.check()

        if not self.projects:
            raise FatalError('No project specified.')

    def verify_option_compatibility(self):
        """Verify that no options incompatible with --options were used.

        The --options option was specified.  Verify that no incompatible
        options or arguments were specified."""

        if self.options.options_incompatible_options or self.args:
            if self.options.options_incompatible_options:
                oio = self.options.options_incompatible_options
                logger.error(
                    '%s: The following options cannot be used in combination with '
                    'the --options\n'
                    'option:\n'
                    '    %s\n'
                    % (error_prefix, '\n    '.join(oio))
                )
            if self.args:
                logger.error(
                    '%s: No cvs-repos-path arguments are allowed with the --options '
                    'option.\n'
                    % (error_prefix,)
                )
            sys.exit(1)

    def process_options_file(self, options_filename):
        """Read options from the file named OPTIONS_FILENAME.

        Store the run options to SELF."""

        from cvs2hg_lib.compat import perform_execfile

        glb = {
            'ctx': Ctx(),
            'run_options': self,
        }
        # Using execfile is common inside tests and may happen in prod configs
        glb['execfile'] = perform_execfile

        perform_execfile(options_filename, glb)

    def usage(self):
        self.parser.print_help()
