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 | |
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>
-rw-r--r-- | cgit.c | 2 | ||||
-rw-r--r-- | cgit.css | 8 | ||||
-rw-r--r-- | cgit.h | 1 | ||||
-rw-r--r-- | cgit.mk | 1 | ||||
-rw-r--r-- | cgitrc.5.txt | 9 | ||||
-rw-r--r-- | cmd.c | 12 | ||||
-rw-r--r-- | ui-blame.c | 227 | ||||
-rw-r--r-- | ui-blame.h | 6 |
8 files changed, 265 insertions, 1 deletions
@@ -167,6 +167,8 @@ static void config_cb(const char *name, const char *value) | |||
167 | ctx.cfg.enable_index_links = atoi(value); | 167 | ctx.cfg.enable_index_links = atoi(value); |
168 | else if (!strcmp(name, "enable-index-owner")) | 168 | else if (!strcmp(name, "enable-index-owner")) |
169 | ctx.cfg.enable_index_owner = atoi(value); | 169 | ctx.cfg.enable_index_owner = atoi(value); |
170 | else if (!strcmp(name, "enable-blame")) | ||
171 | ctx.cfg.enable_blame = atoi(value); | ||
170 | else if (!strcmp(name, "enable-commit-graph")) | 172 | else if (!strcmp(name, "enable-commit-graph")) |
171 | ctx.cfg.enable_commit_graph = atoi(value); | 173 | ctx.cfg.enable_commit_graph = atoi(value); |
172 | else if (!strcmp(name, "enable-log-filecount")) | 174 | else if (!strcmp(name, "enable-log-filecount")) |
@@ -329,6 +329,14 @@ div#cgit table.ssdiff td.lineno a:hover { | |||
329 | color: black; | 329 | color: black; |
330 | } | 330 | } |
331 | 331 | ||
332 | div#cgit table.blame tr:nth-child(even) { | ||
333 | background: #eee; | ||
334 | } | ||
335 | |||
336 | div#cgit table.blame tr:nth-child(odd) { | ||
337 | background: white; | ||
338 | } | ||
339 | |||
332 | div#cgit table.bin-blob { | 340 | div#cgit table.bin-blob { |
333 | margin-top: 0.5em; | 341 | margin-top: 0.5em; |
334 | border: solid 1px black; | 342 | border: solid 1px black; |
@@ -228,6 +228,7 @@ struct cgit_config { | |||
228 | int enable_http_clone; | 228 | int enable_http_clone; |
229 | int enable_index_links; | 229 | int enable_index_links; |
230 | int enable_index_owner; | 230 | int enable_index_owner; |
231 | int enable_blame; | ||
231 | int enable_commit_graph; | 232 | int enable_commit_graph; |
232 | int enable_log_filecount; | 233 | int enable_log_filecount; |
233 | int enable_log_linecount; | 234 | int enable_log_linecount; |
@@ -77,6 +77,7 @@ CGIT_OBJ_NAMES += parsing.o | |||
77 | CGIT_OBJ_NAMES += scan-tree.o | 77 | CGIT_OBJ_NAMES += scan-tree.o |
78 | CGIT_OBJ_NAMES += shared.o | 78 | CGIT_OBJ_NAMES += shared.o |
79 | CGIT_OBJ_NAMES += ui-atom.o | 79 | CGIT_OBJ_NAMES += ui-atom.o |
80 | CGIT_OBJ_NAMES += ui-blame.o | ||
80 | CGIT_OBJ_NAMES += ui-blob.o | 81 | CGIT_OBJ_NAMES += ui-blob.o |
81 | CGIT_OBJ_NAMES += ui-clone.o | 82 | CGIT_OBJ_NAMES += ui-clone.o |
82 | CGIT_OBJ_NAMES += ui-commit.o | 83 | CGIT_OBJ_NAMES += ui-commit.o |
diff --git a/cgitrc.5.txt b/cgitrc.5.txt index 9fcf445..4da166c 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt | |||
@@ -141,6 +141,11 @@ embedded:: | |||
141 | suitable for embedding in other html pages. Default value: none. See | 141 | suitable for embedding in other html pages. Default value: none. See |
142 | also: "noheader". | 142 | also: "noheader". |
143 | 143 | ||
144 | enable-blame:: | ||
145 | Flag which, when set to "1", will allow cgit to provide a "blame" page | ||
146 | for files, and will make it generate links to that page in appropriate | ||
147 | places. Default value: "0". | ||
148 | |||
144 | enable-commit-graph:: | 149 | enable-commit-graph:: |
145 | Flag which, when set to "1", will make cgit print an ASCII-art commit | 150 | Flag which, when set to "1", will make cgit print an ASCII-art commit |
146 | history graph to the left of the commit messages in the repository | 151 | history graph to the left of the commit messages in the repository |
@@ -799,6 +804,10 @@ enable-http-clone=1 | |||
799 | enable-index-links=1 | 804 | enable-index-links=1 |
800 | 805 | ||
801 | 806 | ||
807 | # Enable blame page and create links to it from tree page | ||
808 | enable-blame=1 | ||
809 | |||
810 | |||
802 | # Enable ASCII art commit history graph on the log pages | 811 | # Enable ASCII art commit history graph on the log pages |
803 | enable-commit-graph=1 | 812 | enable-commit-graph=1 |
804 | 813 | ||
@@ -1,6 +1,6 @@ | |||
1 | /* cmd.c: the cgit command dispatcher | 1 | /* cmd.c: the cgit command dispatcher |
2 | * | 2 | * |
3 | * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> | 3 | * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> |
4 | * | 4 | * |
5 | * Licensed under GNU General Public License v2 | 5 | * Licensed under GNU General Public License v2 |
6 | * (see COPYING for full license text) | 6 | * (see COPYING for full license text) |
@@ -11,6 +11,7 @@ | |||
11 | #include "cache.h" | 11 | #include "cache.h" |
12 | #include "ui-shared.h" | 12 | #include "ui-shared.h" |
13 | #include "ui-atom.h" | 13 | #include "ui-atom.h" |
14 | #include "ui-blame.h" | ||
14 | #include "ui-blob.h" | 15 | #include "ui-blob.h" |
15 | #include "ui-clone.h" | 16 | #include "ui-clone.h" |
16 | #include "ui-commit.h" | 17 | #include "ui-commit.h" |
@@ -63,6 +64,14 @@ static void about_fn(void) | |||
63 | cgit_print_site_readme(); | 64 | cgit_print_site_readme(); |
64 | } | 65 | } |
65 | 66 | ||
67 | static void blame_fn(void) | ||
68 | { | ||
69 | if (ctx.cfg.enable_blame) | ||
70 | cgit_print_blame(); | ||
71 | else | ||
72 | cgit_print_error_page(403, "Forbidden", "Blame is disabled"); | ||
73 | } | ||
74 | |||
66 | static void blob_fn(void) | 75 | static void blob_fn(void) |
67 | { | 76 | { |
68 | cgit_print_blob(ctx.qry.sha1, ctx.qry.path, ctx.qry.head, 0); | 77 | cgit_print_blob(ctx.qry.sha1, ctx.qry.path, ctx.qry.head, 0); |
@@ -164,6 +173,7 @@ struct cgit_cmd *cgit_get_cmd(void) | |||
164 | def_cmd(HEAD, 1, 0, 1), | 173 | def_cmd(HEAD, 1, 0, 1), |
165 | def_cmd(atom, 1, 0, 0), | 174 | def_cmd(atom, 1, 0, 0), |
166 | def_cmd(about, 0, 0, 0), | 175 | def_cmd(about, 0, 0, 0), |
176 | def_cmd(blame, 1, 1, 0), | ||
167 | def_cmd(blob, 1, 0, 0), | 177 | def_cmd(blob, 1, 0, 0), |
168 | def_cmd(commit, 1, 1, 0), | 178 | def_cmd(commit, 1, 1, 0), |
169 | def_cmd(diff, 1, 1, 0), | 179 | def_cmd(diff, 1, 1, 0), |
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 | } | ||
diff --git a/ui-blame.h b/ui-blame.h new file mode 100644 index 0000000..5b97e03 --- /dev/null +++ b/ui-blame.h | |||
@@ -0,0 +1,6 @@ | |||
1 | #ifndef UI_BLAME_H | ||
2 | #define UI_BLAME_H | ||
3 | |||
4 | extern void cgit_print_blame(void); | ||
5 | |||
6 | #endif /* UI_BLAME_H */ | ||