filter-repo: implement --paths-from-file

This allows the user to put a whole bunch of paths they want to keep (or
want to remove) in a file and then just provide the path to it.  They
can also use globs or regexes (similar to --replace-text) and can also
do renames.  In fact, this allows regex renames, despite the fact that I
never added a --path-rename-regex option.

Signed-off-by: Elijah Newren <newren@gmail.com>
pull/13/head
Elijah Newren 5 years ago
parent 9744c57106
commit 2472d1c93f

@ -1918,6 +1918,12 @@ class FilteringOptions(object):
raise SystemExit(_("Error: HelperFilter given invalid option_string: %s")
% option_string) # pragma: no cover
class FileWithPathsFilter(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if not namespace.path_changes:
namespace.path_changes = []
namespace.path_changes += FilteringOptions.get_paths_from_file(values)
@staticmethod
def create_arg_parser():
# Include usage in the summary, so we can put the description first
@ -1996,6 +2002,13 @@ class FilteringOptions(object):
"specified."))
helpers = parser.add_argument_group(title=_("Path shortcuts"))
helpers.add_argument('--paths-from-file', metavar='FILENAME',
type=os.fsencode,
action=FilteringOptions.FileWithPathsFilter, dest='path_changes',
help=_("Specify several path filtering and renaming directives, one "
"per line. Lines with '==>' in them specify path renames, "
"and lines can begin with 'literal:' (the default), 'glob:', "
"or 'regex: ' to specify different matching styles"))
helpers.add_argument('--subdirectory-filter', metavar='DIRECTORY',
action=FilteringOptions.HelperFilter, type=os.fsencode,
help=_("Only look at history that touches the given subdirectory "
@ -2207,6 +2220,50 @@ class FilteringOptions(object):
replace_literals.append((line, replacement))
return {'literals': replace_literals, 'regexes': replace_regexes}
@staticmethod
def get_paths_from_file(filename):
new_path_changes = []
with open(filename, 'br') as f:
for line in f:
line = line.rstrip(b'\r\n')
# Skip blank lines
if not line:
continue
# Determine the replacement
match_type, repl = 'literal', None
if b'==>' in line:
line, repl = line.rsplit(b'==>', 1)
# See if we need to match via regex
match_type = 'match' # a.k.a. 'literal'
if line.startswith(b'regex:'):
match_type = 'regex'
match = re.compile(line[6:])
elif line.startswith(b'glob:'):
match_type = 'glob'
match = line[5:]
if repl:
raise SystemExit(_("Error: In %s, 'glob:' and '==>' are incompatible (renaming globs makes no sense)" % decode(filename)))
else:
if line.startswith(b'literal:'):
match = line[8:]
else:
match = line
if repl is not None:
if match and repl and match.endswith(b'/') != repl.endswith(b'/'):
raise SystemExit(_("Error: When rename directories, if OLDNAME "
"and NEW_NAME are both non-empty and either "
"ends with a slash then both must."))
# Record the filter or rename
if repl is not None:
new_path_changes.append(['rename', match_type, (match, repl)])
else:
new_path_changes.append(['filter', match_type, match])
return new_path_changes
@staticmethod
def default_options():
return FilteringOptions.parse_args([], error_on_empty = False)
@ -2948,9 +3005,11 @@ class RepoFilter(object):
wanted = True
elif mod_type == 'rename':
match, repl = path_exp
assert match_type in ('match',)
assert match_type in ('match','regex') # glob was translated to regex
if match_type == 'match' and filename_matches(match, full_pathname):
full_pathname = full_pathname.replace(match, repl, 1)
if match_type == 'regex':
full_pathname = match.sub(repl, full_pathname)
return full_pathname if (wanted == filtering_is_inclusive) else None
# Change the commit message according to callback

@ -145,6 +145,36 @@ test_expect_success '--path-rename inability to squash' '
)
'
test_expect_success '--paths-from-file' '
(
git clone file://"$(pwd)"/path_rename paths_from_file &&
cd paths_from_file &&
cat >../path_changes <<-EOF &&
literal:values/huge
values/huge==>values/gargantuan
glob:*rge
regex:.*med.*
regex:^([^/]*)/(.*)ge$==>\2/\1/ge
EOF
git filter-repo --paths-from-file ../path_changes &&
git log --format=%n --name-only | sort | uniq >filenames &&
# intermediate, medium, two larges, gargantuan, and a blank line
test_line_count = 6 filenames &&
! grep sequences/tiny filenames &&
grep sequences/intermediate filenames &&
grep lar/sequences/ge filenames &&
grep lar/values/ge filenames &&
grep values/gargantuan filenames &&
! grep sequences/small filenames &&
grep sequences/medium filenames &&
rm ../path_changes
)
'
test_expect_success 'setup metasyntactic repo' '
test_create_repo metasyntactic &&
(
@ -861,7 +891,13 @@ test_expect_success 'other startup error cases and requests for help' '
test_i18ngrep ": --use-base-name and --path-rename are incompatible" err &&
test_must_fail git filter-repo --path-rename foo:bar/ 2>err &&
test_i18ngrep "either ends with a slash then both must." err
test_i18ngrep "either ends with a slash then both must." err &&
test_must_fail git filter-repo --paths-from-file <(echo "foo==>bar/") 2>err &&
test_i18ngrep "either ends with a slash then both must." err &&
test_must_fail git filter-repo --paths-from-file <(echo "glob:*.py==>newname") 2>err &&
test_i18ngrep "renaming globs makes no sense" err
)
'

Loading…
Cancel
Save