diff options
| author | Jeff Smith | 2017-10-01 23:39:08 -0500 |
|---|---|---|
| committer | John Keeping | 2017-10-03 19:19:34 +0100 |
| commit | c1cd290d1f83d3d1c2d081d734e8d213f12cc06b (patch) | |
| tree | ad5027076d139912140c45b6baec2d390389f7a1 /ui-blame.c | |
| parent | f6ffe40d1a2c985494e48dc2d36663ffde1e6044 (diff) | |
| download | cgit-c1cd290d1f83d3d1c2d081d734e8d213f12cc06b.tar.gz cgit-c1cd290d1f83d3d1c2d081d734e8d213f12cc06b.tar.bz2 cgit-c1cd290d1f83d3d1c2d081d734e8d213f12cc06b.zip | |
ui-blame: add blame UI
Implement a page which provides the blame view of a specified file.
This feature is controlled by a new config variable, "enable-blame",
which is disabled by default.
Signed-off-by: Jeff Smith <whydoubt@gmail.com>
Reviewed-by: John Keeping <john@keeping.me.uk>
Diffstat (limited to 'ui-blame.c')
| -rw-r--r-- | ui-blame.c | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/ui-blame.c b/ui-blame.c new file mode 100644 index 0000000..62cf431 --- /dev/null +++ b/ui-blame.c | |||
| @@ -0,0 +1,227 @@ | |||
| 1 | /* ui-blame.c: functions for blame output | ||
| 2 | * | ||
| 3 | * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> | ||
| 4 | * | ||
| 5 | * Licensed under GNU General Public License v2 | ||
| 6 | * (see COPYING for full license text) | ||
| 7 | */ | ||
| 8 | |||
| 9 | #include "cgit.h" | ||
| 10 | #include "ui-blame.h" | ||
| 11 | #include "html.h" | ||
| 12 | #include "ui-shared.h" | ||
| 13 | #include "argv-array.h" | ||
| 14 | #include "blame.h" | ||
| 15 | |||
| 16 | |||
| 17 | static char *emit_suspect_detail(struct blame_origin *suspect) | ||
| 18 | { | ||
| 19 | struct commitinfo *info; | ||
| 20 | struct strbuf detail = STRBUF_INIT; | ||
| 21 | |||
| 22 | info = cgit_parse_commit(suspect->commit); | ||
| 23 | |||
| 24 | strbuf_addf(&detail, "author %s", info->author); | ||
| 25 | if (!ctx.cfg.noplainemail) | ||
| 26 | strbuf_addf(&detail, " %s", info->author_email); | ||
| 27 | strbuf_addf(&detail, " %s\n", | ||
| 28 | show_date(info->author_date, info->author_tz, | ||
| 29 | cgit_date_mode(DATE_ISO8601))); | ||
| 30 | |||
| 31 | strbuf_addf(&detail, "committer %s", info->committer); | ||
| 32 | if (!ctx.cfg.noplainemail) | ||
| 33 | strbuf_addf(&detail, " %s", info->committer_email); | ||
| 34 | strbuf_addf(&detail, " %s\n\n", | ||
| 35 | show_date(info->committer_date, info->committer_tz, | ||
| 36 | cgit_date_mode(DATE_ISO8601))); | ||
| 37 | |||
| 38 | strbuf_addstr(&detail, info->subject); | ||
| 39 | |||
| 40 | cgit_free_commitinfo(info); | ||
| 41 | return strbuf_detach(&detail, NULL); | ||
| 42 | } | ||
| 43 | |||
| 44 | static void emit_blame_entry(struct blame_scoreboard *sb, | ||
| 45 | struct blame_entry *ent) | ||
| 46 | { | ||
| 47 | struct blame_origin *suspect = ent->suspect; | ||
| 48 | struct object_id *oid = &suspect->commit->object.oid; | ||
| 49 | const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n"; | ||
| 50 | const char *cp, *cpend; | ||
| 51 | |||
| 52 | char *detail = emit_suspect_detail(suspect); | ||
| 53 | |||
| 54 | html("<tr><td class='sha1 lines'>"); | ||
| 55 | cgit_commit_link(find_unique_abbrev(oid->hash, DEFAULT_ABBREV), detail, | ||
| 56 | NULL, ctx.qry.head, oid_to_hex(oid), suspect->path); | ||
| 57 | html("</td>\n"); | ||
| 58 | |||
| 59 | free(detail); | ||
| 60 | |||
| 61 | if (ctx.cfg.enable_tree_linenumbers) { | ||
| 62 | unsigned long lineno = ent->lno; | ||
| 63 | html("<td class='linenumbers'><pre>"); | ||
| 64 | while (lineno < ent->lno + ent->num_lines) | ||
| 65 | htmlf(numberfmt, ++lineno); | ||
| 66 | html("</pre></td>\n"); | ||
| 67 | } | ||
| 68 | |||
| 69 | cp = blame_nth_line(sb, ent->lno); | ||
| 70 | cpend = blame_nth_line(sb, ent->lno + ent->num_lines); | ||
| 71 | |||
| 72 | html("<td class='lines'><pre><code>"); | ||
| 73 | html_ntxt(cp, cpend - cp); | ||
| 74 | html("</code></pre></td></tr>\n"); | ||
| 75 | } | ||
| 76 | |||
| 77 | struct walk_tree_context { | ||
| 78 | char *curr_rev; | ||
| 79 | int match_baselen; | ||
| 80 | int state; | ||
| 81 | }; | ||
| 82 | |||
| 83 | static void print_object(const unsigned char *sha1, const char *path, | ||
| 84 | const char *basename, const char *rev) | ||
| 85 | { | ||
| 86 | enum object_type type; | ||
| 87 | unsigned long size; | ||
| 88 | struct argv_array rev_argv = ARGV_ARRAY_INIT; | ||
| 89 | struct rev_info revs; | ||
| 90 | struct blame_scoreboard sb; | ||
| 91 | struct blame_origin *o; | ||
| 92 | struct blame_entry *ent = NULL; | ||
| 93 | |||
| 94 | type = sha1_object_info(sha1, &size); | ||
| 95 | if (type == OBJ_BAD) { | ||
| 96 | cgit_print_error_page(404, "Not found", "Bad object name: %s", | ||
| 97 | sha1_to_hex(sha1)); | ||
| 98 | return; | ||
| 99 | } | ||
| 100 | |||
| 101 | argv_array_push(&rev_argv, "blame"); | ||
| 102 | argv_array_push(&rev_argv, rev); | ||
| 103 | init_revisions(&revs, NULL); | ||
| 104 | DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV); | ||
| 105 | setup_revisions(rev_argv.argc, rev_argv.argv, &revs, NULL); | ||
| 106 | init_scoreboard(&sb); | ||
| 107 | sb.revs = &revs; | ||
| 108 | setup_scoreboard(&sb, path, &o); | ||
| 109 | o->suspects = blame_entry_prepend(NULL, 0, sb.num_lines, o); | ||
| 110 | prio_queue_put(&sb.commits, o->commit); | ||
| 111 | blame_origin_decref(o); | ||
| 112 | sb.ent = NULL; | ||
| 113 | sb.path = path; | ||
| 114 | assign_blame(&sb, 0); | ||
| 115 | blame_sort_final(&sb); | ||
| 116 | blame_coalesce(&sb); | ||
| 117 | |||
| 118 | cgit_set_title_from_path(path); | ||
| 119 | |||
| 120 | cgit_print_layout_start(); | ||
| 121 | htmlf("blob: %s (", sha1_to_hex(sha1)); | ||
| 122 | cgit_plain_link("plain", NULL, NULL, ctx.qry.head, rev, path); | ||
| 123 | html(") ("); | ||
| 124 | cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path); | ||
| 125 | html(")\n"); | ||
| 126 | |||
| 127 | if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) { | ||
| 128 | htmlf("<div class='error'>blob size (%ldKB)" | ||
| 129 | " exceeds display size limit (%dKB).</div>", | ||
| 130 | size / 1024, ctx.cfg.max_blob_size); | ||
| 131 | return; | ||
| 132 | } | ||
| 133 | |||
| 134 | html("<table class='blame blob'>"); | ||
| 135 | for (ent = sb.ent; ent; ) { | ||
| 136 | struct blame_entry *e = ent->next; | ||
| 137 | emit_blame_entry(&sb, ent); | ||
| 138 | free(ent); | ||
| 139 | ent = e; | ||
| 140 | } | ||
| 141 | html("</table>\n"); | ||
| 142 | free((void *)sb.final_buf); | ||
| 143 | |||
| 144 | cgit_print_layout_end(); | ||
| 145 | } | ||
| 146 | |||
| 147 | static int walk_tree(const unsigned char *sha1, struct strbuf *base, | ||
| 148 | const char *pathname, unsigned mode, int stage, | ||
| 149 | void *cbdata) | ||
| 150 | { | ||
| 151 | struct walk_tree_context *walk_tree_ctx = cbdata; | ||
| 152 | |||
| 153 | if (base->len == walk_tree_ctx->match_baselen) { | ||
| 154 | if (S_ISREG(mode)) { | ||
| 155 | struct strbuf buffer = STRBUF_INIT; | ||
| 156 | strbuf_addbuf(&buffer, base); | ||
| 157 | strbuf_addstr(&buffer, pathname); | ||
| 158 | print_object(sha1, buffer.buf, pathname, | ||
| 159 | walk_tree_ctx->curr_rev); | ||
| 160 | strbuf_release(&buffer); | ||
| 161 | walk_tree_ctx->state = 1; | ||
| 162 | } else if (S_ISDIR(mode)) { | ||
| 163 | walk_tree_ctx->state = 2; | ||
| 164 | } | ||
| 165 | } else if (base->len < INT_MAX | ||
| 166 | && (int)base->len > walk_tree_ctx->match_baselen) { | ||
| 167 | walk_tree_ctx->state = 2; | ||
| 168 | } else if (S_ISDIR(mode)) { | ||
| 169 | return READ_TREE_RECURSIVE; | ||
| 170 | } | ||
| 171 | return 0; | ||
| 172 | } | ||
| 173 | |||
| 174 | static int basedir_len(const char *path) | ||
| 175 | { | ||
| 176 | char *p = strrchr(path, '/'); | ||
| 177 | if (p) | ||
| 178 | return p - path + 1; | ||
| 179 | return 0; | ||
| 180 | } | ||
| 181 | |||
| 182 | void cgit_print_blame(void) | ||
| 183 | { | ||
| 184 | const char *rev = ctx.qry.sha1; | ||
| 185 | struct object_id oid; | ||
| 186 | struct commit *commit; | ||
| 187 | struct pathspec_item path_items = { | ||
| 188 | .match = ctx.qry.path, | ||
| 189 | .len = ctx.qry.path ? strlen(ctx.qry.path) : 0 | ||
| 190 | }; | ||
| 191 | struct pathspec paths = { | ||
| 192 | .nr = 1, | ||
| 193 | .items = &path_items | ||
| 194 | }; | ||
| 195 | struct walk_tree_context walk_tree_ctx = { | ||
| 196 | .state = 0 | ||
| 197 | }; | ||
| 198 | |||
| 199 | if (!rev) | ||
| 200 | rev = ctx.qry.head; | ||
| 201 | |||
| 202 | if (get_oid(rev, &oid)) { | ||
| 203 | cgit_print_error_page(404, "Not found", | ||
| 204 | "Invalid revision name: %s", rev); | ||
| 205 | return; | ||
| 206 | } | ||
| 207 | commit = lookup_commit_reference(&oid); | ||
| 208 | if (!commit || parse_commit(commit)) { | ||
| 209 | cgit_print_error_page(404, "Not found", | ||
| 210 | "Invalid commit reference: %s", rev); | ||
| 211 | return; | ||
| 212 | } | ||
| 213 | |||
| 214 | walk_tree_ctx.curr_rev = xstrdup(rev); | ||
| 215 | walk_tree_ctx.match_baselen = (path_items.match) ? | ||
| 216 | basedir_len(path_items.match) : -1; | ||
| 217 | |||
| 218 | read_tree_recursive(commit->tree, "", 0, 0, &paths, walk_tree, | ||
| 219 | &walk_tree_ctx); | ||
| 220 | if (!walk_tree_ctx.state) | ||
| 221 | cgit_print_error_page(404, "Not found", "Not found"); | ||
| 222 | else if (walk_tree_ctx.state == 2) | ||
| 223 | cgit_print_error_page(404, "No blame for folders", | ||
| 224 | "Blame is not available for folders."); | ||
| 225 | |||
| 226 | free(walk_tree_ctx.curr_rev); | ||
| 227 | } | ||
