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
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`, and `date` to extract the named component. For email fields (`authoremail`,
`committeremail` and `taggeremail`), `:trim` can be appended to get the email `committeremail` and `taggeremail`), `:trim` can be appended to get the email
without angle brackets, and `:localpart` to get the part before the `@` symbol 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`. The raw data in an object is `raw`.

View File

@ -13,6 +13,8 @@
#include "oid-array.h" #include "oid-array.h"
#include "repository.h" #include "repository.h"
#include "commit.h" #include "commit.h"
#include "mailmap.h"
#include "ident.h"
#include "remote.h" #include "remote.h"
#include "color.h" #include "color.h"
#include "tag.h" #include "tag.h"
@ -215,8 +217,16 @@ static struct used_atom {
struct { struct {
enum { O_SIZE, O_SIZE_DISK } option; enum { O_SIZE, O_SIZE_DISK } option;
} objectsize; } objectsize;
struct email_option { struct {
enum { EO_RAW, EO_TRIM, EO_LOCALPART } option; 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; } email_option;
struct { struct {
enum { S_BARE, S_GRADE, S_SIGNER, S_KEY, enum { S_BARE, S_GRADE, S_SIGNER, S_KEY,
@ -720,21 +730,55 @@ static int oid_atom_parser(struct ref_format *format UNUSED,
return 0; return 0;
} }
static int person_email_atom_parser(struct ref_format *format UNUSED, static int person_name_atom_parser(struct ref_format *format UNUSED,
struct used_atom *atom, struct used_atom *atom,
const char *arg, struct strbuf *err) const char *arg, struct strbuf *err)
{ {
if (!arg) if (!arg)
atom->u.email_option.option = EO_RAW; atom->u.name_option.option = N_RAW;
else if (!strcmp(arg, "trim")) else if (!strcmp(arg, "mailmap"))
atom->u.email_option.option = EO_TRIM; atom->u.name_option.option = N_MAILMAP;
else if (!strcmp(arg, "localpart"))
atom->u.email_option.option = EO_LOCALPART;
else else
return err_bad_arg(err, atom->name, arg); return err_bad_arg(err, atom->name, arg);
return 0; 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)
{
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;
}
static int refname_atom_parser(struct ref_format *format UNUSED, static int refname_atom_parser(struct ref_format *format UNUSED,
struct used_atom *atom, struct used_atom *atom,
const char *arg, struct strbuf *err) const char *arg, struct strbuf *err)
@ -877,15 +921,15 @@ static struct {
[ATOM_TYPE] = { "type", SOURCE_OBJ }, [ATOM_TYPE] = { "type", SOURCE_OBJ },
[ATOM_TAG] = { "tag", SOURCE_OBJ }, [ATOM_TAG] = { "tag", SOURCE_OBJ },
[ATOM_AUTHOR] = { "author", 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_AUTHOREMAIL] = { "authoremail", SOURCE_OBJ, FIELD_STR, person_email_atom_parser },
[ATOM_AUTHORDATE] = { "authordate", SOURCE_OBJ, FIELD_TIME }, [ATOM_AUTHORDATE] = { "authordate", SOURCE_OBJ, FIELD_TIME },
[ATOM_COMMITTER] = { "committer", SOURCE_OBJ }, [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_COMMITTEREMAIL] = { "committeremail", SOURCE_OBJ, FIELD_STR, person_email_atom_parser },
[ATOM_COMMITTERDATE] = { "committerdate", SOURCE_OBJ, FIELD_TIME }, [ATOM_COMMITTERDATE] = { "committerdate", SOURCE_OBJ, FIELD_TIME },
[ATOM_TAGGER] = { "tagger", SOURCE_OBJ }, [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_TAGGEREMAIL] = { "taggeremail", SOURCE_OBJ, FIELD_STR, person_email_atom_parser },
[ATOM_TAGGERDATE] = { "taggerdate", SOURCE_OBJ, FIELD_TIME }, [ATOM_TAGGERDATE] = { "taggerdate", SOURCE_OBJ, FIELD_TIME },
[ATOM_CREATOR] = { "creator", SOURCE_OBJ }, [ATOM_CREATOR] = { "creator", SOURCE_OBJ },
@ -1486,32 +1530,49 @@ static const char *copy_name(const char *buf)
return xstrdup(""); 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) static const char *copy_email(const char *buf, struct used_atom *atom)
{ {
const char *email = strchr(buf, '<'); const char *email = strchr(buf, '<');
const char *eoemail; const char *eoemail;
int opt = atom->u.email_option.option;
if (!email) if (!email)
return xstrdup(""); 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) if (!eoemail)
return xstrdup(""); return xstrdup("");
return xmemdupz(email, eoemail - email); 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; v->value = 0;
} }
static struct string_list mailmap = STRING_LIST_INIT_NODUP;
/* See grab_values */ /* See grab_values */
static void grab_person(const char *who, struct atom_value *val, int deref, void *buf) static void grab_person(const char *who, struct atom_value *val, int deref, void *buf)
{ {
int i; int i;
int wholen = strlen(who); int wholen = strlen(who);
const char *wholine = NULL; const char *wholine = NULL;
const char *headers[] = { "author ", "committer ",
"tagger ", NULL };
for (i = 0; i < used_atom_cnt; i++) { 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 atom_value *v = &val[i];
struct strbuf mailmap_buf = STRBUF_INIT;
if (!!deref != (*name == '*')) if (!!deref != (*name == '*'))
continue; continue;
if (deref) 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)) if (strncmp(who, name, wholen))
continue; continue;
if (name[wholen] != 0 && if (name[wholen] != 0 &&
strcmp(name + wholen, "name") && !starts_with(name + wholen, "name") &&
!starts_with(name + wholen, "email") && !starts_with(name + wholen, "email") &&
!starts_with(name + wholen, "date")) !starts_with(name + wholen, "date"))
continue; 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); wholine = find_wholine(who, wholen, buf);
}
if (!wholine) if (!wholine)
return; /* no point looking for it */ return; /* no point looking for it */
if (name[wholen] == 0) if (name[wholen] == 0)
v->s = copy_line(wholine); v->s = copy_line(wholine);
else if (!strcmp(name + wholen, "name")) else if (starts_with(name + wholen, "name"))
v->s = copy_name(wholine); v->s = copy_name(wholine);
else if (starts_with(name + wholen, "email")) else if (starts_with(name + wholen, "email"))
v->s = copy_email(wholine, &used_atom[i]); v->s = copy_email(wholine, &used_atom[i]);
else if (starts_with(name + wholen, "date")) else if (starts_with(name + wholen, "date"))
grab_date(wholine, v, name); grab_date(wholine, v, name);
strbuf_release(&mailmap_buf);
} }
/* /*

View File

@ -25,6 +25,13 @@ test_expect_success setup '
disklen sha1:138 disklen sha1:138
disklen sha256:154 disklen sha256:154
EOF 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 && setdate_and_increment &&
echo "Using $datestamp" > one && echo "Using $datestamp" > one &&
git add one && git add one &&
@ -41,25 +48,29 @@ test_expect_success setup '
git config push.default current git config push.default current
' '
test_atom() { test_atom () {
case "$1" in case "$1" in
head) ref=refs/heads/main ;; head) ref=refs/heads/main ;;
tag) ref=refs/tags/testtag ;; tag) ref=refs/tags/testtag ;;
sym) ref=refs/heads/sym ;; sym) ref=refs/heads/sym ;;
*) ref=$1 ;; *) ref=$1 ;;
esac esac
format=$2
test_do=test_expect_${4:-success}
printf '%s\n' "$3" >expected printf '%s\n' "$3" >expected
test_expect_${4:-success} $PREREQ "basic atom: $1 $2" " $test_do $PREREQ "basic atom: $ref $format" '
git for-each-ref --format='%($2)' $ref >actual && git for-each-ref --format="%($format)" "$ref" >actual &&
sanitize_pgp <actual >actual.clean && sanitize_pgp <actual >actual.clean &&
test_cmp expected actual.clean test_cmp expected actual.clean
" '
# Automatically test "contents:size" atom after testing "contents" # Automatically test "contents:size" atom after testing "contents"
if test "$2" = "contents" if test "$format" = "contents"
then then
# for commit leg, $3 is changed there # for commit leg, $3 is changed there
expect=$(printf '%s' "$3" | wc -c) 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") && type=$(git cat-file -t "$ref") &&
case $type in case $type in
tag) tag)
@ -141,15 +152,31 @@ test_atom head '*objectname' ''
test_atom head '*objecttype' '' test_atom head '*objecttype' ''
test_atom head author 'A U Thor <author@example.com> 1151968724 +0200' test_atom head author 'A U Thor <author@example.com> 1151968724 +0200'
test_atom head authorname 'A U Thor' 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 '<author@example.com>'
test_atom head authoremail:trim 'author@example.com' test_atom head authoremail:trim 'author@example.com'
test_atom head authoremail:localpart 'author' 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 authordate 'Tue Jul 4 01:18:44 2006 +0200'
test_atom head committer 'C O Mitter <committer@example.com> 1151968723 +0200' test_atom head committer 'C O Mitter <committer@example.com> 1151968723 +0200'
test_atom head committername 'C O Mitter' 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 '<committer@example.com>'
test_atom head committeremail:trim 'committer@example.com' test_atom head committeremail:trim 'committer@example.com'
test_atom head committeremail:localpart 'committer' 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 committerdate 'Tue Jul 4 01:18:43 2006 +0200'
test_atom head tag '' test_atom head tag ''
test_atom head tagger '' 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 '*objecttype' 'commit'
test_atom tag author '' test_atom tag author ''
test_atom tag authorname '' test_atom tag authorname ''
test_atom tag authorname:mailmap ''
test_atom tag authoremail '' test_atom tag authoremail ''
test_atom tag authoremail:trim '' test_atom tag authoremail:trim ''
test_atom tag authoremail:localpart '' 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 authordate ''
test_atom tag committer '' test_atom tag committer ''
test_atom tag committername '' test_atom tag committername ''
test_atom tag committername:mailmap ''
test_atom tag committeremail '' test_atom tag committeremail ''
test_atom tag committeremail:trim '' test_atom tag committeremail:trim ''
test_atom tag committeremail:localpart '' 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 committerdate ''
test_atom tag tag 'testtag' test_atom tag tag 'testtag'
test_atom tag tagger 'C O Mitter <committer@example.com> 1151968725 +0200' test_atom tag tagger 'C O Mitter <committer@example.com> 1151968725 +0200'
test_atom tag taggername 'C O Mitter' 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 '<committer@example.com>'
test_atom tag taggeremail:trim 'committer@example.com' test_atom tag taggeremail:trim 'committer@example.com'
test_atom tag taggeremail:localpart 'committer' 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 taggerdate 'Tue Jul 4 01:18:45 2006 +0200'
test_atom tag creator 'C O Mitter <committer@example.com> 1151968725 +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' 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_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 () { test_date () {
f=$1 && f=$1 &&
committer_date=$2 && committer_date=$2 &&