/* * The commit page, which shows one commit on its own and is where the log and * the ref listings link. It renders the idents, the object ids, the message, * the trailer table when the repository asks for one and any note, then hands * off to ui-diff.c for the diff against the first parent. Free text and * addresses are written through the repository's commit, trailer and email * filters, so a site can rewrite any of them on the way out. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "filter.h" #include "html.h" #include "parsing.h" #include "shared.h" #include "ui-commit.h" #include "ui-diff.h" #include "ui-log.h" #include "ui-shared.h" #include #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. #define OCTOPUS_PARENTS 3 static void print_ident_row(const char *role, const char *name, const char *email, timestamp_t date, int tz) { htmlf("%s", role); cgit_open_filter(ctx.repo->email_filter, email ? email : "", "commit"); html_txt(name); if (ctx.cfg.enable_plain_email) { html(" "); html_txt(email); } cgit_close_filter(ctx.repo->email_filter); html("\n"); } /* * Counts the parents that could be read and leaves the first of them in first, * which the diff below the message is taken against. */ static int print_parent_rows(struct commit *commit, const char *rev, const char *prefix, struct object_id *first) { struct commit_list *p; struct commit *parent; struct commitinfo *info; const char *parent_hex, *label; int parents = 0; for (p = commit->parents; p; p = p->next) { parent = lookup_commit_reference(the_repository, &p->item->object.oid); if (!parent) { html(""); cgit_print_error("Error reading parent commit"); html("\n"); continue; } if (!parents) oidcpy(first, &p->item->object.oid); html("parent
"); parent_hex = label = oid_to_hex(&p->item->object.oid); info = ctx.repo->enable_subject_links ? cgit_parse_commit(parent) : NULL; if (info) label = info->subject; cgit_commit_link(label, NULL, NULL, ctx.qry.head, parent_hex, prefix); if (info) cgit_free_commitinfo(info); html(" ("); cgit_diff_link("diff", NULL, NULL, ctx.qry.head, rev, oid_to_hex(&p->item->object.oid), prefix); html(")
\n"); parents++; } return parents; } static void print_filtered_text(const char *text) { cgit_open_filter(ctx.repo->commit_filter); html_txt(text); cgit_close_filter(ctx.repo->commit_filter); } /* * A value shaped like an ident is written the way the author row is, through * the mailmap and the email filter, and anything after the address, such as a * role in parentheses, follows as text. 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 string_list *map; struct strbuf email = STRBUF_INIT; const char *name, *mail, *rest; size_t name_len, mail_len; if (!split_ident_line(&ident, value, strlen(value)) && memchr(ident.mail_begin, '@', ident.mail_end - ident.mail_begin)) { name = ident.name_begin; name_len = ident.name_end - ident.name_begin; mail = ident.mail_begin; mail_len = ident.mail_end - ident.mail_begin; map = cgit_mailmap(); if (map) map_user(map, &mail, &mail_len, &name, &name_len); strbuf_addch(&email, '<'); strbuf_add(&email, mail, mail_len); strbuf_addch(&email, '>'); cgit_open_filter(ctx.repo->email_filter, email.buf, "commit"); html_ntxt(name, name_len); if (ctx.cfg.enable_plain_email) { html(" "); html_txt(email.buf); } cgit_close_filter(ctx.repo->email_filter); strbuf_release(&email); rest = ident.mail_end + 1; while (isspace((unsigned char)*rest)) rest++; if (*rest) { html(" "); html_txt(rest); } } 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, compared * without regard to case, becomes a single row listing every value. A line git * kept in the block without a value, such as a cherry-pick note or a key with * nothing after its colon, gets a row of its own as written. */ static void print_trailers(const char *msg) { struct trailer_iterator iter; struct strbuf key = STRBUF_INIT, line = 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; strbuf_reset(&line); strbuf_addstr(&line, iter.raw); strbuf_rtrim(&line); 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(line.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); strbuf_release(&line); } /* * 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); // git leaves trailing comment lines and an old style Conflicts // section after the block, which the split would lose, so such // a message is shown whole. if (start >= prefix && start < end && !full.buf[end + strspn(full.buf + end, "\n")]) { 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; struct commitinfo *info; struct strbuf notes = STRBUF_INIT; struct object_id oid, first_oid; const char *commit_hex; char *first_parent; char *tree_rev; int parents; if (!hex) hex = ctx.qry.head; if (repo_get_oid(the_repository, hex, &oid)) { cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex); return; } commit = lookup_commit_reference(the_repository, &oid); if (!commit) { cgit_print_error_page(404, "Not Found", "Not a commit: %s", hex); return; } info = cgit_parse_commit(commit); format_display_notes(&commit->object.oid, ¬es, PAGE_ENCODING, 1); load_ref_decorations(NULL, DECORATE_FULL_REFS); ctx.page.title = cgit_fmtalloc("%s - %s", info->subject, ctx.page.title); cgit_print_layout_start(); cgit_print_diff_ctrls(); html("\n"); print_ident_row("author", info->author, info->author_email, info->author_date, info->author_tz); print_ident_row("committer", info->committer, info->committer_email, info->committer_date, info->committer_tz); // The spanning cells are kept out of the column widths, so a hash row // never pushes the dates away from the names above it. html("\n"); // The parent rows follow the commit row so the commit hashes sit // together, leaving the tree beside the download links. parents = print_parent_rows(commit, hex, prefix, &first_oid); html("\n"); if (ctx.repo->snapshots) { html("\n"); } html("
commit
"); commit_hex = oid_to_hex(&commit->object.oid); cgit_commit_link(commit_hex, NULL, NULL, ctx.qry.head, commit_hex, prefix); html(" ("); cgit_patch_link("patch", NULL, NULL, NULL, commit_hex, prefix); html(")
tree
"); tree_rev = xstrdup(hex); cgit_tree_link(oid_to_hex(get_commit_tree_oid(commit)), NULL, NULL, ctx.qry.head, tree_rev, NULL); if (prefix) { html(" /"); cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tree_rev, prefix); } free(tree_rev); html("
download
"); cgit_print_snapshot_links(ctx.repo, hex, "
"); html("
\n"); html("
"); print_filtered_text(info->subject); cgit_print_commit_decorations(commit); html("
\n"); print_message(info); if (notes.len != 0) { html("
Notes
\n"); html("
"); print_filtered_text(notes.buf); html("
\n"); html("\n"); } if (parents < OCTOPUS_PARENTS) { first_parent = parents ? xstrdup(oid_to_hex(&first_oid)) : NULL; cgit_print_diff(ctx.qry.oid, first_parent, prefix, 0, 0); free(first_parent); } strbuf_release(¬es); cgit_free_commitinfo(info); cgit_print_layout_end(); }