diff options
| -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 */ | ||
