From 2431904f26c3e6b83bd276a0bf592d43bdc2121d Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 16 Feb 2019 23:59:43 -0800 Subject: [PATCH] filter-repo: add command line parameters for passing body of callbacks Many of the callback functions might only be a single line, and as such instead of forcing the user to write a full blown program with an import and everything, let them just specify the body of the callback function as a command line parameter. Add several tests of this functionality as well. Signed-off-by: Elijah Newren --- git-filter-repo | 82 +++++++++++++++--- t/t9392-python-callback.sh | 173 +++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 11 deletions(-) create mode 100755 t/t9392-python-callback.sh diff --git a/git-filter-repo b/git-filter-repo index 5fca0ce..fedaece 100755 --- a/git-filter-repo +++ b/git-filter-repo @@ -1908,6 +1908,37 @@ class FilteringOptions(object): a replacement choice other than the default of "***REMOVED***". ''') + callback = parser.add_argument_group(title='Generic callback code snippets') + callback.add_argument('--filename-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing filenames; + see CALLBACKS sections below.''') + callback.add_argument('--message-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing messages + (both commit messages and tag messages); + see CALLBACKS sections below.''') + callback.add_argument('--name-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing names of + people; see CALLBACKS sections below.''') + callback.add_argument('--email-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing emails + addresses; see CALLBACKS sections below.''') + callback.add_argument('--refname-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing refnames; + see CALLBACKS sections below.''') + + callback.add_argument('--blob-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing blob objects; + see CALLBACKS sections below.''') + callback.add_argument('--commit-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing commit objects; + see CALLBACKS sections below.''') + callback.add_argument('--tag-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing tag objects; + see CALLBACKS sections below.''') + callback.add_argument('--reset-callback', metavar="FUNCTION_BODY", + help='''Python code body for processing reset objects; + see CALLBACKS sections below.''') + location = parser.add_argument_group(title='Location to filter from/to') location.add_argument('--source', help='''Git repository to read from''') @@ -2521,7 +2552,7 @@ class RepoFilter(object): args, filename_callback = None, message_callback = None, - person_name_callback = None, + name_callback = None, email_callback = None, refname_callback = None, blob_callback = None, @@ -2540,11 +2571,12 @@ class RepoFilter(object): self._everything_callback = everything_callback # {blob,commit,tag,reset} # Store callbacks for acting on slices of FastExport objects - self._filename_callback = filename_callback # filenames from commits - self._message_callback = message_callback # commit OR tag message - self._person_name_callback = person_name_callback # author, committer, tagger - self._email_callback = email_callback # author, committer, tagger - self._refname_callback = refname_callback # from commit/tag/reset + self._filename_callback = filename_callback # filenames from commits + self._message_callback = message_callback # commit OR tag message + self._name_callback = name_callback # author, committer, tagger + self._email_callback = email_callback # author, committer, tagger + self._refname_callback = refname_callback # from commit/tag/reset + self._handle_arg_callbacks() # Defaults for input self._input = None @@ -2563,6 +2595,34 @@ class RepoFilter(object): self._orig_refs = None self._newnames = {} + def _handle_arg_callbacks(self): + def make_callback(argname, str): + exec('def callback({}):\n'.format(argname)+ + ' '+'\n '.join(str.splitlines()), globals()) + return callback #namespace['callback'] + def handle(type): + callback_field = '_{}_callback'.format(type) + code_string = getattr(self._args, type+'_callback') + if code_string: + if getattr(self, callback_field): + raise SystemExit("Error: Cannot pass a {}_callback to RepoFilter " + "AND pass --{}-callback" + .format(type, type)) + if 'return ' not in code_string and \ + type not in ('blob', 'commit', 'tag', 'reset'): + raise SystemExit("Error: --{}-callback should have a return statement" + .format(type)) + setattr(self, callback_field, make_callback(type, code_string)) + handle('filename') + handle('message') + handle('name') + handle('email') + handle('refname') + handle('blob') + handle('commit') + handle('tag') + handle('reset') + def _run_sanity_checks(self): self._sanity_checks_handled = True if not self._managed_output: @@ -2699,9 +2759,9 @@ class RepoFilter(object): commit.committer_name, commit.committer_email = \ args.mailmap.translate(commit.committer_name, commit.committer_email) # Change author & committer according to callbacks - if self._person_name_callback: - commit.author_name = self._person_name_callback(commit.author_name) - commit.committer_name = self._person_name_callback(commit.committer_name) + if self._name_callback: + commit.author_name = self._name_callback(commit.author_name) + commit.committer_name = self._name_callback(commit.committer_name) if self._email_callback: commit.author_email = self._email_callback(commit.author_email) commit.committer_email = self._email_callback(commit.committer_email) @@ -2775,8 +2835,8 @@ class RepoFilter(object): if self._args.mailmap: tag.tagger_name, tag.tagger_email = \ self._args.mailmap.translate(tag.tagger_name, tag.tagger_email) - if self._person_name_callback: - tag.tagger_name = self._person_name_callback(tag.tagger_name) + if self._name_callback: + tag.tagger_name = self._name_callback(tag.tagger_name) if self._email_callback: tag.tagger_email = self._email_callback(tag.tagger_email) diff --git a/t/t9392-python-callback.sh b/t/t9392-python-callback.sh new file mode 100755 index 0000000..243228f --- /dev/null +++ b/t/t9392-python-callback.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +test_description='Usage of git-filter-repo with python callbacks' +. ./test-lib.sh + +export PATH=$(dirname $TEST_DIRECTORY):$PATH # Put git-filter-repo in PATH + +setup() +{ + git init $1 && + ( + cd $1 && + echo hello > world && + git add world && + test_tick && + git commit -m initial && + printf "The launch code is 1-2-3-4." > secret && + git add secret && + test_tick && + git commit -m "Sssh. Dont tell no one" && + echo A file that you cant trust > file.doc && + echo there >> world && + git add file.doc world && + test_tick && + printf "Random useless changes\n\nLet us be like the marketing group. Marketing is staffed with pansies" | git commit -F - && + echo Do not use a preposition to end a setence with > advice && + git add advice && + test_tick && + GIT_AUTHOR_NAME="Copy N. Paste" git commit -m "hypocrisy is fun" && + echo Avoid cliches like the plague >> advice && + test_tick && + GIT_AUTHOR_EMAIL="foo@my.crp" git commit -m "it is still fun" advice && + echo " \$Id: A bunch of junk$" > foobar.c && + git add foobar.c && + test_tick && + git commit -m "Brain damage" && + + git tag v1.0 HEAD~3 && + git tag -a -m 'Super duper snazzy release' v2.0 HEAD~1 && + git branch testing master && + + # Make it look like a fresh clone (avoid need for --force) + git gc && + git remote add origin . && + git update-ref refs/remotes/origin/master refs/heads/master + git update-ref refs/remotes/origin/testing refs/heads/testing + ) +} + +test_expect_success '--filename-callback' ' + setup filename-callback && + ( + cd filename-callback && + git filter-repo --filename-callback "return None if filename.endswith(\".doc\") else \"src/\"+filename" && + git log --format=%n --name-only | sort | uniq | grep -v ^$ > f && + ! grep file.doc f && + COMPARE=$(wc -l filtered_f && + test_line_count = $COMPARE filtered_f + ) +' + +test_expect_success '--message-callback' ' + setup message-callback && + ( + cd message-callback && + git filter-repo --message-callback "return \"TLDR: \"+message[0:5]" && + git log --format=%s >log-messages && + grep TLDR:...... log-messages >modified-messages && + test_line_count = 6 modified-messages + ) +' + +test_expect_success '--name-callback' ' + setup name-callback && + ( + cd name-callback && + git filter-repo --name-callback "return name.replace(\"N.\", \"And\")" && + git log --format=%an >log-person-names && + grep Copy.And.Paste log-person-names + ) +' + +test_expect_success '--email-callback' ' + setup email-callback && + ( + cd email-callback && + git filter-repo --email-callback "return email.replace(\".com\", \".org\")" && + git log --format=%ae%n%ce >log-emails && + ! grep .com log-emails && + grep .org log-emails + ) +' + +test_expect_success '--refname-callback' ' + setup refname-callback && + ( + cd refname-callback && + git filter-repo --refname-callback " + dir,path = os.path.split(refname) + return dir+\"/prefix-\"+path" && + git show-ref | grep refs/heads/prefix-master && + git show-ref | grep refs/tags/prefix-v1.0 && + git show-ref | grep refs/tags/prefix-v2.0 + ) +' + +test_expect_success '--refname-callback sanity check' ' + setup refname-sanity-check && + ( + cd refname-sanity-check && + + test_must_fail git filter-repo --refname-callback "return re.sub(\"tags\", \"other-tags\", refname)" 2>../err && + test_i18ngrep "fast-import requires tags to be in refs/tags/ namespace" ../err && + rm ../err + ) +' + +test_expect_success '--blob-callback' ' + setup blob-callback && + ( + cd blob-callback && + git log --format=%n --name-only | sort | uniq | grep -v ^$ > f && + test_line_count = 5 f && + rm f && + git filter-repo --blob-callback "if len(blob.data) > 25: blob.skip()" && + git log --format=%n --name-only | sort | uniq | grep -v ^$ > f && + test_line_count = 2 f + ) +' + +test_expect_success '--commit-callback' ' + setup commit-callback && + ( + cd commit-callback && + git filter-repo --commit-callback " + commit.committer_name = commit.author_name + commit.committer_email = commit.author_email + commit.committer_date = commit.author_date + for change in commit.file_changes: + change.mode = \"100755\" + " && + git log --format=%ae%n%ce >log-emails && + ! grep committer@example.com log-emails && + git log --raw | grep ^: >file-changes && + ! grep 100644 file-changes && + grep 100755 file-changes + ) +' + +test_expect_success '--tag-callback' ' + setup tag-callback && + ( + cd tag-callback && + git filter-repo --tag-callback " + tag.tagger_name = \"Dr. \"+tag.tagger_name + tag.message = \"Awesome sauce \"+tag.message + " && + git cat-file -p v2.0 | grep ^tagger.Dr\\. && + git cat-file -p v2.0 | grep ^Awesome.sauce.Super + ) +' + +test_expect_success '--reset-callback' ' + setup reset-callback && + ( + cd reset-callback && + git filter-repo --reset-callback "reset.from_ref = 3" && + test $(git rev-parse testing) = $(git rev-parse master~3) + ) +' + +test_done