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 <newren@gmail.com>
pull/13/head
Elijah Newren 5 years ago
parent 43cf7a09c7
commit 2431904f26

@ -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)

@ -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 <f) &&
grep src/ f >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
Loading…
Cancel
Save