]> gitweb.ps.run Git - ps-cgit/blob - ui-blame.c
git: update to v2.46.0
[ps-cgit] / ui-blame.c
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 #define USE_THE_REPOSITORY_VARIABLE
10
11 #include "cgit.h"
12 #include "ui-blame.h"
13 #include "html.h"
14 #include "ui-shared.h"
15 #include "strvec.h"
16 #include "blame.h"
17
18
19 static char *emit_suspect_detail(struct blame_origin *suspect)
20 {
21         struct commitinfo *info;
22         struct strbuf detail = STRBUF_INIT;
23
24         info = cgit_parse_commit(suspect->commit);
25
26         strbuf_addf(&detail, "author  %s", info->author);
27         if (!ctx.cfg.noplainemail)
28                 strbuf_addf(&detail, " %s", info->author_email);
29         strbuf_addf(&detail, "  %s\n",
30                     show_date(info->author_date, info->author_tz,
31                                     cgit_date_mode(DATE_ISO8601)));
32
33         strbuf_addf(&detail, "committer  %s", info->committer);
34         if (!ctx.cfg.noplainemail)
35                 strbuf_addf(&detail, " %s", info->committer_email);
36         strbuf_addf(&detail, "  %s\n\n",
37                     show_date(info->committer_date, info->committer_tz,
38                                     cgit_date_mode(DATE_ISO8601)));
39
40         strbuf_addstr(&detail, info->subject);
41
42         cgit_free_commitinfo(info);
43         return strbuf_detach(&detail, NULL);
44 }
45
46 static void emit_blame_entry_hash(struct blame_entry *ent)
47 {
48         struct blame_origin *suspect = ent->suspect;
49         struct object_id *oid = &suspect->commit->object.oid;
50         unsigned long line = 0;
51
52         char *detail = emit_suspect_detail(suspect);
53         html("<span class='oid'>");
54         cgit_commit_link(repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV), detail,
55                          NULL, ctx.qry.head, oid_to_hex(oid), suspect->path);
56         html("</span>");
57         free(detail);
58
59         if (!repo_parse_commit(the_repository, suspect->commit) && suspect->commit->parents) {
60                 struct commit *parent = suspect->commit->parents->item;
61
62                 html(" ");
63                 cgit_blame_link("^", "Blame the previous revision", NULL,
64                                 ctx.qry.head, oid_to_hex(&parent->object.oid),
65                                 suspect->path);
66         }
67
68         while (line++ < ent->num_lines)
69                 html("\n");
70 }
71
72 static void emit_blame_entry_linenumber(struct blame_entry *ent)
73 {
74         const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
75
76         unsigned long lineno = ent->lno;
77         while (lineno < ent->lno + ent->num_lines)
78                 htmlf(numberfmt, ++lineno);
79 }
80
81 static void emit_blame_entry_line_background(struct blame_scoreboard *sb,
82                                              struct blame_entry *ent)
83 {
84         unsigned long line;
85         size_t len, maxlen = 2;
86         const char* pos, *endpos;
87
88         for (line = ent->lno; line < ent->lno + ent->num_lines; line++) {
89                 html("\n");
90                 pos = blame_nth_line(sb, line);
91                 endpos = blame_nth_line(sb, line + 1);
92                 len = 0;
93                 while (pos < endpos) {
94                         len++;
95                         if (*pos++ == '\t')
96                                 len = (len + 7) & ~7;
97                 }
98                 if (len > maxlen)
99                         maxlen = len;
100         }
101
102         for (len = 0; len < maxlen - 1; len++)
103                 html(" ");
104 }
105
106 struct walk_tree_context {
107         char *curr_rev;
108         int match_baselen;
109         int state;
110 };
111
112 static void print_object(const struct object_id *oid, const char *path,
113                          const char *basename, const char *rev)
114 {
115         enum object_type type;
116         char *buf;
117         unsigned long size;
118         struct strvec rev_argv = STRVEC_INIT;
119         struct rev_info revs;
120         struct blame_scoreboard sb;
121         struct blame_origin *o;
122         struct blame_entry *ent = NULL;
123
124         type = oid_object_info(the_repository, oid, &size);
125         if (type == OBJ_BAD) {
126                 cgit_print_error_page(404, "Not found", "Bad object name: %s",
127                                       oid_to_hex(oid));
128                 return;
129         }
130
131         buf = repo_read_object_file(the_repository, oid, &type, &size);
132         if (!buf) {
133                 cgit_print_error_page(500, "Internal server error",
134                         "Error reading object %s", oid_to_hex(oid));
135                 return;
136         }
137
138         strvec_push(&rev_argv, "blame");
139         strvec_push(&rev_argv, rev);
140         repo_init_revisions(the_repository, &revs, NULL);
141         revs.diffopt.flags.allow_textconv = 1;
142         setup_revisions(rev_argv.nr, rev_argv.v, &revs, NULL);
143         init_scoreboard(&sb);
144         sb.revs = &revs;
145         sb.repo = the_repository;
146         sb.path = path;
147         setup_scoreboard(&sb, &o);
148         o->suspects = blame_entry_prepend(NULL, 0, sb.num_lines, o);
149         prio_queue_put(&sb.commits, o->commit);
150         blame_origin_decref(o);
151         sb.ent = NULL;
152         sb.path = path;
153         assign_blame(&sb, 0);
154         blame_sort_final(&sb);
155         blame_coalesce(&sb);
156
157         cgit_set_title_from_path(path);
158
159         cgit_print_layout_start();
160         htmlf("blob: %s (", oid_to_hex(oid));
161         cgit_plain_link("plain", NULL, NULL, ctx.qry.head, rev, path);
162         html(") (");
163         cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path);
164         html(")\n");
165
166         if (buffer_is_binary(buf, size)) {
167                 html("<div class='error'>blob is binary.</div>");
168                 goto cleanup;
169         }
170         if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
171                 htmlf("<div class='error'>blob size (%ldKB)"
172                       " exceeds display size limit (%dKB).</div>",
173                       size / 1024, ctx.cfg.max_blob_size);
174                 goto cleanup;
175         }
176
177         html("<table class='blame blob'>\n<tr>\n");
178
179         /* Commit hashes */
180         html("<td class='hashes'>");
181         for (ent = sb.ent; ent; ent = ent->next) {
182                 html("<div class='alt'><pre>");
183                 emit_blame_entry_hash(ent);
184                 html("</pre></div>");
185         }
186         html("</td>\n");
187
188         /* Line numbers */
189         if (ctx.cfg.enable_tree_linenumbers) {
190                 html("<td class='linenumbers'>");
191                 for (ent = sb.ent; ent; ent = ent->next) {
192                         html("<div class='alt'><pre>");
193                         emit_blame_entry_linenumber(ent);
194                         html("</pre></div>");
195                 }
196                 html("</td>\n");
197         }
198
199         html("<td class='lines'><div>");
200
201         /* Colored bars behind lines */
202         html("<div>");
203         for (ent = sb.ent; ent; ) {
204                 struct blame_entry *e = ent->next;
205                 html("<div class='alt'><pre>");
206                 emit_blame_entry_line_background(&sb, ent);
207                 html("</pre></div>");
208                 free(ent);
209                 ent = e;
210         }
211         html("</div>");
212
213         free((void *)sb.final_buf);
214
215         /* Lines */
216         html("<pre><code>");
217         if (ctx.repo->source_filter) {
218                 char *filter_arg = xstrdup(basename);
219                 cgit_open_filter(ctx.repo->source_filter, filter_arg);
220                 html_raw(buf, size);
221                 cgit_close_filter(ctx.repo->source_filter);
222                 free(filter_arg);
223         } else {
224                 html_txt(buf);
225         }
226         html("</code></pre>");
227
228         html("</div></td>\n");
229
230         html("</tr>\n</table>\n");
231
232         cgit_print_layout_end();
233
234 cleanup:
235         free(buf);
236 }
237
238 static int walk_tree(const struct object_id *oid, struct strbuf *base,
239                      const char *pathname, unsigned mode, void *cbdata)
240 {
241         struct walk_tree_context *walk_tree_ctx = cbdata;
242
243         if (base->len == walk_tree_ctx->match_baselen) {
244                 if (S_ISREG(mode)) {
245                         struct strbuf buffer = STRBUF_INIT;
246                         strbuf_addbuf(&buffer, base);
247                         strbuf_addstr(&buffer, pathname);
248                         print_object(oid, buffer.buf, pathname,
249                                      walk_tree_ctx->curr_rev);
250                         strbuf_release(&buffer);
251                         walk_tree_ctx->state = 1;
252                 } else if (S_ISDIR(mode)) {
253                         walk_tree_ctx->state = 2;
254                 }
255         } else if (base->len < INT_MAX
256                         && (int)base->len > walk_tree_ctx->match_baselen) {
257                 walk_tree_ctx->state = 2;
258         } else if (S_ISDIR(mode)) {
259                 return READ_TREE_RECURSIVE;
260         }
261         return 0;
262 }
263
264 static int basedir_len(const char *path)
265 {
266         char *p = strrchr(path, '/');
267         if (p)
268                 return p - path + 1;
269         return 0;
270 }
271
272 void cgit_print_blame(void)
273 {
274         const char *rev = ctx.qry.oid;
275         struct object_id oid;
276         struct commit *commit;
277         struct pathspec_item path_items = {
278                 .match = ctx.qry.path,
279                 .len = ctx.qry.path ? strlen(ctx.qry.path) : 0
280         };
281         struct pathspec paths = {
282                 .nr = 1,
283                 .items = &path_items
284         };
285         struct walk_tree_context walk_tree_ctx = {
286                 .state = 0
287         };
288
289         if (!rev)
290                 rev = ctx.qry.head;
291
292         if (repo_get_oid(the_repository, rev, &oid)) {
293                 cgit_print_error_page(404, "Not found",
294                         "Invalid revision name: %s", rev);
295                 return;
296         }
297         commit = lookup_commit_reference(the_repository, &oid);
298         if (!commit || repo_parse_commit(the_repository, commit)) {
299                 cgit_print_error_page(404, "Not found",
300                         "Invalid commit reference: %s", rev);
301                 return;
302         }
303
304         walk_tree_ctx.curr_rev = xstrdup(rev);
305         walk_tree_ctx.match_baselen = (path_items.match) ?
306                                        basedir_len(path_items.match) : -1;
307
308         read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
309                   &paths, walk_tree, &walk_tree_ctx);
310         if (!walk_tree_ctx.state)
311                 cgit_print_error_page(404, "Not found", "Not found");
312         else if (walk_tree_ctx.state == 2)
313                 cgit_print_error_page(404, "No blame for folders",
314                         "Blame is not available for folders.");
315
316         free(walk_tree_ctx.curr_rev);
317 }