From cd9e0e9a53fc921f53d22add7ed422da677c7d19 Mon Sep 17 00:00:00 2001 From: Bryce Kwon Date: Tue, 15 Sep 2026 18:20:18 -1000 Subject: Split trailers out of the commit message --- source/cgit.c | 16 ++++++- source/cgit.h | 4 ++ source/filter.c | 3 ++ source/filter.h | 2 +- source/shared.c | 2 + source/ui-commit.c | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 149 insertions(+), 6 deletions(-) (limited to 'source') diff --git a/source/cgit.c b/source/cgit.c index e6e8613..ae91dd2 100644 --- a/source/cgit.c +++ b/source/cgit.c @@ -247,6 +247,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo) cgit_fprintf_filter(repo->source_filter, f, "repo.source-filter="); if (repo->email_filter && repo->email_filter != ctx.cfg.email_filter) cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter="); + if (repo->trailer_filter && repo->trailer_filter != ctx.cfg.trailer_filter) + cgit_fprintf_filter(repo->trailer_filter, f, "repo.trailer-filter="); if (repo->snapshots != ctx.cfg.snapshots) { char *formats = build_snapshot_setting(repo->snapshots); fprintf(f, "repo.snapshots=%s\n", formats ? formats : ""); @@ -262,6 +264,7 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.logo-link=%s\n", repo->logo_link); fprintf(f, "repo.enable-remote-branches=%d\n", repo->enable_remote_branches); fprintf(f, "repo.enable-subject-links=%d\n", repo->enable_subject_links); + fprintf(f, "repo.enable-trailers=%d\n", repo->enable_trailers); fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving); if (repo->branch_sort == 1) fprintf(f, "repo.branch-sort=age\n"); @@ -560,6 +563,8 @@ static void apply_config(const char *name, const char *value) ctx.cfg.enable_remote_branches = atoi(value); else if (!strcmp(name, "enable-subject-links")) ctx.cfg.enable_subject_links = atoi(value); + else if (!strcmp(name, "enable-trailers")) + ctx.cfg.enable_trailers = atoi(value); else if (!strcmp(name, "enable-html-serving")) ctx.cfg.enable_html_serving = atoi(value); else if (!strcmp(name, "enable-tree-linenumbers")) @@ -598,6 +603,8 @@ static void apply_config(const char *name, const char *value) ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT); else if (!strcmp(name, "email-filter")) ctx.cfg.email_filter = cgit_new_filter(value, EMAIL); + else if (!strcmp(name, "trailer-filter")) + ctx.cfg.trailer_filter = cgit_new_filter(value, TRAILER); else if (!strcmp(name, "auth-filter")) ctx.cfg.auth_filter = cgit_new_filter(value, AUTH); else if (!strcmp(name, "embedded")) @@ -1165,6 +1172,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu repo->enable_remote_branches = atoi(value); else if (!strcmp(name, "enable-subject-links")) repo->enable_subject_links = atoi(value); + else if (!strcmp(name, "enable-trailers")) + repo->enable_trailers = atoi(value); else if (!strcmp(name, "enable-html-serving")) repo->enable_html_serving = atoi(value); else if (!strcmp(name, "branch-sort")) { @@ -1204,7 +1213,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu !strcmp(name, "about-filter") || !strcmp(name, "commit-filter") || !strcmp(name, "source-filter") || - !strcmp(name, "email-filter") + !strcmp(name, "email-filter") || + !strcmp(name, "trailer-filter") ) { if (!ctx.cfg.enable_filter_overrides) fprintf(stderr, "[cgit] Ignoring repo %s: enable-filter-overrides is not set\n", name); @@ -1214,8 +1224,10 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu repo->commit_filter = cgit_new_filter(value, COMMIT); else if (!strcmp(name, "source-filter")) repo->source_filter = cgit_new_filter(value, SOURCE); - else + else if (!strcmp(name, "email-filter")) repo->email_filter = cgit_new_filter(value, EMAIL); + else + repo->trailer_filter = cgit_new_filter(value, TRAILER); } else fprintf(stderr, "[cgit] Unknown repo config key: %s\n", name); } diff --git a/source/cgit.h b/source/cgit.h index cc4ac02..d976906 100644 --- a/source/cgit.h +++ b/source/cgit.h @@ -90,6 +90,7 @@ struct cgit_repo { int enable_log_linecount; int enable_remote_branches; int enable_subject_links; + int enable_trailers; int enable_html_serving; int max_stats; int branch_sort; @@ -99,6 +100,7 @@ struct cgit_repo { struct cgit_filter *commit_filter; struct cgit_filter *source_filter; struct cgit_filter *email_filter; + struct cgit_filter *trailer_filter; struct string_list submodules; int hide; int ignore; @@ -228,6 +230,7 @@ struct cgit_config { int enable_relative_dates; int enable_remote_branches; int enable_subject_links; + int enable_trailers; int enable_html_serving; int enable_tree_linenumbers; int enable_tree_group_dirs; @@ -263,6 +266,7 @@ struct cgit_config { struct cgit_filter *commit_filter; struct cgit_filter *source_filter; struct cgit_filter *email_filter; + struct cgit_filter *trailer_filter; struct cgit_filter *auth_filter; }; diff --git a/source/filter.c b/source/filter.c index 2731d58..81c4681 100644 --- a/source/filter.c +++ b/source/filter.c @@ -395,12 +395,14 @@ void cgit_cleanup_filters(void) cleanup_filter(ctx.cfg.commit_filter); cleanup_filter(ctx.cfg.source_filter); cleanup_filter(ctx.cfg.email_filter); + cleanup_filter(ctx.cfg.trailer_filter); cleanup_filter(ctx.cfg.auth_filter); for (i = 0; i < cgit_repolist.count; ++i) { cleanup_filter(cgit_repolist.repos[i].about_filter); cleanup_filter(cgit_repolist.repos[i].commit_filter); cleanup_filter(cgit_repolist.repos[i].source_filter); cleanup_filter(cgit_repolist.repos[i].email_filter); + cleanup_filter(cgit_repolist.repos[i].trailer_filter); } } @@ -438,6 +440,7 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) argument_count = 12; break; case EMAIL: + case TRAILER: argument_count = 2; break; case SOURCE: diff --git a/source/filter.h b/source/filter.h index 861578c..fa43416 100644 --- a/source/filter.h +++ b/source/filter.h @@ -13,7 +13,7 @@ #include typedef enum { - ABOUT, COMMIT, SOURCE, EMAIL, AUTH + ABOUT, COMMIT, SOURCE, EMAIL, TRAILER, AUTH } filter_type; struct cgit_filter { diff --git a/source/shared.c b/source/shared.c index beda48b..59712ce 100644 --- a/source/shared.c +++ b/source/shared.c @@ -215,6 +215,7 @@ struct cgit_repo *cgit_add_repo(const char *url) repo->enable_log_linecount = ctx.cfg.enable_log_linecount; 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; repo->enable_html_serving = ctx.cfg.enable_html_serving; repo->max_stats = ctx.cfg.max_stats; repo->branch_sort = ctx.cfg.branch_sort; @@ -226,6 +227,7 @@ struct cgit_repo *cgit_add_repo(const char *url) repo->commit_filter = ctx.cfg.commit_filter; repo->source_filter = ctx.cfg.source_filter; repo->email_filter = ctx.cfg.email_filter; + repo->trailer_filter = ctx.cfg.trailer_filter; repo->clone_url = ctx.cfg.clone_url; repo->submodules.strdup_strings = 1; repo->hide = repo->ignore = 0; diff --git a/source/ui-commit.c b/source/ui-commit.c index 6055509..b67f5db 100644 --- a/source/ui-commit.c +++ b/source/ui-commit.c @@ -18,6 +18,9 @@ #include "ui-log.h" #include "ui-shared.h" +#include +#include + // The diff below the message is always taken against the first parent alone, // which says little about a merge of many branches, so a commit with this // many parents or more is shown without a diff at all. @@ -76,6 +79,127 @@ static void print_filtered_text(const char *text) cgit_close_filter(ctx.repo->commit_filter); } +/* + * A value shaped like an ident is written the way the author row is, through + * the email filter. Anything else goes through the trailer filter when one is + * set, and otherwise through the commit filter, so the links a site already + * draws in message text keep appearing on trailer lines. + */ +static void print_trailer_value(const char *key, const char *value) +{ + struct ident_split ident; + struct strbuf name = STRBUF_INIT, email = STRBUF_INIT; + + if (!split_ident_line(&ident, value, strlen(value)) && + memchr(ident.mail_begin, '@', ident.mail_end - ident.mail_begin)) { + strbuf_add(&name, ident.name_begin, ident.name_end - ident.name_begin); + strbuf_addch(&email, '<'); + strbuf_add(&email, ident.mail_begin, ident.mail_end - ident.mail_begin); + strbuf_addch(&email, '>'); + cgit_open_filter(ctx.repo->email_filter, email.buf, "commit"); + html_txt(name.buf); + if (ctx.cfg.enable_plain_email) { + html(" "); + html_txt(email.buf); + } + cgit_close_filter(ctx.repo->email_filter); + strbuf_release(&name); + strbuf_release(&email); + } else if (ctx.repo->trailer_filter) { + cgit_open_filter(ctx.repo->trailer_filter, key, "commit"); + html_txt(value); + cgit_close_filter(ctx.repo->trailer_filter); + } else + print_filtered_text(value); +} + +/* + * One row per trailer, except that a run of trailers sharing a key becomes a + * single row listing every value. A line git kept in the block without being + * able to split it, such as a cherry-pick note, gets a row of its own. + */ +static void print_trailers(const char *msg) +{ + struct trailer_iterator iter; + struct strbuf key = STRBUF_INIT; + int open = 0; + + html("\n"); + trailer_iterator_init(&iter, msg); + while (trailer_iterator_advance(&iter)) { + if (!iter.val.len) { + if (open) + html("\n"); + open = 0; + html("\n"); + continue; + } + if (open && !strcasecmp(key.buf, iter.key.buf)) + html("
"); + else { + if (open) + html("\n"); + strbuf_reset(&key); + strbuf_addbuf(&key, &iter.key); + html("\n"); + html("
"); + print_filtered_text(iter.key.buf); + html("
"); + html_txt(key.buf); + html(""); + open = 1; + } + print_trailer_value(key.buf, iter.val.buf); + } + if (open) + html("
\n"); + trailer_iterator_release(&iter); + strbuf_release(&key); +} + +/* + * Git finds the trailer block by looking at the whole message, title included, + * so the subject goes back in front of the body before asking, and the offsets + * that come back are shifted by that prefix. The blank line that separated the + * body from its block is dropped along with the block. + */ +static void print_message(struct commitinfo *info) +{ + struct strbuf full = STRBUF_INIT; + struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT; + struct trailer_block *block; + LIST_HEAD(items); + size_t prefix, start, end; + char *body = NULL; + + if (ctx.repo->enable_trailers) { + strbuf_addf(&full, "%s\n\n%s", info->subject, info->msg); + prefix = full.len - strlen(info->msg); + opts.no_divider = 1; + block = parse_trailers(&opts, full.buf, &items); + start = trailer_block_start(block); + end = trailer_block_end(block); + free_trailers(&items); + trailer_block_release(block); + if (start >= prefix && start < end) { + size_t len = start - prefix; + + while (len > 0 && info->msg[len - 1] == '\n') + len--; + body = xmemdupz(info->msg, len); + } + } + + html("
"); + print_filtered_text(body ? body : info->msg); + html("
\n"); + if (body) + print_trailers(full.buf); + + free(body); + strbuf_release(&full); +} + void cgit_print_commit(char *hex, const char *prefix) { struct commit *commit; @@ -145,9 +269,7 @@ void cgit_print_commit(char *hex, const char *prefix) print_filtered_text(info->subject); cgit_print_commit_decorations(commit); html("\n"); - html("
"); - print_filtered_text(info->msg); - html("
\n"); + print_message(info); if (notes.len != 0) { html("
Notes
\n"); html("
"); -- cgit v2.8.0