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 | } | ||