about summary refs log tree commit diff stats
path: root/ui-log.c
diff options
context:
space:
mode:
authorJohn Keeping2015-08-12 15:55:28 +0100
committerJason A. Donenfeld2015-08-12 16:57:46 +0200
commit30304d8156a72ffc95e45e1aa9407319b81bd253 (patch)
treec3f8220fb2abfa0da7f7f0b479415db42820d838 /ui-log.c
parentshared: make cgit_diff_tree_cb public (diff)
downloadcgit-30304d8156a72ffc95e45e1aa9407319b81bd253.tar.gz
cgit-30304d8156a72ffc95e45e1aa9407319b81bd253.zip
log: allow users to follow a file
Teach the "log" UI to behave in the same way as "git log --follow", when
given a suitable instruction by the user.  The default behaviour remains
to show the log without following renames, but the follow behaviour can
be activated by following a link in the page header.

Follow is not the default because outputting merges in follow mode is
tricky ("git log --follow" will not show merges).  We also disable the
graph in follow mode because the commit graph is not simplified so we
end up with frequent gaps in the graph and many lines that do not
connect with any commits we're actually showing.

We also teach the "diff" and "commit" UIs to respect the follow flag on
URLs, causing the single-file version of these UIs to detect renames.
This feature is needed only for commits that rename the path we're
interested in.

For commits before the file has been renamed (i.e. that appear later in
the log list) we change the file path in the links from the log to point
to the old name; this means that links to commits always limit by the
path known to that commit.  If we didn't do this we would need to walk
down the log diff'ing every commit whenever we want to show a commit.
The drawback is that the "Log" link in the top bar of such a page links
to the log limited by the old name, so it will only show pre-rename
commits.  I consider this a reasonable trade-off since the "Back" button
still works and the log matches the path displayed in the top bar.

Since following renames requires running diff on every commit we
consider, I've added a knob to the configuration file to globally
enable/disable this feature.  Note that we may consider a large number
of commits the revision walking machinery no longer performs any path
limitation so we have to examine every commit until we find a page full
of commits that affect the target path or something related to it.

Suggested-by: René Neumann <necoro@necoro.eu>
Signed-off-by: John Keeping <john@keeping.me.uk>
Diffstat (limited to 'ui-log.c')
-rw-r--r--ui-log.c131
1 files changed, 120 insertions, 11 deletions
diff --git a/ui-log.c b/ui-log.c index 8028b27..ff832ce 100644 --- a/ui-log.c +++ b/ui-log.c
@@ -12,7 +12,7 @@
12#include "ui-shared.h" 12#include "ui-shared.h"
13#include "argv-array.h" 13#include "argv-array.h"
14 14
15static int files, add_lines, rem_lines; 15static int files, add_lines, rem_lines, lines_counted;
16 16
17/* 17/*
18 * The list of available column colors in the commit graph. 18 * The list of available column colors in the commit graph.
@@ -67,7 +67,7 @@ void show_commit_decorations(struct commit *commit)
67 strncpy(buf, deco->name + 11, sizeof(buf) - 1); 67 strncpy(buf, deco->name + 11, sizeof(buf) - 1);
68 cgit_log_link(buf, NULL, "branch-deco", buf, NULL, 68 cgit_log_link(buf, NULL, "branch-deco", buf, NULL,
69 ctx.qry.vpath, 0, NULL, NULL, 69 ctx.qry.vpath, 0, NULL, NULL,
70 ctx.qry.showmsg); 70 ctx.qry.showmsg, 0);
71 } 71 }
72 else if (starts_with(deco->name, "tag: refs/tags/")) { 72 else if (starts_with(deco->name, "tag: refs/tags/")) {
73 strncpy(buf, deco->name + 15, sizeof(buf) - 1); 73 strncpy(buf, deco->name + 15, sizeof(buf) - 1);
@@ -84,7 +84,7 @@ void show_commit_decorations(struct commit *commit)
84 cgit_log_link(buf, NULL, "remote-deco", NULL, 84 cgit_log_link(buf, NULL, "remote-deco", NULL,
85 sha1_to_hex(commit->object.sha1), 85 sha1_to_hex(commit->object.sha1),
86 ctx.qry.vpath, 0, NULL, NULL, 86 ctx.qry.vpath, 0, NULL, NULL,
87 ctx.qry.showmsg); 87 ctx.qry.showmsg, 0);
88 } 88 }
89 else { 89 else {
90 strncpy(buf, deco->name, sizeof(buf) - 1); 90 strncpy(buf, deco->name, sizeof(buf) - 1);
@@ -98,6 +98,74 @@ next:
98 html("</span>"); 98 html("</span>");
99} 99}
100 100
101static void handle_rename(struct diff_filepair *pair)
102{
103 /*
104 * After we have seen a rename, we generate links to the previous
105 * name of the file so that commit & diff views get fed the path
106 * that is correct for the commit they are showing, avoiding the
107 * need to walk the entire history leading back to every commit we
108 * show in order detect renames.
109 */
110 if (0 != strcmp(ctx.qry.vpath, pair->two->path)) {
111 free(ctx.qry.vpath);
112 ctx.qry.vpath = xstrdup(pair->two->path);
113 }
114 inspect_files(pair);
115}
116
117static int show_commit(struct commit *commit, struct rev_info *revs)
118{
119 struct commit_list *parents = commit->parents;
120 struct commit *parent;
121 int found = 0, saved_fmt;
122 unsigned saved_flags = revs->diffopt.flags;
123
124
125 /* Always show if we're not in "follow" mode with a single file. */
126 if (!ctx.qry.follow)
127 return 1;
128
129 /*
130 * In "follow" mode, we don't show merges. This is consistent with
131 * "git log --follow -- <file>".
132 */
133 if (parents && parents->next)
134 return 0;
135
136 /*
137 * If this is the root commit, do what rev_info tells us.
138 */
139 if (!parents)
140 return revs->show_root_diff;
141
142 /* When we get here we have precisely one parent. */
143 parent = parents->item;
144 parse_commit(parent);
145
146 files = 0;
147 add_lines = 0;
148 rem_lines = 0;
149
150 DIFF_OPT_SET(&revs->diffopt, RECURSIVE);
151 diff_tree_sha1(parent->tree->object.sha1,
152 commit->tree->object.sha1,
153 "", &revs->diffopt);
154 diffcore_std(&revs->diffopt);
155
156 found = !diff_queue_is_empty();
157 saved_fmt = revs->diffopt.output_format;
158 revs->diffopt.output_format = DIFF_FORMAT_CALLBACK;
159 revs->diffopt.format_callback = cgit_diff_tree_cb;
160 revs->diffopt.format_callback_data = handle_rename;
161 diff_flush(&revs->diffopt);
162 revs->diffopt.output_format = saved_fmt;
163 revs->diffopt.flags = saved_flags;
164
165 lines_counted = 1;
166 return found;
167}
168
101static void print_commit(struct commit *commit, struct rev_info *revs) 169static void print_commit(struct commit *commit, struct rev_info *revs)
102{ 170{
103 struct commitinfo *info; 171 struct commitinfo *info;
@@ -177,7 +245,8 @@ static void print_commit(struct commit *commit, struct rev_info *revs)
177 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 245 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
178 } 246 }
179 247
180 if (ctx.repo->enable_log_filecount || ctx.repo->enable_log_linecount) { 248 if (!lines_counted && (ctx.repo->enable_log_filecount ||
249 ctx.repo->enable_log_linecount)) {
181 files = 0; 250 files = 0;
182 add_lines = 0; 251 add_lines = 0;
183 rem_lines = 0; 252 rem_lines = 0;
@@ -325,7 +394,17 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
325 } 394 }
326 } 395 }
327 } 396 }
328 if (commit_graph) { 397
398 if (!path || !ctx.cfg.enable_follow_links) {
399 /*
400 * If we don't have a path, "follow" is a no-op so make sure
401 * the variable is set to false to avoid needing to check
402 * both this and whether we have a path everywhere.
403 */
404 ctx.qry.follow = 0;
405 }
406
407 if (commit_graph && !ctx.qry.follow) {
329 argv_array_push(&rev_argv, "--graph"); 408 argv_array_push(&rev_argv, "--graph");
330 argv_array_push(&rev_argv, "--color"); 409 argv_array_push(&rev_argv, "--color");
331 graph_set_column_colors(column_colors_html, 410 graph_set_column_colors(column_colors_html,
@@ -337,6 +416,8 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
337 else if (commit_sort == 2) 416 else if (commit_sort == 2)
338 argv_array_push(&rev_argv, "--topo-order"); 417 argv_array_push(&rev_argv, "--topo-order");
339 418
419 if (path && ctx.qry.follow)
420 argv_array_push(&rev_argv, "--follow");
340 argv_array_push(&rev_argv, "--"); 421 argv_array_push(&rev_argv, "--");
341 if (path) 422 if (path)
342 argv_array_push(&rev_argv, path); 423 argv_array_push(&rev_argv, path);
@@ -347,10 +428,17 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
347 rev.verbose_header = 1; 428 rev.verbose_header = 1;
348 rev.show_root_diff = 0; 429 rev.show_root_diff = 0;
349 rev.ignore_missing = 1; 430 rev.ignore_missing = 1;
431 rev.simplify_history = 1;
350 setup_revisions(rev_argv.argc, rev_argv.argv, &rev, NULL); 432 setup_revisions(rev_argv.argc, rev_argv.argv, &rev, NULL);
351 load_ref_decorations(DECORATE_FULL_REFS); 433 load_ref_decorations(DECORATE_FULL_REFS);
352 rev.show_decorations = 1; 434 rev.show_decorations = 1;
353 rev.grep_filter.regflags |= REG_ICASE; 435 rev.grep_filter.regflags |= REG_ICASE;
436
437 rev.diffopt.detect_rename = 1;
438 rev.diffopt.rename_limit = ctx.cfg.renamelimit;
439 if (ctx.qry.ignorews)
440 DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE);
441
354 compile_grep_patterns(&rev.grep_filter); 442 compile_grep_patterns(&rev.grep_filter);
355 prepare_revision_walk(&rev); 443 prepare_revision_walk(&rev);
356 444
@@ -368,11 +456,12 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
368 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL, 456 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
369 NULL, ctx.qry.head, ctx.qry.sha1, 457 NULL, ctx.qry.head, ctx.qry.sha1,
370 ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep, 458 ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep,
371 ctx.qry.search, ctx.qry.showmsg ? 0 : 1); 459 ctx.qry.search, ctx.qry.showmsg ? 0 : 1,
460 ctx.qry.follow);
372 html(")"); 461 html(")");
373 } 462 }
374 html("</th><th class='left'>Author</th>"); 463 html("</th><th class='left'>Author</th>");
375 if (commit_graph) 464 if (rev.graph)
376 html("<th class='left'>Age</th>"); 465 html("<th class='left'>Age</th>");
377 if (ctx.repo->enable_log_filecount) { 466 if (ctx.repo->enable_log_filecount) {
378 html("<th class='left'>Files</th>"); 467 html("<th class='left'>Files</th>");
@@ -388,13 +477,30 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
388 ofs = 0; 477 ofs = 0;
389 478
390 for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) { 479 for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) {
480 if (show_commit(commit, &rev))
481 i++;
391 free_commit_buffer(commit); 482 free_commit_buffer(commit);
392 free_commit_list(commit->parents); 483 free_commit_list(commit->parents);
393 commit->parents = NULL; 484 commit->parents = NULL;
394 } 485 }
395 486
396 for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; i++) { 487 for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; i++) {
397 print_commit(commit, &rev); 488 /*
489 * In "follow" mode, we must count the files and lines the
490 * first time we invoke diff on a given commit, and we need
491 * to do that to see if the commit touches the path we care
492 * about, so we do it in show_commit. Hence we must clear
493 * lines_counted here.
494 *
495 * This has the side effect of avoiding running diff twice
496 * when we are both following renames and showing file
497 * and/or line counts.
498 */
499 lines_counted = 0;
500 if (show_commit(commit, &rev)) {
501 i++;
502 print_commit(commit, &rev);
503 }
398 free_commit_buffer(commit); 504 free_commit_buffer(commit);
399 free_commit_list(commit->parents); 505 free_commit_list(commit->parents);
400 commit->parents = NULL; 506 commit->parents = NULL;
@@ -406,7 +512,8 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
406 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, 512 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head,
407 ctx.qry.sha1, ctx.qry.vpath, 513 ctx.qry.sha1, ctx.qry.vpath,
408 ofs - cnt, ctx.qry.grep, 514 ofs - cnt, ctx.qry.grep,
409 ctx.qry.search, ctx.qry.showmsg); 515 ctx.qry.search, ctx.qry.showmsg,
516 ctx.qry.follow);
410 html("</li>"); 517 html("</li>");
411 } 518 }
412 if ((commit = get_revision(&rev)) != NULL) { 519 if ((commit = get_revision(&rev)) != NULL) {
@@ -414,14 +521,16 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
414 cgit_log_link("[next]", NULL, NULL, ctx.qry.head, 521 cgit_log_link("[next]", NULL, NULL, ctx.qry.head,
415 ctx.qry.sha1, ctx.qry.vpath, 522 ctx.qry.sha1, ctx.qry.vpath,
416 ofs + cnt, ctx.qry.grep, 523 ofs + cnt, ctx.qry.grep,
417 ctx.qry.search, ctx.qry.showmsg); 524 ctx.qry.search, ctx.qry.showmsg,
525 ctx.qry.follow);
418 html("</li>"); 526 html("</li>");
419 } 527 }
420 html("</ul>"); 528 html("</ul>");
421 } else if ((commit = get_revision(&rev)) != NULL) { 529 } else if ((commit = get_revision(&rev)) != NULL) {
422 htmlf("<tr class='nohover'><td colspan='%d'>", columns); 530 htmlf("<tr class='nohover'><td colspan='%d'>", columns);
423 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, 531 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL,
424 ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg); 532 ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg,
533 ctx.qry.follow);
425 html("</td></tr>\n"); 534 html("</td></tr>\n");
426 } 535 }
427 536