/* * 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" // A tab in the rendered source runs on to the next multiple of this. The // stylesheet leaves tab-size alone, so the measurement has to match what the // browser does on its own rather than anything cgit picks. #define TAB_WIDTH 8 enum blame_target { TARGET_MISSING, TARGET_FILE, TARGET_FOLDER, }; struct walk_tree_context { 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; static void free_suspect_details(void) { struct string_list_item *item; for_each_string_list_item(item, &suspect_details) free(item->util); string_list_clear(&suspect_details, 0); } /* * 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; sb->path = path; 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); strbuf_addf(&detail, "author %s", info->author); if (!ctx.cfg.noplainemail) 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); if (!ctx.cfg.noplainemail) 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_hashes(struct blame_scoreboard *sb) { struct blame_entry *ent; html(""); for (ent = sb->ent; ent; ent = ent->next) { html("
");
		emit_entry_hash(ent);
		html("
"); } html("\n"); } static void emit_entry_linenumbers(struct blame_entry *ent) { const char *numberfmt = "%1$d\n"; struct strbuf numbers = STRBUF_INIT; int lineno = ent->lno; while (lineno < ent->lno + ent->num_lines) { strbuf_addf(&numbers, numberfmt, ++lineno); if (numbers.len >= HTML_BATCH) { html_raw(numbers.buf, numbers.len); strbuf_reset(&numbers); } } html_raw(numbers.buf, numbers.len); strbuf_release(&numbers); } static void emit_linenumbers(struct blame_scoreboard *sb) { struct blame_entry *ent; html(""); for (ent = sb->ent; ent; ent = ent->next) { html("
");
		emit_entry_linenumbers(ent);
		html("
"); } html("\n"); } static size_t line_width(struct blame_scoreboard *sb, int line) { const char *pos = blame_nth_line(sb, line); const char *end = blame_nth_line(sb, line + 1); size_t width = 0; while (pos < end) { width++; if (*pos++ == '\t') width = (width + TAB_WIDTH - 1) & ~(TAB_WIDTH - 1); } return width; } /* * The stylesheet takes the source pre out of flow and positions it over these * blocks, so nothing else gives the cell a size and each block has to be * padded to the height and the width of the lines it stands behind. */ static void emit_entry_background(struct blame_scoreboard *sb, struct blame_entry *ent) { size_t widest = 2; int line; for (line = ent->lno; line < ent->lno + ent->num_lines; line++) { size_t width = line_width(sb, line); if (width > widest) widest = width; } emit_chars('\n', ent->num_lines); emit_chars(' ', widest - 1); } /* * Frees each entry on the way past, so this has to be the last pass over the * scoreboard's entries. */ static void emit_line_backgrounds(struct blame_scoreboard *sb) { struct blame_entry *ent = sb->ent; html("
"); while (ent) { struct blame_entry *next = ent->next; html("
");
		emit_entry_background(sb, ent);
		html("
"); free(ent); ent = next; } html("
"); } static void print_blame_page(const struct object_id *oid, const char *path, const char *filename, const char *rev) { enum object_type type; char *buf; unsigned long size; struct rev_info revs; struct blame_scoreboard sb; type = odb_read_object_info(the_repository->objects, oid, &size); if (type == OBJ_BAD) { cgit_print_error_page(404, "Not Found", "Bad object name: %s", oid_to_hex(oid)); return; } if (ctx.cfg.max_blob_size && size / 1024 > (unsigned long)ctx.cfg.max_blob_size) { cgit_print_error_page(413, "Content Too Large", "blob size (%luKB) exceeds display size limit (%dKB)", size / 1024, ctx.cfg.max_blob_size); return; } buf = odb_read_object(the_repository->objects, oid, &type, &size); if (!buf) { cgit_print_error_page(500, "Internal Server Error", "Error reading object %s", oid_to_hex(oid)); return; } run_blame(&sb, &revs, path, rev); cgit_set_title_from_path(path); cgit_print_layout_start(); htmlf("blob: %s (", oid_to_hex(oid)); cgit_plain_link("plain", NULL, NULL, ctx.qry.head, rev, path); html(") ("); cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path); html(")\n"); // A NUL past the window buffer_is_binary sniffs would end html_txt // early while the hash and line number columns still cover the whole // file, so a blob holding one anywhere is treated as binary too. if (buffer_is_binary(buf, size) || memchr(buf, 0, size)) { struct blame_entry *ent = sb.ent, *next; // The scoreboard is normally consumed by the column passes, // so this early exit has to release it itself. for (; ent; ent = next) { next = ent->next; free(ent); } free((void *)sb.final_buf); html("
blob is binary.
"); goto cleanup; } html("\n\n"); emit_hashes(&sb); if (ctx.cfg.enable_tree_linenumbers) emit_linenumbers(&sb); html("\n"); html("\n
"); emit_line_backgrounds(&sb); free((void *)sb.final_buf); html("
");
	if (ctx.repo->source_filter) {
		char *filter_arg = xstrdup(filename);
		cgit_open_filter(ctx.repo->source_filter, filter_arg);
		html_raw(buf, size);
		cgit_close_filter(ctx.repo->source_filter);
		free(filter_arg);
	} else {
		html_txt(buf);
	}
	html("
"); html("
\n"); cleanup: cgit_print_layout_end(); free_suspect_details(); free(buf); } static int walk_tree(const struct object_id *oid, struct strbuf *base, const char *pathname, unsigned mode, void *data) { struct walk_tree_context *walk = data; // match_baselen is -1 when no path was given, which no length equals. if (walk->match_baselen >= 0 && base->len == (size_t)walk->match_baselen) { if (S_ISREG(mode)) { struct strbuf fullpath = STRBUF_INIT; strbuf_addbuf(&fullpath, base); strbuf_addstr(&fullpath, pathname); print_blame_page(oid, fullpath.buf, pathname, walk->rev); strbuf_release(&fullpath); walk->found = TARGET_FILE; } else if (S_ISDIR(mode)) { walk->found = TARGET_FOLDER; } } else if (base->len < INT_MAX && (int)base->len > walk->match_baselen) { walk->found = TARGET_FOLDER; } else if (S_ISDIR(mode)) { return READ_TREE_RECURSIVE; } return 0; } static int basedir_len(const char *path) { const char *slash = strrchr(path, '/'); if (slash) return slash - path + 1; return 0; } void cgit_print_blame(void) { const char *rev = ctx.qry.oid; struct object_id oid; struct commit *commit; int path_len = ctx.qry.path ? strlen(ctx.qry.path) : 0; // nowildcard_len matches len so git treats the path as literal rather // than as a glob, which is what a request naming one file means. struct pathspec_item path_items = { .match = ctx.qry.path, .len = path_len, .nowildcard_len = path_len }; struct pathspec paths = { .nr = 1, .items = &path_items }; struct walk_tree_context walk = { .found = TARGET_MISSING }; if (!rev) rev = ctx.qry.head; if (repo_get_oid(the_repository, rev, &oid)) { cgit_print_error_page(404, "Not Found", "Invalid revision name: %s", rev); return; } commit = lookup_commit_reference(the_repository, &oid); if (!commit || repo_parse_commit(the_repository, commit)) { cgit_print_error_page(404, "Not Found", "Invalid commit reference: %s", rev); return; } walk.rev = xstrdup(rev); walk.match_baselen = path_items.match ? basedir_len(path_items.match) : -1; read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk); if (walk.found == TARGET_MISSING) cgit_print_error_page(404, "Not Found", "Not found"); else if (walk.found == TARGET_FOLDER) cgit_print_error_page(404, "Not Found", "Blame is not available for folders."); free(walk.rev); }