diff --git a/cgit.c b/cgit.c index baad1c8..d84b4be 100644 --- a/cgit.c +++ b/cgit.c @@ -152,6 +152,8 @@ static void config_cb(const char *name, const char *value) ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); else if (!strcmp(name, "enable-filter-overrides")) ctx.cfg.enable_filter_overrides = atoi(value); + else if (!strcmp(name, "enable-follow-links")) + ctx.cfg.enable_follow_links = atoi(value); else if (!strcmp(name, "enable-http-clone")) ctx.cfg.enable_http_clone = atoi(value); else if (!strcmp(name, "enable-index-links")) @@ -333,6 +335,8 @@ static void querystring_cb(const char *name, const char *value) ctx.qry.context = atoi(value); } else if (!strcmp(name, "ignorews")) { ctx.qry.ignorews = atoi(value); + } else if (!strcmp(name, "follow")) { + ctx.qry.follow = atoi(value); } } diff --git a/cgit.h b/cgit.h index b2253d2..3120562 100644 --- a/cgit.h +++ b/cgit.h @@ -179,6 +179,7 @@ struct cgit_query { int show_all; int context; int ignorews; + int follow; char *vpath; }; @@ -221,6 +222,7 @@ struct cgit_config { int case_sensitive_sort; int embedded; int enable_filter_overrides; + int enable_follow_links; int enable_http_clone; int enable_index_links; int enable_index_owner; diff --git a/cgitrc.5.txt b/cgitrc.5.txt index e21ece9..759f353 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -150,6 +150,10 @@ enable-filter-overrides:: Flag which, when set to "1", allows all filter settings to be overridden in repository-specific cgitrc files. Default value: none. +enable-follow-links:: + Flag which, when set to "1", allows users to follow a file in the log + view. Default value: "0". + enable-http-clone:: If set to "1", cgit will act as an dumb HTTP endpoint for git clones. You can add "http://$HTTP_HOST$SCRIPT_NAME/$CGIT_REPO_URL" to clone-url diff --git a/ui-diff.c b/ui-diff.c index 1cf2ce0..caebd5d 100644 --- a/ui-diff.c +++ b/ui-diff.c @@ -36,6 +36,7 @@ static struct fileinfo { static int use_ssdiff = 0; static struct diff_filepair *current_filepair; +static const char *current_prefix; struct diff_filespec *cgit_get_current_old_file(void) { @@ -132,11 +133,30 @@ static void count_diff_lines(char *line, int len) } } +static int show_filepair(struct diff_filepair *pair) +{ + /* Always show if we have no limiting prefix. */ + if (!current_prefix) + return 1; + + /* Show if either path in the pair begins with the prefix. */ + if (starts_with(pair->one->path, current_prefix) || + starts_with(pair->two->path, current_prefix)) + return 1; + + /* Otherwise we don't want to show this filepair. */ + return 0; +} + static void inspect_filepair(struct diff_filepair *pair) { int binary = 0; unsigned long old_size = 0; unsigned long new_size = 0; + + if (!show_filepair(pair)) + return; + files++; lines_added = 0; lines_removed = 0; @@ -279,6 +299,9 @@ static void filepair_cb(struct diff_filepair *pair) int binary = 0; linediff_fn print_line_fn = print_line; + if (!show_filepair(pair)) + return; + current_filepair = pair; if (use_ssdiff) { cgit_ssdiff_header_begin(); @@ -365,6 +388,18 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, const unsigned char *old_tree_sha1, *new_tree_sha1; diff_type difftype; + /* + * If "follow" is set then the diff machinery needs to examine the + * entire commit to detect renames so we must limit the paths in our + * own callbacks and not pass the prefix to the diff machinery. + */ + if (ctx.qry.follow && ctx.cfg.enable_follow_links) { + current_prefix = prefix; + prefix = ""; + } else { + current_prefix = NULL; + } + if (!new_rev) new_rev = ctx.qry.head; if (get_sha1(new_rev, new_rev_sha1)) { 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 @@ #include "ui-shared.h" #include "argv-array.h" -static int files, add_lines, rem_lines; +static int files, add_lines, rem_lines, lines_counted; /* * The list of available column colors in the commit graph. @@ -67,7 +67,7 @@ void show_commit_decorations(struct commit *commit) strncpy(buf, deco->name + 11, sizeof(buf) - 1); cgit_log_link(buf, NULL, "branch-deco", buf, NULL, ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg); + ctx.qry.showmsg, 0); } else if (starts_with(deco->name, "tag: refs/tags/")) { strncpy(buf, deco->name + 15, sizeof(buf) - 1); @@ -84,7 +84,7 @@ void show_commit_decorations(struct commit *commit) cgit_log_link(buf, NULL, "remote-deco", NULL, sha1_to_hex(commit->object.sha1), ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg); + ctx.qry.showmsg, 0); } else { strncpy(buf, deco->name, sizeof(buf) - 1); @@ -98,6 +98,74 @@ next: html(""); } +static void handle_rename(struct diff_filepair *pair) +{ + /* + * After we have seen a rename, we generate links to the previous + * name of the file so that commit & diff views get fed the path + * that is correct for the commit they are showing, avoiding the + * need to walk the entire history leading back to every commit we + * show in order detect renames. + */ + if (0 != strcmp(ctx.qry.vpath, pair->two->path)) { + free(ctx.qry.vpath); + ctx.qry.vpath = xstrdup(pair->two->path); + } + inspect_files(pair); +} + +static int show_commit(struct commit *commit, struct rev_info *revs) +{ + struct commit_list *parents = commit->parents; + struct commit *parent; + int found = 0, saved_fmt; + unsigned saved_flags = revs->diffopt.flags; + + + /* Always show if we're not in "follow" mode with a single file. */ + if (!ctx.qry.follow) + return 1; + + /* + * In "follow" mode, we don't show merges. This is consistent with + * "git log --follow -- ". + */ + if (parents && parents->next) + return 0; + + /* + * If this is the root commit, do what rev_info tells us. + */ + if (!parents) + return revs->show_root_diff; + + /* When we get here we have precisely one parent. */ + parent = parents->item; + parse_commit(parent); + + files = 0; + add_lines = 0; + rem_lines = 0; + + DIFF_OPT_SET(&revs->diffopt, RECURSIVE); + diff_tree_sha1(parent->tree->object.sha1, + commit->tree->object.sha1, + "", &revs->diffopt); + diffcore_std(&revs->diffopt); + + found = !diff_queue_is_empty(); + saved_fmt = revs->diffopt.output_format; + revs->diffopt.output_format = DIFF_FORMAT_CALLBACK; + revs->diffopt.format_callback = cgit_diff_tree_cb; + revs->diffopt.format_callback_data = handle_rename; + diff_flush(&revs->diffopt); + revs->diffopt.output_format = saved_fmt; + revs->diffopt.flags = saved_flags; + + lines_counted = 1; + return found; +} + static void print_commit(struct commit *commit, struct rev_info *revs) { struct commitinfo *info; @@ -177,7 +245,8 @@ static void print_commit(struct commit *commit, struct rev_info *revs) cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); } - if (ctx.repo->enable_log_filecount || ctx.repo->enable_log_linecount) { + if (!lines_counted && (ctx.repo->enable_log_filecount || + ctx.repo->enable_log_linecount)) { files = 0; add_lines = 0; rem_lines = 0; @@ -325,7 +394,17 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern } } } - if (commit_graph) { + + if (!path || !ctx.cfg.enable_follow_links) { + /* + * If we don't have a path, "follow" is a no-op so make sure + * the variable is set to false to avoid needing to check + * both this and whether we have a path everywhere. + */ + ctx.qry.follow = 0; + } + + if (commit_graph && !ctx.qry.follow) { argv_array_push(&rev_argv, "--graph"); argv_array_push(&rev_argv, "--color"); 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 else if (commit_sort == 2) argv_array_push(&rev_argv, "--topo-order"); + if (path && ctx.qry.follow) + argv_array_push(&rev_argv, "--follow"); argv_array_push(&rev_argv, "--"); if (path) 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 rev.verbose_header = 1; rev.show_root_diff = 0; rev.ignore_missing = 1; + rev.simplify_history = 1; setup_revisions(rev_argv.argc, rev_argv.argv, &rev, NULL); load_ref_decorations(DECORATE_FULL_REFS); rev.show_decorations = 1; rev.grep_filter.regflags |= REG_ICASE; + + rev.diffopt.detect_rename = 1; + rev.diffopt.rename_limit = ctx.cfg.renamelimit; + if (ctx.qry.ignorews) + DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE); + compile_grep_patterns(&rev.grep_filter); prepare_revision_walk(&rev); @@ -368,11 +456,12 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL, NULL, ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep, - ctx.qry.search, ctx.qry.showmsg ? 0 : 1); + ctx.qry.search, ctx.qry.showmsg ? 0 : 1, + ctx.qry.follow); html(")"); } html("Author"); - if (commit_graph) + if (rev.graph) html("Age"); if (ctx.repo->enable_log_filecount) { html("Files"); @@ -388,13 +477,30 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern ofs = 0; for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) { + if (show_commit(commit, &rev)) + i++; free_commit_buffer(commit); free_commit_list(commit->parents); commit->parents = NULL; } for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; i++) { - print_commit(commit, &rev); + /* + * In "follow" mode, we must count the files and lines the + * first time we invoke diff on a given commit, and we need + * to do that to see if the commit touches the path we care + * about, so we do it in show_commit. Hence we must clear + * lines_counted here. + * + * This has the side effect of avoiding running diff twice + * when we are both following renames and showing file + * and/or line counts. + */ + lines_counted = 0; + if (show_commit(commit, &rev)) { + i++; + print_commit(commit, &rev); + } free_commit_buffer(commit); free_commit_list(commit->parents); commit->parents = NULL; @@ -406,7 +512,8 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath, ofs - cnt, ctx.qry.grep, - ctx.qry.search, ctx.qry.showmsg); + ctx.qry.search, ctx.qry.showmsg, + ctx.qry.follow); html(""); } 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 cgit_log_link("[next]", NULL, NULL, ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath, ofs + cnt, ctx.qry.grep, - ctx.qry.search, ctx.qry.showmsg); + ctx.qry.search, ctx.qry.showmsg, + ctx.qry.follow); html(""); } html(""); } else if ((commit = get_revision(&rev)) != NULL) { htmlf("", columns); cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, - ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg); + ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg, + ctx.qry.follow); html("\n"); } diff --git a/ui-refs.c b/ui-refs.c index d3d71dd..73a187b 100644 --- a/ui-refs.c +++ b/ui-refs.c @@ -63,7 +63,7 @@ static int print_branch(struct refinfo *ref) return 1; html(""); cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL, - ctx.qry.showmsg); + ctx.qry.showmsg, 0); html(""); if (ref->object->type == OBJ_COMMIT) { diff --git a/ui-repolist.c b/ui-repolist.c index 2453a7f..edefc4c 100644 --- a/ui-repolist.c +++ b/ui-repolist.c @@ -330,7 +330,7 @@ void cgit_print_repolist(void) html(""); cgit_summary_link("summary", NULL, "button", NULL); cgit_log_link("log", NULL, "button", NULL, NULL, NULL, - 0, NULL, NULL, ctx.qry.showmsg); + 0, NULL, NULL, ctx.qry.showmsg, 0); cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL); html(""); } diff --git a/ui-shared.c b/ui-shared.c index 4f84b7c..6be0c2e 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -303,7 +303,8 @@ void cgit_plain_link(const char *name, const char *title, const char *class, void cgit_log_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path, - int ofs, const char *grep, const char *pattern, int showmsg) + int ofs, const char *grep, const char *pattern, int showmsg, + int follow) { char *delim; @@ -332,6 +333,11 @@ void cgit_log_link(const char *name, const char *title, const char *class, if (showmsg) { html(delim); html("showmsg=1"); + delim = "&"; + } + if (follow) { + html(delim); + html("follow=1"); } html("'>"); html_txt(name); @@ -373,6 +379,10 @@ void cgit_commit_link(char *name, const char *title, const char *class, html("ignorews=1"); delim = "&"; } + if (ctx.qry.follow) { + html(delim); + html("follow=1"); + } html("'>"); if (name[0] != '\0') html_txt(name); @@ -429,6 +439,10 @@ void cgit_diff_link(const char *name, const char *title, const char *class, html("ignorews=1"); delim = "&"; } + if (ctx.qry.follow) { + html(delim); + html("follow=1"); + } html("'>"); html_txt(name); html(""); @@ -469,7 +483,7 @@ static void cgit_self_link(char *name, const char *title, const char *class) ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, ctx.qry.path, ctx.qry.ofs, ctx.qry.grep, ctx.qry.search, - ctx.qry.showmsg); + ctx.qry.showmsg, ctx.qry.follow); else if (!strcmp(ctx.qry.page, "commit")) cgit_commit_link(name, title, class, ctx.qry.head, ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL, @@ -945,7 +959,7 @@ void cgit_print_pageheader(void) ctx.qry.sha1, NULL); cgit_log_link("log", NULL, hc("log"), ctx.qry.head, NULL, ctx.qry.vpath, 0, NULL, NULL, - ctx.qry.showmsg); + ctx.qry.showmsg, ctx.qry.follow); cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath); cgit_commit_link("commit", NULL, hc("commit"), @@ -993,6 +1007,14 @@ void cgit_print_pageheader(void) html("
"); html("path: "); cgit_print_path_crumbs(ctx.qry.vpath); + if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) { + html(" ("); + ctx.qry.follow = !ctx.qry.follow; + cgit_self_link(ctx.qry.follow ? "follow" : "unfollow", + NULL, NULL); + ctx.qry.follow = !ctx.qry.follow; + html(")"); + } html("
"); } html("
"); diff --git a/ui-shared.h b/ui-shared.h index 43d0fa6..788b1bc 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -31,7 +31,7 @@ extern void cgit_plain_link(const char *name, const char *title, extern void cgit_log_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path, int ofs, const char *grep, - const char *pattern, int showmsg); + const char *pattern, int showmsg, int follow); extern void cgit_commit_link(char *name, const char *title, const char *class, const char *head, const char *rev, const char *path); diff --git a/ui-tree.c b/ui-tree.c index bbc468e..c8d24f6 100644 --- a/ui-tree.c +++ b/ui-tree.c @@ -166,7 +166,7 @@ static int ls_item(const unsigned char *sha1, struct strbuf *base, html(""); cgit_log_link("log", NULL, "button", ctx.qry.head, walk_tree_ctx->curr_rev, fullpath.buf, 0, NULL, NULL, - ctx.qry.showmsg); + ctx.qry.showmsg, 0); if (ctx.repo->max_stats) cgit_stats_link("stats", NULL, "button", ctx.qry.head, fullpath.buf);