]> gitweb.ps.run Git - ps-cgit/blobdiff - ui-blame.c
ui-blame: add blame UI
[ps-cgit] / ui-blame.c
diff --git a/ui-blame.c b/ui-blame.c
new file mode 100644 (file)
index 0000000..62cf431
--- /dev/null
@@ -0,0 +1,227 @@
+/* ui-blame.c: functions for blame output
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-blame.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "argv-array.h"
+#include "blame.h"
+
+
+static char *emit_suspect_detail(struct blame_origin *suspect)
+{
+       struct commitinfo *info;
+       struct strbuf detail = STRBUF_INIT;
+
+       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);
+       return strbuf_detach(&detail, NULL);
+}
+
+static void emit_blame_entry(struct blame_scoreboard *sb,
+                            struct blame_entry *ent)
+{
+       struct blame_origin *suspect = ent->suspect;
+       struct object_id *oid = &suspect->commit->object.oid;
+       const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
+       const char *cp, *cpend;
+
+       char *detail = emit_suspect_detail(suspect);
+
+       html("<tr><td class='sha1 lines'>");
+       cgit_commit_link(find_unique_abbrev(oid->hash, DEFAULT_ABBREV), detail,
+                        NULL, ctx.qry.head, oid_to_hex(oid), suspect->path);
+       html("</td>\n");
+
+       free(detail);
+
+       if (ctx.cfg.enable_tree_linenumbers) {
+               unsigned long lineno = ent->lno;
+               html("<td class='linenumbers'><pre>");
+               while (lineno < ent->lno + ent->num_lines)
+                       htmlf(numberfmt, ++lineno);
+               html("</pre></td>\n");
+       }
+
+       cp = blame_nth_line(sb, ent->lno);
+       cpend = blame_nth_line(sb, ent->lno + ent->num_lines);
+
+       html("<td class='lines'><pre><code>");
+       html_ntxt(cp, cpend - cp);
+       html("</code></pre></td></tr>\n");
+}
+
+struct walk_tree_context {
+       char *curr_rev;
+       int match_baselen;
+       int state;
+};
+
+static void print_object(const unsigned char *sha1, const char *path,
+                        const char *basename, const char *rev)
+{
+       enum object_type type;
+       unsigned long size;
+       struct argv_array rev_argv = ARGV_ARRAY_INIT;
+       struct rev_info revs;
+       struct blame_scoreboard sb;
+       struct blame_origin *o;
+       struct blame_entry *ent = NULL;
+
+       type = sha1_object_info(sha1, &size);
+       if (type == OBJ_BAD) {
+               cgit_print_error_page(404, "Not found", "Bad object name: %s",
+                                     sha1_to_hex(sha1));
+               return;
+       }
+
+       argv_array_push(&rev_argv, "blame");
+       argv_array_push(&rev_argv, rev);
+       init_revisions(&revs, NULL);
+       DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV);
+       setup_revisions(rev_argv.argc, rev_argv.argv, &revs, NULL);
+       init_scoreboard(&sb);
+       sb.revs = &revs;
+       setup_scoreboard(&sb, path, &o);
+       o->suspects = blame_entry_prepend(NULL, 0, sb.num_lines, o);
+       prio_queue_put(&sb.commits, o->commit);
+       blame_origin_decref(o);
+       sb.ent = NULL;
+       sb.path = path;
+       assign_blame(&sb, 0);
+       blame_sort_final(&sb);
+       blame_coalesce(&sb);
+
+       cgit_set_title_from_path(path);
+
+       cgit_print_layout_start();
+       htmlf("blob: %s (", sha1_to_hex(sha1));
+       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");
+
+       if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
+               htmlf("<div class='error'>blob size (%ldKB)"
+                     " exceeds display size limit (%dKB).</div>",
+                     size / 1024, ctx.cfg.max_blob_size);
+               return;
+       }
+
+       html("<table class='blame blob'>");
+       for (ent = sb.ent; ent; ) {
+               struct blame_entry *e = ent->next;
+               emit_blame_entry(&sb, ent);
+               free(ent);
+               ent = e;
+       }
+       html("</table>\n");
+       free((void *)sb.final_buf);
+
+       cgit_print_layout_end();
+}
+
+static int walk_tree(const unsigned char *sha1, struct strbuf *base,
+                    const char *pathname, unsigned mode, int stage,
+                    void *cbdata)
+{
+       struct walk_tree_context *walk_tree_ctx = cbdata;
+
+       if (base->len == walk_tree_ctx->match_baselen) {
+               if (S_ISREG(mode)) {
+                       struct strbuf buffer = STRBUF_INIT;
+                       strbuf_addbuf(&buffer, base);
+                       strbuf_addstr(&buffer, pathname);
+                       print_object(sha1, buffer.buf, pathname,
+                                    walk_tree_ctx->curr_rev);
+                       strbuf_release(&buffer);
+                       walk_tree_ctx->state = 1;
+               } else if (S_ISDIR(mode)) {
+                       walk_tree_ctx->state = 2;
+               }
+       } else if (base->len < INT_MAX
+                       && (int)base->len > walk_tree_ctx->match_baselen) {
+               walk_tree_ctx->state = 2;
+       } else if (S_ISDIR(mode)) {
+               return READ_TREE_RECURSIVE;
+       }
+       return 0;
+}
+
+static int basedir_len(const char *path)
+{
+       char *p = strrchr(path, '/');
+       if (p)
+               return p - path + 1;
+       return 0;
+}
+
+void cgit_print_blame(void)
+{
+       const char *rev = ctx.qry.sha1;
+       struct object_id oid;
+       struct commit *commit;
+       struct pathspec_item path_items = {
+               .match = ctx.qry.path,
+               .len = ctx.qry.path ? strlen(ctx.qry.path) : 0
+       };
+       struct pathspec paths = {
+               .nr = 1,
+               .items = &path_items
+       };
+       struct walk_tree_context walk_tree_ctx = {
+               .state = 0
+       };
+
+       if (!rev)
+               rev = ctx.qry.head;
+
+       if (get_oid(rev, &oid)) {
+               cgit_print_error_page(404, "Not found",
+                       "Invalid revision name: %s", rev);
+               return;
+       }
+       commit = lookup_commit_reference(&oid);
+       if (!commit || parse_commit(commit)) {
+               cgit_print_error_page(404, "Not found",
+                       "Invalid commit reference: %s", rev);
+               return;
+       }
+
+       walk_tree_ctx.curr_rev = xstrdup(rev);
+       walk_tree_ctx.match_baselen = (path_items.match) ?
+                                      basedir_len(path_items.match) : -1;
+
+       read_tree_recursive(commit->tree, "", 0, 0, &paths, walk_tree,
+               &walk_tree_ctx);
+       if (!walk_tree_ctx.state)
+               cgit_print_error_page(404, "Not found", "Not found");
+       else if (walk_tree_ctx.state == 2)
+               cgit_print_error_page(404, "No blame for folders",
+                       "Blame is not available for folders.");
+
+       free(walk_tree_ctx.curr_rev);
+}