filter-repo: restructure argument parsing for re-use

Signed-off-by: Elijah Newren <newren@gmail.com>
This commit is contained in:
Elijah Newren 2018-12-24 22:59:38 -08:00
parent 9bb4188e83
commit 2f3a445875

View File

@ -25,9 +25,9 @@ import textwrap
from datetime import tzinfo, timedelta, datetime from datetime import tzinfo, timedelta, datetime
__all__ = ["Blob", "Reset", "FileChanges", "Commit", "Tag", "Progress", __all__ = ["Blob", "Reset", "FileChanges", "Commit", "Tag", "Progress",
"Checkpoint", "FastExportFilter", "FixedTimeZone", "Checkpoint", "FastExportFilter", "FixedTimeZone", "ProgressWriter",
"fast_export_output", "fast_import_input", "get_commit_count", "fast_export_output", "fast_import_input", "get_commit_count",
"get_total_objects", "record_id_rename"] "get_total_objects", "record_id_rename", "FilteringOptions"]
def _timedelta_to_seconds(delta): def _timedelta_to_seconds(delta):
@ -1670,186 +1670,203 @@ _CURRENT_STREAM_NUMBER = 0
###################################################################### ######################################################################
class AppendFilter(argparse.Action): class FilteringOptions(object):
def __call__(self, parser, namespace, values, option_string=None): class AppendFilter(argparse.Action):
suffix = option_string[len('--path-'):] or 'match' def __call__(self, parser, namespace, values, option_string=None):
if suffix == 'rename': suffix = option_string[len('--path-'):] or 'match'
mod_type = 'rename' if suffix == 'rename':
match_type = 'prefix' mod_type = 'rename'
elif suffix.startswith('rename-'): match_type = 'prefix'
mod_type = 'rename' elif suffix.startswith('rename-'):
match_type = suffix[len('rename-'):] mod_type = 'rename'
else: match_type = suffix[len('rename-'):]
mod_type = 'filter' else:
match_type = suffix mod_type = 'filter'
items = getattr(namespace, self.dest, []) or [] match_type = suffix
items.append((mod_type, match_type, values)) items = getattr(namespace, self.dest, []) or []
setattr(namespace, self.dest, items) items.append((mod_type, match_type, values))
setattr(namespace, self.dest, items)
class HelperFilter(argparse.Action): class HelperFilter(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
af = AppendFilter(dest='path_changes', option_strings=None) af = FilteringOptions.AppendFilter(dest='path_changes',
dirname = values if values[-1] == '/' else values+'/' option_strings=None)
if option_string == '--subdirectory-filter': dirname = values if values[-1] == '/' else values+'/'
af(parser, namespace, dirname, '--path-match') if option_string == '--subdirectory-filter':
af(parser, namespace, dirname+':', '--path-rename') af(parser, namespace, dirname, '--path-match')
elif option_string == '--to-subdirectory-filter': af(parser, namespace, dirname+':', '--path-rename')
af(parser, namespace, ':'+dirname, '--path-rename') elif option_string == '--to-subdirectory-filter':
else: af(parser, namespace, ':'+dirname, '--path-rename')
raise SystemExit("Error: HelperFilter given invalid option_string: {}" else:
.format(option_string)) raise SystemExit("Error: HelperFilter given invalid option_string: {}"
.format(option_string))
def get_args(): @staticmethod
# Include usage in the summary, so we can put the description first def create_arg_parser():
summary = '''Rewrite (or analyze) repository history # Include usage in the summary, so we can put the description first
summary = '''Rewrite (or analyze) repository history
git-filter-repo destructively rewrites history (unless --analyze or --dry-run git-filter-repo destructively rewrites history (unless --analyze or --dry-run
are specified) according to specified rules. It refuses to do any rewriting are specified) according to specified rules. It refuses to do any rewriting
unless either run from a clean fresh clone, or --force was specified. unless either run from a clean fresh clone, or --force was specified.
Basic Usage: Basic Usage:
git-filter-repo --analyze git-filter-repo --analyze
git-filter-repo [FILTER/RENAME/CONTROL OPTIONS] git-filter-repo [FILTER/RENAME/CONTROL OPTIONS]
See EXAMPLES section for details. See EXAMPLES section for details.
'''.rstrip() '''.rstrip()
# Provide a long helpful examples section # Provide a long helpful examples section
example_text = '''EXAMPLES example_text = '''EXAMPLES
To get help: To get help:
git-filter-repo --help git-filter-repo --help
''' '''
# Create the basic parser # Create the basic parser
parser = argparse.ArgumentParser(description=summary, parser = argparse.ArgumentParser(description=summary,
usage = argparse.SUPPRESS, usage = argparse.SUPPRESS,
add_help = False, add_help = False,
epilog = example_text, epilog = example_text,
formatter_class=argparse.RawDescriptionHelpFormatter) formatter_class=argparse.RawDescriptionHelpFormatter)
# FIXME: Need to special case all --* args that rev-list takes, or call # FIXME: Need to special case all --* args that rev-list takes, or call
# git rev-parse ... # git rev-parse ...
analyze = parser.add_argument_group(title='Analysis') analyze = parser.add_argument_group(title='Analysis')
analyze.add_argument('--analyze', action='store_true', analyze.add_argument('--analyze', action='store_true',
help='''Analyze repository history and create a help='''Analyze repository history and create a
report that may be useful in determining report that may be useful in determining
what to filter in a subsequent run. Will what to filter in a subsequent run. Will
not modify your repo.''') not modify your repo.''')
refs = parser.add_argument_group(title='Git References (positional args)') refs = parser.add_argument_group(title='Git References (positional args)')
refs.add_argument('refs', nargs='*', refs.add_argument('refs', nargs='*',
help='''git refs (branches, tags, etc.) to rewrite, help='''git refs (branches, tags, etc.) to rewrite,
e.g. 'master v2.1.0 v0.5.0'. Special rev-list e.g. 'master v2.1.0 v0.5.0'. Special rev-list
options, such as --branches, --tags, --all, --glob, options, such as --branches, --tags, --all, --glob,
or --exclude are allowed. [default: --all]. To or --exclude are allowed. [default: --all]. To
avoid mixing new history with old, any references avoid mixing new history with old, any references
not specified will be deleted.''') not specified will be deleted.''')
path = parser.add_argument_group(title='Filtering based on paths') path = parser.add_argument_group(title='Filtering based on paths')
path.add_argument('--invert-paths', action='store_false', path.add_argument('--invert-paths', action='store_false',
dest='inclusive', dest='inclusive',
help='''Invert the selection of files from the specified help='''Invert the selection of files from the specified
--path-{match,glob,regex} options below, i.e. only --path-{match,glob,regex} options below, i.e. only
select files matching none of those options.''') select files matching none of those options.''')
path.add_argument('--path-match', '--path', metavar='DIR_OR_FILE', path.add_argument('--path-match', '--path', metavar='DIR_OR_FILE',
action=AppendFilter, dest='path_changes', action=FilteringOptions.AppendFilter, dest='path_changes',
help='''Exact paths (files or directories) to include in help='''Exact paths (files or directories) to include in
filtered history. Multiple --path options can be filtered history. Multiple --path options can be
specified to get a union of paths.''') specified to get a union of paths.''')
path.add_argument('--path-glob', metavar='GLOB', path.add_argument('--path-glob', metavar='GLOB',
action=AppendFilter, dest='path_changes', action=FilteringOptions.AppendFilter, dest='path_changes',
help='''Glob of paths to include in filtered history. help='''Glob of paths to include in filtered history.
Multiple --path-glob options can be specified to Multiple --path-glob options can be specified to
get a union of paths.''') get a union of paths.''')
path.add_argument('--path-regex', metavar='REGEX', path.add_argument('--path-regex', metavar='REGEX',
action=AppendFilter, dest='path_changes', action=FilteringOptions.AppendFilter, dest='path_changes',
help='''Regex of paths to include in filtered history. help='''Regex of paths to include in filtered history.
Multiple --path-regex options can be specified to Multiple --path-regex options can be specified to
get a union of paths''') get a union of paths''')
rename = parser.add_argument_group(title='Renaming based on paths') rename = parser.add_argument_group(title='Renaming based on paths')
rename.add_argument('--path-rename', '--path-rename-prefix', rename.add_argument('--path-rename', '--path-rename-prefix',
metavar='OLD_NAME:NEW_NAME', metavar='OLD_NAME:NEW_NAME',
action=AppendFilter, dest='path_changes', action=FilteringOptions.AppendFilter,
help='''Prefix to rename; if filename starts with dest='path_changes',
OLD_NAME, replace that with NEW_NAME. Multiple help='''Prefix to rename; if filename starts with
--path-rename options can be specified.''') OLD_NAME, replace that with NEW_NAME. Multiple
--path-rename options can be specified.''')
refrename = parser.add_argument_group(title='Renaming of refs') refrename = parser.add_argument_group(title='Renaming of refs')
refrename.add_argument('--tag-rename', metavar='OLD:NEW', refrename.add_argument('--tag-rename', metavar='OLD:NEW',
help='''Rename tags starting with OLD to start with help='''Rename tags starting with OLD to start with
NEW. e.g. --tag-rename foo:bar will rename NEW. e.g. --tag-rename foo:bar will rename
tag foo-1.2.3 to bar-1.2.3; either OLD or NEW tag foo-1.2.3 to bar-1.2.3; either OLD or NEW
can be empty.''') can be empty.''')
helpers = parser.add_argument_group(title='Shortcuts') helpers = parser.add_argument_group(title='Shortcuts')
helpers.add_argument('--subdirectory-filter', metavar='DIRECTORY', helpers.add_argument('--subdirectory-filter', metavar='DIRECTORY',
action=HelperFilter, action=FilteringOptions.HelperFilter,
help='''Only look at history that touches the given help='''Only look at history that touches the given
subdirectory and treat that directory as the subdirectory and treat that directory as the
project root. Equivalent to using project root. Equivalent to using
"--path DIRECTORY/ --path-rename DIRECTORY/:"''') "--path DIRECTORY/ --path-rename DIRECTORY/:"
helpers.add_argument('--to-subdirectory-filter', metavar='DIRECTORY', ''')
action=HelperFilter, helpers.add_argument('--to-subdirectory-filter', metavar='DIRECTORY',
help='''Treat the project root as instead being under action=FilteringOptions.HelperFilter,
DIRECTORY. Equivalent to using help='''Treat the project root as instead being under
"--path-rename :DIRECTORY/"''') DIRECTORY. Equivalent to using
"--path-rename :DIRECTORY/"''')
misc = parser.add_argument_group(title='Miscellaneous options') misc = parser.add_argument_group(title='Miscellaneous options')
misc.add_argument('--help', '-h', action='store_true', misc.add_argument('--help', '-h', action='store_true',
help='''Show this help message and exit.''') help='''Show this help message and exit.''')
misc.add_argument('--force', '-f', action='store_true', misc.add_argument('--force', '-f', action='store_true',
help='''Rewrite history even if the current repo does not help='''Rewrite history even if the current repo does not
look like a fresh clone.''') look like a fresh clone.''')
misc.add_argument('--dry-run', action='store_true', misc.add_argument('--dry-run', action='store_true',
help='''Do not change the repository. Run `git help='''Do not change the repository. Run `git
fast-export` and filter its output, and save both fast-export` and filter its output, and save both
the original and the filtered version for the original and the filtered version for
comparison. Some filtering of empty commits may comparison. Some filtering of empty commits may
not occur due to inability to query the fast-import not occur due to inability to query the fast-import
backend.''') backend.''')
misc.add_argument('--debug', action='store_true', misc.add_argument('--debug', action='store_true',
help='''Print additional information about operations being help='''Print additional information about operations being
performed and commands being run. When used performed and commands being run. When used
together with --dry-run, also show extra together with --dry-run, also show extra
information about what would be run.''') information about what would be run.''')
misc.add_argument('--stdin', action='store_true', misc.add_argument('--stdin', action='store_true',
help='''Instead of running `git fast-export` and filtering help='''Instead of running `git fast-export` and filtering
its output, filter the fast-export stream from its output, filter the fast-export stream from
stdin.''') stdin.''')
misc.add_argument('--quiet', action='store_true', misc.add_argument('--quiet', action='store_true',
help='''Pass --quiet to other git commands called''') help='''Pass --quiet to other git commands called''')
return parser
if len(sys.argv) == 1: @staticmethod
parser.print_usage() def sanity_check_args(args):
raise SystemExit("No arguments specified.") if not args.refs:
args = parser.parse_args() args.refs = ['--all']
if args.help: if args.analyze and args.path_changes:
parser.print_help() raise SystemExit("Error: --analyze is incompatible with --path* flags; "
raise SystemExit() "it's a read-only operation.")
if not args.refs: if args.analyze and args.stdin:
args.refs = ['--all'] raise SystemExit("Error: --analyze is incompatible with --stdin.")
if args.analyze and args.path_changes: # If no path_changes are found, initialize with empty list but mark as
raise SystemExit("Error: --analyze is incompatible with --path* flags; " # not inclusive so that all files match
"it's a read-only operation.") if args.path_changes == None:
if args.analyze and args.stdin: args.path_changes = []
raise SystemExit("Error: --analyze is incompatible with --stdin.")
# If no path_changes are found, initialize with empty list but mark as
# not inclusive so that all files match
if args.path_changes == None:
args.path_changes = []
args.inclusive = False
# Similarly, if we only have renames, all paths should match
else:
has_filter = False
for (mod_type, match_type, path_expression) in args.path_changes:
if mod_type == 'filter':
has_filter = True
if not has_filter:
args.inclusive = False args.inclusive = False
return args # Similarly, if we only have renames, all paths should match
else:
has_filter = False
for (mod_type, match_type, path_expression) in args.path_changes:
if mod_type == 'filter':
has_filter = True
if not has_filter:
args.inclusive = False
@staticmethod
def default_options():
return FilteringOptions.parse_args([], error_on_empty = False)
@staticmethod
def parse_args(input_args, error_on_empty = True):
parser = FilteringOptions.create_arg_parser()
if not input_args and error_on_empty:
parser.print_usage()
raise SystemExit("No arguments specified.")
args = parser.parse_args(input_args)
if args.help:
parser.print_help()
raise SystemExit()
FilteringOptions.sanity_check_args(args)
return args
def is_repository_bare(): def is_repository_bare():
output = subprocess.check_output('git rev-parse --is-bare-repository'.split()) output = subprocess.check_output('git rev-parse --is-bare-repository'.split())
@ -2474,7 +2491,7 @@ class DualFileWriter:
self.file2.close() self.file2.close()
def run_fast_filter(): def run_fast_filter():
args = get_args() args = FilteringOptions.parse_args(sys.argv[1:])
if args.debug: if args.debug:
print("[DEBUG] Parsed arguments:\n{}".format(args)) print("[DEBUG] Parsed arguments:\n{}".format(args))