Merge branch 'ks/ref-filter-mailmap'

"git for-each-ref" and friends learn to apply mailmap to authorname
and other fields.

* ks/ref-filter-mailmap:
  ref-filter: add mailmap support
  t/t6300: introduce test_bad_atom
  t/t6300: cleanup test_atom
This commit is contained in:
Junio C Hamano 2023-10-04 13:28:52 -07:00
commit 42b495e9c5
3 changed files with 240 additions and 43 deletions

View file

@ -303,7 +303,11 @@ Fields that have name-email-date tuple as its value (`author`,
and `date` to extract the named component. For email fields (`authoremail`,
`committeremail` and `taggeremail`), `:trim` can be appended to get the email
without angle brackets, and `:localpart` to get the part before the `@` symbol
out of the trimmed email.
out of the trimmed email. In addition to these, the `:mailmap` option and the
corresponding `:mailmap,trim` and `:mailmap,localpart` can be used (order does
not matter) to get values of the name and email according to the .mailmap file
or according to the file set in the mailmap.file or mailmap.blob configuration
variable (see linkgit:gitmailmap[5]).
The raw data in an object is `raw`.

View file

@ -13,6 +13,8 @@
#include "oid-array.h"
#include "repository.h"
#include "commit.h"
#include "mailmap.h"
#include "ident.h"
#include "remote.h"
#include "color.h"
#include "tag.h"
@ -215,8 +217,16 @@ static struct used_atom {
struct {
enum { O_SIZE, O_SIZE_DISK } option;
} objectsize;
struct email_option {
enum { EO_RAW, EO_TRIM, EO_LOCALPART } option;
struct {
enum { N_RAW, N_MAILMAP } option;
} name_option;
struct {
enum {
EO_RAW = 0,
EO_TRIM = 1<<0,
EO_LOCALPART = 1<<1,
EO_MAILMAP = 1<<2,
} option;
} email_option;
struct {
enum { S_BARE, S_GRADE, S_SIGNER, S_KEY,
@ -720,18 +730,52 @@ static int oid_atom_parser(struct ref_format *format UNUSED,
return 0;
}
static int person_name_atom_parser(struct ref_format *format UNUSED,
struct used_atom *atom,
const char *arg, struct strbuf *err)
{
if (!arg)
atom->u.name_option.option = N_RAW;
else if (!strcmp(arg, "mailmap"))
atom->u.name_option.option = N_MAILMAP;
else
return err_bad_arg(err, atom->name, arg);
return 0;
}
static int email_atom_option_parser(struct used_atom *atom,
const char **arg, struct strbuf *err)
{
if (!*arg)
return EO_RAW;
if (skip_prefix(*arg, "trim", arg))
return EO_TRIM;
if (skip_prefix(*arg, "localpart", arg))
return EO_LOCALPART;
if (skip_prefix(*arg, "mailmap", arg))
return EO_MAILMAP;
return -1;
}
static int person_email_atom_parser(struct ref_format *format UNUSED,
struct used_atom *atom,
const char *arg, struct strbuf *err)
{
if (!arg)
atom->u.email_option.option = EO_RAW;
else if (!strcmp(arg, "trim"))
atom->u.email_option.option = EO_TRIM;
else if (!strcmp(arg, "localpart"))
atom->u.email_option.option = EO_LOCALPART;
else
return err_bad_arg(err, atom->name, arg);
for (;;) {
int opt = email_atom_option_parser(atom, &arg, err);
const char *bad_arg = arg;
if (opt < 0)
return err_bad_arg(err, atom->name, bad_arg);
atom->u.email_option.option |= opt;
if (!arg || !*arg)
break;
if (*arg == ',')
arg++;
else
return err_bad_arg(err, atom->name, bad_arg);
}
return 0;
}
@ -877,15 +921,15 @@ static struct {
[ATOM_TYPE] = { "type", SOURCE_OBJ },
[ATOM_TAG] = { "tag", SOURCE_OBJ },
[ATOM_AUTHOR] = { "author", SOURCE_OBJ },
[ATOM_AUTHORNAME] = { "authorname", SOURCE_OBJ },
[ATOM_AUTHORNAME] = { "authorname", SOURCE_OBJ, FIELD_STR, person_name_atom_parser },
[ATOM_AUTHOREMAIL] = { "authoremail", SOURCE_OBJ, FIELD_STR, person_email_atom_parser },
[ATOM_AUTHORDATE] = { "authordate", SOURCE_OBJ, FIELD_TIME },
[ATOM_COMMITTER] = { "committer", SOURCE_OBJ },
[ATOM_COMMITTERNAME] = { "committername", SOURCE_OBJ },
[ATOM_COMMITTERNAME] = { "committername", SOURCE_OBJ, FIELD_STR, person_name_atom_parser },
[ATOM_COMMITTEREMAIL] = { "committeremail", SOURCE_OBJ, FIELD_STR, person_email_atom_parser },
[ATOM_COMMITTERDATE] = { "committerdate", SOURCE_OBJ, FIELD_TIME },
[ATOM_TAGGER] = { "tagger", SOURCE_OBJ },
[ATOM_TAGGERNAME] = { "taggername", SOURCE_OBJ },
[ATOM_TAGGERNAME] = { "taggername", SOURCE_OBJ, FIELD_STR, person_name_atom_parser },
[ATOM_TAGGEREMAIL] = { "taggeremail", SOURCE_OBJ, FIELD_STR, person_email_atom_parser },
[ATOM_TAGGERDATE] = { "taggerdate", SOURCE_OBJ, FIELD_TIME },
[ATOM_CREATOR] = { "creator", SOURCE_OBJ },
@ -1486,32 +1530,49 @@ static const char *copy_name(const char *buf)
return xstrdup("");
}
static const char *find_end_of_email(const char *email, int opt)
{
const char *eoemail;
if (opt & EO_LOCALPART) {
eoemail = strchr(email, '@');
if (eoemail)
return eoemail;
return strchr(email, '>');
}
if (opt & EO_TRIM)
return strchr(email, '>');
/*
* The option here is either the raw email option or the raw
* mailmap option (that is EO_RAW or EO_MAILMAP). In such cases,
* we directly grab the whole email including the closing
* angle brackets.
*
* If EO_MAILMAP was set with any other option (that is either
* EO_TRIM or EO_LOCALPART), we already grab the end of email
* above.
*/
eoemail = strchr(email, '>');
if (eoemail)
eoemail++;
return eoemail;
}
static const char *copy_email(const char *buf, struct used_atom *atom)
{
const char *email = strchr(buf, '<');
const char *eoemail;
int opt = atom->u.email_option.option;
if (!email)
return xstrdup("");
switch (atom->u.email_option.option) {
case EO_RAW:
eoemail = strchr(email, '>');
if (eoemail)
eoemail++;
break;
case EO_TRIM:
email++;
eoemail = strchr(email, '>');
break;
case EO_LOCALPART:
email++;
eoemail = strchr(email, '@');
if (!eoemail)
eoemail = strchr(email, '>');
break;
default:
BUG("unknown email option");
}
if (opt & (EO_LOCALPART | EO_TRIM))
email++;
eoemail = find_end_of_email(email, opt);
if (!eoemail)
return xstrdup("");
return xmemdupz(email, eoemail - email);
@ -1572,16 +1633,23 @@ static void grab_date(const char *buf, struct atom_value *v, const char *atomnam
v->value = 0;
}
static struct string_list mailmap = STRING_LIST_INIT_NODUP;
/* See grab_values */
static void grab_person(const char *who, struct atom_value *val, int deref, void *buf)
{
int i;
int wholen = strlen(who);
const char *wholine = NULL;
const char *headers[] = { "author ", "committer ",
"tagger ", NULL };
for (i = 0; i < used_atom_cnt; i++) {
const char *name = used_atom[i].name;
struct used_atom *atom = &used_atom[i];
const char *name = atom->name;
struct atom_value *v = &val[i];
struct strbuf mailmap_buf = STRBUF_INIT;
if (!!deref != (*name == '*'))
continue;
if (deref)
@ -1589,22 +1657,36 @@ static void grab_person(const char *who, struct atom_value *val, int deref, void
if (strncmp(who, name, wholen))
continue;
if (name[wholen] != 0 &&
strcmp(name + wholen, "name") &&
!starts_with(name + wholen, "name") &&
!starts_with(name + wholen, "email") &&
!starts_with(name + wholen, "date"))
continue;
if (!wholine)
if ((starts_with(name + wholen, "name") &&
(atom->u.name_option.option == N_MAILMAP)) ||
(starts_with(name + wholen, "email") &&
(atom->u.email_option.option & EO_MAILMAP))) {
if (!mailmap.items)
read_mailmap(&mailmap);
strbuf_addstr(&mailmap_buf, buf);
apply_mailmap_to_header(&mailmap_buf, headers, &mailmap);
wholine = find_wholine(who, wholen, mailmap_buf.buf);
} else {
wholine = find_wholine(who, wholen, buf);
}
if (!wholine)
return; /* no point looking for it */
if (name[wholen] == 0)
v->s = copy_line(wholine);
else if (!strcmp(name + wholen, "name"))
else if (starts_with(name + wholen, "name"))
v->s = copy_name(wholine);
else if (starts_with(name + wholen, "email"))
v->s = copy_email(wholine, &used_atom[i]);
else if (starts_with(name + wholen, "date"))
grab_date(wholine, v, name);
strbuf_release(&mailmap_buf);
}
/*

View file

@ -25,6 +25,13 @@ test_expect_success setup '
disklen sha1:138
disklen sha256:154
EOF
# setup .mailmap
cat >.mailmap <<-EOF &&
A Thor <athor@example.com> A U Thor <author@example.com>
C Mitter <cmitter@example.com> C O Mitter <committer@example.com>
EOF
setdate_and_increment &&
echo "Using $datestamp" > one &&
git add one &&
@ -41,25 +48,29 @@ test_expect_success setup '
git config push.default current
'
test_atom() {
test_atom () {
case "$1" in
head) ref=refs/heads/main ;;
tag) ref=refs/tags/testtag ;;
sym) ref=refs/heads/sym ;;
*) ref=$1 ;;
esac
format=$2
test_do=test_expect_${4:-success}
printf '%s\n' "$3" >expected
test_expect_${4:-success} $PREREQ "basic atom: $1 $2" "
git for-each-ref --format='%($2)' $ref >actual &&
$test_do $PREREQ "basic atom: $ref $format" '
git for-each-ref --format="%($format)" "$ref" >actual &&
sanitize_pgp <actual >actual.clean &&
test_cmp expected actual.clean
"
'
# Automatically test "contents:size" atom after testing "contents"
if test "$2" = "contents"
if test "$format" = "contents"
then
# for commit leg, $3 is changed there
expect=$(printf '%s' "$3" | wc -c)
test_expect_${4:-success} $PREREQ "basic atom: $1 contents:size" '
$test_do $PREREQ "basic atom: $ref contents:size" '
type=$(git cat-file -t "$ref") &&
case $type in
tag)
@ -141,15 +152,31 @@ test_atom head '*objectname' ''
test_atom head '*objecttype' ''
test_atom head author 'A U Thor <author@example.com> 1151968724 +0200'
test_atom head authorname 'A U Thor'
test_atom head authorname:mailmap 'A Thor'
test_atom head authoremail '<author@example.com>'
test_atom head authoremail:trim 'author@example.com'
test_atom head authoremail:localpart 'author'
test_atom head authoremail:trim,localpart 'author'
test_atom head authoremail:mailmap '<athor@example.com>'
test_atom head authoremail:mailmap,trim 'athor@example.com'
test_atom head authoremail:trim,mailmap 'athor@example.com'
test_atom head authoremail:mailmap,localpart 'athor'
test_atom head authoremail:localpart,mailmap 'athor'
test_atom head authoremail:mailmap,trim,localpart,mailmap,trim 'athor'
test_atom head authordate 'Tue Jul 4 01:18:44 2006 +0200'
test_atom head committer 'C O Mitter <committer@example.com> 1151968723 +0200'
test_atom head committername 'C O Mitter'
test_atom head committername:mailmap 'C Mitter'
test_atom head committeremail '<committer@example.com>'
test_atom head committeremail:trim 'committer@example.com'
test_atom head committeremail:localpart 'committer'
test_atom head committeremail:localpart,trim 'committer'
test_atom head committeremail:mailmap '<cmitter@example.com>'
test_atom head committeremail:mailmap,trim 'cmitter@example.com'
test_atom head committeremail:trim,mailmap 'cmitter@example.com'
test_atom head committeremail:mailmap,localpart 'cmitter'
test_atom head committeremail:localpart,mailmap 'cmitter'
test_atom head committeremail:trim,mailmap,trim,trim,localpart 'cmitter'
test_atom head committerdate 'Tue Jul 4 01:18:43 2006 +0200'
test_atom head tag ''
test_atom head tagger ''
@ -199,22 +226,46 @@ test_atom tag '*objectname' $(git rev-parse refs/tags/testtag^{})
test_atom tag '*objecttype' 'commit'
test_atom tag author ''
test_atom tag authorname ''
test_atom tag authorname:mailmap ''
test_atom tag authoremail ''
test_atom tag authoremail:trim ''
test_atom tag authoremail:localpart ''
test_atom tag authoremail:trim,localpart ''
test_atom tag authoremail:mailmap ''
test_atom tag authoremail:mailmap,trim ''
test_atom tag authoremail:trim,mailmap ''
test_atom tag authoremail:mailmap,localpart ''
test_atom tag authoremail:localpart,mailmap ''
test_atom tag authoremail:mailmap,trim,localpart,mailmap,trim ''
test_atom tag authordate ''
test_atom tag committer ''
test_atom tag committername ''
test_atom tag committername:mailmap ''
test_atom tag committeremail ''
test_atom tag committeremail:trim ''
test_atom tag committeremail:localpart ''
test_atom tag committeremail:localpart,trim ''
test_atom tag committeremail:mailmap ''
test_atom tag committeremail:mailmap,trim ''
test_atom tag committeremail:trim,mailmap ''
test_atom tag committeremail:mailmap,localpart ''
test_atom tag committeremail:localpart,mailmap ''
test_atom tag committeremail:trim,mailmap,trim,trim,localpart ''
test_atom tag committerdate ''
test_atom tag tag 'testtag'
test_atom tag tagger 'C O Mitter <committer@example.com> 1151968725 +0200'
test_atom tag taggername 'C O Mitter'
test_atom tag taggername:mailmap 'C Mitter'
test_atom tag taggeremail '<committer@example.com>'
test_atom tag taggeremail:trim 'committer@example.com'
test_atom tag taggeremail:localpart 'committer'
test_atom tag taggeremail:trim,localpart 'committer'
test_atom tag taggeremail:mailmap '<cmitter@example.com>'
test_atom tag taggeremail:mailmap,trim 'cmitter@example.com'
test_atom tag taggeremail:trim,mailmap 'cmitter@example.com'
test_atom tag taggeremail:mailmap,localpart 'cmitter'
test_atom tag taggeremail:localpart,mailmap 'cmitter'
test_atom tag taggeremail:trim,mailmap,trim,localpart,localpart 'cmitter'
test_atom tag taggerdate 'Tue Jul 4 01:18:45 2006 +0200'
test_atom tag creator 'C O Mitter <committer@example.com> 1151968725 +0200'
test_atom tag creatordate 'Tue Jul 4 01:18:45 2006 +0200'
@ -267,6 +318,66 @@ test_expect_success 'arguments to %(objectname:short=) must be positive integers
test_must_fail git for-each-ref --format="%(objectname:short=foo)"
'
test_bad_atom () {
case "$1" in
head) ref=refs/heads/main ;;
tag) ref=refs/tags/testtag ;;
sym) ref=refs/heads/sym ;;
*) ref=$1 ;;
esac
format=$2
test_do=test_expect_${4:-success}
printf '%s\n' "$3" >expect
$test_do $PREREQ "err basic atom: $ref $format" '
test_must_fail git for-each-ref \
--format="%($format)" "$ref" 2>error &&
test_cmp expect error
'
}
test_bad_atom head 'authoremail:foo' \
'fatal: unrecognized %(authoremail) argument: foo'
test_bad_atom head 'authoremail:mailmap,trim,bar' \
'fatal: unrecognized %(authoremail) argument: bar'
test_bad_atom head 'authoremail:trim,' \
'fatal: unrecognized %(authoremail) argument: '
test_bad_atom head 'authoremail:mailmaptrim' \
'fatal: unrecognized %(authoremail) argument: trim'
test_bad_atom head 'committeremail: ' \
'fatal: unrecognized %(committeremail) argument: '
test_bad_atom head 'committeremail: trim,foo' \
'fatal: unrecognized %(committeremail) argument: trim,foo'
test_bad_atom head 'committeremail:mailmap,localpart ' \
'fatal: unrecognized %(committeremail) argument: '
test_bad_atom head 'committeremail:trim_localpart' \
'fatal: unrecognized %(committeremail) argument: _localpart'
test_bad_atom head 'committeremail:localpart,,,trim' \
'fatal: unrecognized %(committeremail) argument: ,,trim'
test_bad_atom tag 'taggeremail:mailmap,trim, foo ' \
'fatal: unrecognized %(taggeremail) argument: foo '
test_bad_atom tag 'taggeremail:trim,localpart,' \
'fatal: unrecognized %(taggeremail) argument: '
test_bad_atom tag 'taggeremail:mailmap;localpart trim' \
'fatal: unrecognized %(taggeremail) argument: ;localpart trim'
test_bad_atom tag 'taggeremail:localpart trim' \
'fatal: unrecognized %(taggeremail) argument: trim'
test_bad_atom tag 'taggeremail:mailmap,mailmap,trim,qux,localpart,trim' \
'fatal: unrecognized %(taggeremail) argument: qux,localpart,trim'
test_date () {
f=$1 &&
committer_date=$2 &&