From b9e94c05c3c087079fa6631e37c0e50ccc05988b Mon Sep 17 00:00:00 2001 From: Bryce Kwon Date: Tue, 15 Sep 2026 19:41:30 -1000 Subject: Apply the mailmap at HEAD to every ident --- MANUAL.txt | 13 ++++++++ custom/cgitrc | 9 ++++++ source/cgit.c | 6 ++++ source/cgit.h | 2 ++ source/parsing.c | 41 +++++++++++++++++++----- source/parsing.h | 1 + source/shared.c | 1 + source/ui-log.c | 2 ++ tests/t0208-mailmap.sh | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 153 insertions(+), 8 deletions(-) create mode 100755 tests/t0208-mailmap.sh diff --git a/MANUAL.txt b/MANUAL.txt index fc91125..aa581f0 100644 --- a/MANUAL.txt +++ b/MANUAL.txt @@ -235,6 +235,15 @@ enable-log-linecount:: and removed lines for each commit on the repository log page. Default value: "0". +enable-mailmap:: + Flag which, when set to "1", applies the repository's mailmap to every + author, committer and tagger shown, so a person who changed name or + address appears under one identity in the log, commit, tag, blame and + statistics pages, the atom feed and the author search. Only the + .mailmap blob at HEAD is read, as git does for a bare repository, and + the mailmap.file and mailmap.blob settings are ignored. Default value: + "1". See also: "repo.enable-mailmap". + enable-plain-email:: Flag which, when set to "0", hides the full author, committer and tagger email addresses wherever they would be shown beside the name. Default @@ -614,6 +623,10 @@ repo.enable-log-linecount:: A flag which can be used to override the global setting "enable-log-linecount". Default value: . +repo.enable-mailmap:: + A flag which can be used to override the global setting + "enable-mailmap". Default value: . + repo.enable-remote-branches:: A flag which can be used to override the global setting "enable-remote-branches". Default value: . diff --git a/custom/cgitrc b/custom/cgitrc index 7ba1e13..9684a0b 100644 --- a/custom/cgitrc +++ b/custom/cgitrc @@ -201,6 +201,11 @@ enable-log-filecount=0 # Default is 0. enable-log-linecount=0 +# Apply the .mailmap at HEAD to every author, committer and tagger shown, so a +# person who changed name or address appears under one identity. Values are 0 +# or 1. Default is 1. +enable-mailmap=1 + # Show ages in the log, refs and index columns as an elapsed time such as "3 # days" rather than a calendar date. Values are 0 or 1. Default is 1. enable-relative-dates=1 @@ -497,6 +502,10 @@ enable-plain-email=1 # global enable-log-linecount value. #repo.enable-log-linecount=0 +# Per-repo override of enable-mailmap. Values are 0 or 1. Default is the global +# enable-mailmap value. +#repo.enable-mailmap=1 + # Per-repo override of enable-remote-branches. Values are 0 or 1. Default is the # global enable-remote-branches value. #repo.enable-remote-branches=0 diff --git a/source/cgit.c b/source/cgit.c index ae91dd2..36f3b25 100644 --- a/source/cgit.c +++ b/source/cgit.c @@ -107,6 +107,7 @@ static void prepare_context(void) ctx.cfg.local_time = 0; ctx.cfg.date_mode = date_mode_from_type(DATE_SHORT); ctx.cfg.enable_header = 1; + ctx.cfg.enable_mailmap = 1; ctx.cfg.enable_plain_email = 1; ctx.cfg.enable_relative_dates = 1; ctx.cfg.enable_http_clone = 1; @@ -239,6 +240,7 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.enable-gitmodules-links=%d\n", repo->enable_gitmodules_links); fprintf(f, "repo.enable-log-filecount=%d\n", repo->enable_log_filecount); fprintf(f, "repo.enable-log-linecount=%d\n", repo->enable_log_linecount); + fprintf(f, "repo.enable-mailmap=%d\n", repo->enable_mailmap); if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) cgit_fprintf_filter(repo->about_filter, f, "repo.about-filter="); if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter) @@ -557,6 +559,8 @@ static void apply_config(const char *name, const char *value) ctx.cfg.enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) ctx.cfg.enable_log_linecount = atoi(value); + else if (!strcmp(name, "enable-mailmap")) + ctx.cfg.enable_mailmap = atoi(value); else if (!strcmp(name, "enable-relative-dates")) ctx.cfg.enable_relative_dates = atoi(value); else if (!strcmp(name, "enable-remote-branches")) @@ -1168,6 +1172,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu repo->enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) repo->enable_log_linecount = atoi(value); + else if (!strcmp(name, "enable-mailmap")) + repo->enable_mailmap = atoi(value); else if (!strcmp(name, "enable-remote-branches")) repo->enable_remote_branches = atoi(value); else if (!strcmp(name, "enable-subject-links")) diff --git a/source/cgit.h b/source/cgit.h index d976906..5a9e756 100644 --- a/source/cgit.h +++ b/source/cgit.h @@ -88,6 +88,7 @@ struct cgit_repo { int enable_gitmodules_links; int enable_log_filecount; int enable_log_linecount; + int enable_mailmap; int enable_remote_branches; int enable_subject_links; int enable_trailers; @@ -226,6 +227,7 @@ struct cgit_config { int enable_commit_graph; int enable_log_filecount; int enable_log_linecount; + int enable_mailmap; int enable_plain_email; int enable_relative_dates; int enable_remote_branches; diff --git a/source/parsing.c b/source/parsing.c index c992538..587f44d 100644 --- a/source/parsing.c +++ b/source/parsing.c @@ -13,6 +13,8 @@ #include "parsing.h" #include "shared.h" +#include + static char *substr(const char *start, const char *end) { size_t len; @@ -29,22 +31,45 @@ static char *substr(const char *start, const char *end) return buf; } +/* + * The repository's mailmap, read once per request and applied to every ident + * a page shows. Only the blob at HEAD is read, the way git reads a bare + * repository, so a repository cannot point the reader at a file on the server. + */ +static struct string_list mailmap = STRING_LIST_INIT_DUP; +static int mailmap_read; + +struct string_list *cgit_mailmap(void) +{ + if (!ctx.repo || !ctx.repo->enable_mailmap) + return NULL; + if (!mailmap_read) { + mailmap_read = 1; + read_mailmap_blob(the_repository, &mailmap, "HEAD:.mailmap"); + } + return mailmap.nr ? &mailmap : NULL; +} + static void parse_user(const char *line, char **name, char **email, timestamp_t *date, int *tz) { struct ident_split ident; struct strbuf address = STRBUF_INIT; - ptrdiff_t email_len; + struct string_list *map; + const char *name_start, *mail_start; + size_t name_len, mail_len; if (!split_ident_line(&ident, line, strchrnul(line, '\n') - line)) { - *name = substr(ident.name_begin, ident.name_end); + name_start = ident.name_begin; + name_len = ident.name_end - ident.name_begin; + mail_start = ident.mail_begin; + mail_len = ident.mail_end - ident.mail_begin; + map = cgit_mailmap(); + if (map) + map_user(map, &mail_start, &mail_len, &name_start, &name_len); + *name = xmemdupz(name_start, name_len); - // Assembled rather than formatted. The length is a pointer - // difference, and the precision of a %.*s conversion has to be - // an int, which cannot hold one on a 64 bit host. - email_len = ident.mail_end - ident.mail_begin; strbuf_addch(&address, '<'); - if (email_len > 0) - strbuf_add(&address, ident.mail_begin, email_len); + strbuf_add(&address, mail_start, mail_len); strbuf_addch(&address, '>'); *email = strbuf_detach(&address, NULL); diff --git a/source/parsing.h b/source/parsing.h index b1dc9f1..d213429 100644 --- a/source/parsing.h +++ b/source/parsing.h @@ -14,6 +14,7 @@ // storing them in ctx.qry. extern void cgit_parse_url(const char *url); +extern struct string_list *cgit_mailmap(void); extern struct commitinfo *cgit_parse_commit(struct commit *commit); extern struct taginfo *cgit_parse_tag(struct tag *tag); diff --git a/source/shared.c b/source/shared.c index 59712ce..6ac2353 100644 --- a/source/shared.c +++ b/source/shared.c @@ -213,6 +213,7 @@ struct cgit_repo *cgit_add_repo(const char *url) repo->enable_gitmodules_links = ctx.cfg.enable_gitmodules_links; repo->enable_log_filecount = ctx.cfg.enable_log_filecount; repo->enable_log_linecount = ctx.cfg.enable_log_linecount; + repo->enable_mailmap = ctx.cfg.enable_mailmap; repo->enable_remote_branches = ctx.cfg.enable_remote_branches; repo->enable_subject_links = ctx.cfg.enable_subject_links; repo->enable_trailers = ctx.cfg.enable_trailers; diff --git a/source/ui-log.c b/source/ui-log.c index 8fe58f9..316df75 100644 --- a/source/ui-log.c +++ b/source/ui-log.c @@ -492,6 +492,8 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern strvec_push(&rev_argv, path); repo_init_revisions(the_repository, &rev, NULL); + // The author and committer searches then match the mapped idents. + rev.mailmap = cgit_mailmap(); rev.abbrev = DEFAULT_ABBREV; rev.commit_format = CMIT_FMT_DEFAULT; rev.verbose_header = 1; diff --git a/tests/t0208-mailmap.sh b/tests/t0208-mailmap.sh new file mode 100755 index 0000000..ca08de0 --- /dev/null +++ b/tests/t0208-mailmap.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +# The mailmap. A repository whose commits and tag carry an old ident gets a +# .mailmap at HEAD renaming it, and the log, commit, tag, atom and search +# pages are checked for the new name, then the flag is turned off globally and +# for the one repository. + +test_description='Check the mailmap is applied to idents' +CGIT_TEST_NO_CREATE_REPOS=YesPlease +. ./setup.sh + +test_expect_success 'create a repository with a mailmap at its head' ' + test_create_repo repos/mapped && + ( + cd repos/mapped && + echo content >file && + git add file && + GIT_AUTHOR_NAME="Old Name" GIT_AUTHOR_EMAIL=old@example.com \ + git commit -m "first commit" && + GIT_COMMITTER_NAME="Old Name" GIT_COMMITTER_EMAIL=old@example.com \ + git tag -a -m "a tag" v1 && + echo "New Name " >.mailmap && + git add .mailmap && + git commit -m "add the mailmap" + ) +' + +test_expect_success 'point cgit at it' ' + cat >cgitrc <<-EOF + virtual-root=/ + cache-size=0 + repo.url=mapped + repo.path=$PWD/repos/mapped/.git + EOF +' + +test_expect_success 'the log shows the mapped author' ' + cgit_url "mapped/log/" >tmp && + grep ">first commit" tmp && + grep "New Name" tmp && + ! grep "Old Name" tmp +' + +test_expect_success 'the commit page shows the mapped author and address' ' + first=$(git -C repos/mapped rev-parse HEAD~1) && + cgit_url "mapped/commit/&id=$first" >tmp && + grep "authorNew Name <new@example.com>" tmp && + ! grep "old@example.com" tmp +' + +test_expect_success 'the tag page shows the mapped tagger' ' + cgit_url "mapped/tag/&h=v1" >tmp && + grep "New Name <new@example.com>" tmp && + ! grep "Old Name" tmp +' + +test_expect_success 'the atom feed uses the mapped name and address' ' + cgit_url "mapped/atom/" >tmp && + grep "New Name" tmp && + grep "new@example.com" tmp && + ! grep "Old Name" tmp +' + +test_expect_success 'the author search matches the mapped name' ' + cgit_query "url=mapped/log/&qt=author&q=New+Name" >tmp && + grep ">first commit" tmp && + cgit_query "url=mapped/log/&qt=author&q=Nobody" >tmp && + ! grep ">first commit" tmp +' + +test_expect_success 'with the flag off the old ident shows' ' + { + echo "enable-mailmap=0" && + cat cgitrc + } >cgitrc-off && + CGIT_CONFIG="$PWD/cgitrc-off" QUERY_STRING="url=mapped/log/" cgit >tmp && + grep "Old Name" tmp +' + +test_expect_success 'the repository flag overrides the global one' ' + echo "repo.enable-mailmap=0" >>cgitrc && + cgit_url "mapped/log/" >tmp && + grep "Old Name" tmp +' + +test_done -- cgit v2.8.0