/* * The blame page, which shows a file next to the commit that last touched * each of its lines. git returns blame as runs of neighbouring lines that * share a commit, and the page turns every run into one block in each column * so the hashes, the line numbers and the striped background all line up with * the source. Only a regular file can be blamed, so a path naming a folder is * turned away. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "filter.h" #include "html.h" #include "parsing.h" #include "shared.h" #include "ui-blame.h" #include "ui-shared.h" enum blame_target { TARGET_MISSING, TARGET_FILE, TARGET_FOLDER, }; struct walk_tree_context { const char *rev; int match_baselen; enum blame_target found; }; // A commit that touched several separate parts of the file comes back once // per part, and each repeat would parse the commit again, so the rendered // detail is cached here and looked up by object id. static struct string_list suspect_details = STRING_LIST_INIT_DUP; /* * The scoreboard keeps a pointer to revs, so the caller owns both and has to * keep them alive together. */ static void run_blame(struct blame_scoreboard *sb, struct rev_info *revs, const char *path, const char *rev) { struct strvec argv = STRVEC_INIT; struct blame_origin *origin; strvec_push(&argv, "blame"); strvec_push(&argv, rev); repo_init_revisions(the_repository, revs, NULL); revs->diffopt.flags.allow_textconv = 1; setup_revisions(argv.nr, argv.v, revs, NULL); init_scoreboard(sb); sb->revs = revs; sb->repo = the_repository; sb->path = path; setup_scoreboard(sb, &origin); origin->suspects = blame_entry_prepend(NULL, 0, sb->num_lines, origin); prio_queue_put(&sb->commits, origin->commit); blame_origin_decref(origin); sb->ent = NULL; assign_blame(sb, 0); blame_sort_final(sb); blame_coalesce(sb); } /* * A single blame entry can cover the whole file, so the run is handed on in * batches rather than built whole. */ static void emit_chars(char ch, unsigned long count) { struct strbuf run = STRBUF_INIT; while (count) { unsigned long n = count < HTML_BATCH ? count : HTML_BATCH; strbuf_addchars(&run, ch, n); html_raw(run.buf, run.len); strbuf_reset(&run); count -= n; } strbuf_release(&run); } /* * The returned string belongs to suspect_details, not the caller. */ static char *suspect_detail(struct blame_origin *suspect) { struct commitinfo *info; struct strbuf detail = STRBUF_INIT; struct string_list_item *cached; char key[GIT_MAX_HEXSZ + 1]; // A private copy, since oid_to_hex hands back a buffer it reuses and // the commit parse below is free to call it again. oid_to_hex_r(key, &suspect->commit->object.oid); cached = string_list_lookup(&suspect_details, key); if (cached) return cached->util; info = cgit_parse_commit(suspect->commit); // A commit object may lack either ident line, leaving the fields NULL. strbuf_addf(&detail, "author %s", info->author ? info->author : ""); if (ctx.cfg.enable_plain_email && info->author_email) strbuf_addf(&detail, " %s", info->author_email); strbuf_addf(&detail, " %s\n", show_date(info->author_date, info->author_tz, cgit_date_mode(DATE_ISO8601))); strbuf_addf(&detail, "committer %s", info->committer ? info->committer : ""); if (ctx.cfg.enable_plain_email && info->committer_email) strbuf_addf(&detail, " %s", info->committer_email); strbuf_addf(&detail, " %s\n\n", show_date(info->committer_date, info->committer_tz, cgit_date_mode(DATE_ISO8601))); strbuf_addstr(&detail, info->subject); cgit_free_commitinfo(info); cached = string_list_insert(&suspect_details, key); cached->util = strbuf_detach(&detail, NULL); return cached->util; } static void emit_entry_hash(struct blame_entry *ent) { struct blame_origin *suspect = ent->suspect; struct object_id *oid = &suspect->commit->object.oid; const char *detail = suspect_detail(suspect); html(""); cgit_commit_link( repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV), detail, NULL, ctx.qry.head, oid_to_hex(oid), suspect->path ); html(""); if (!repo_parse_commit(the_repository, suspect->commit) && suspect->commit->parents) { struct commit *parent = suspect->commit->parents->item; html(" "); cgit_blame_link( "^", "Blame the previous revision", NULL, ctx.qry.head, oid_to_hex(&parent->object.oid), suspect->path ); } // The stripes only line up across the columns if each column gives an // entry the same height, so pad out to the lines the entry covers. emit_chars('\n', ent->num_lines); } static void emit_entry_linenumbers(struct blame_entry *ent) { cgit_print_line_anchors((size_t)ent->lno + 1, (size_t)ent->lno + ent->num_lines); } static void emit_column(struct blame_scoreboard *sb, const char *class, void (*emit_entry)(struct blame_entry *ent)) { struct blame_entry *ent; htmlf("
");
emit_entry(ent);
html("");
emit_entry_background(sb, ent);
html("");
emit_line_backgrounds(&sb);
free((void *)sb.final_buf);
html(" ");
html(" | \n");
html("