| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | cgit.c | 8 | ||||
| -rw-r--r-- | cgit.css | 24 | ||||
| -rw-r--r-- | cgit.h | 2 | ||||
| -rwxr-xr-x | tests/t0106-diff.sh | 129 | ||||
| -rw-r--r-- | ui-diff.c | 157 | ||||
| -rw-r--r-- | ui-shared.c | 19 |
7 files changed, 336 insertions, 5 deletions
@@ -12,6 +12,8 @@ This is a custom expansion to cgit. Its most prominent changes by now are: * Include optional solarized.css addon theme. +* Allow user to pick variable refs for diff in web ui. + The original cgit project is hosted at [git.zx2c4.com](https://git.zx2c4.com/cgit/about/) ## cgit – CGI for Git @@ -393,6 +393,14 @@ static void querystring_cb(const char *name, const char *value) ctx.qry.ignorews = atoi(value); } else if (!strcmp(name, "follow")) { ctx.qry.follow = atoi(value); + } else if (!strcmp(name, "idoff")) { + ctx.qry.idoff = atoi(value); + if (ctx.qry.idoff < 0) + ctx.qry.idoff = 0; + } else if (!strcmp(name, "id2off")) { + ctx.qry.id2off = atoi(value); + if (ctx.qry.id2off < 0) + ctx.qry.id2off = 0; } } @@ -478,6 +478,14 @@ div#cgit div.cgit-panel { margin-top: 1.5em; } +div#cgit form.diff-ctrls div.cgit-panel { + float: none; +} + +div#cgit form.diff-ctrls { + float: right; +} + div#cgit div.cgit-panel table { border-collapse: collapse; border: solid 1px var(--border-mid); @@ -666,6 +674,22 @@ div#cgit a.button { padding: 0em 0.5em; } +div#cgit form.diff-ctrls div.cgit-panel a.button, +div#cgit form.diff-ctrls div.cgit-panel input[type=submit] { + border: solid 1px var(--border-mid); + background-color: var(--bg-hl); +} + +div#cgit form.diff-ctrls div.cgit-panel input[type=submit] { + font-size: 80%; + padding: 0em 0.5em; +} + +div#cgit span.hint { + color: var(--fg-mute); + font-size: 80%; +} + div#cgit a.primary { font-size: 100%; } @@ -181,6 +181,8 @@ struct cgit_query { char *head; char *oid; char *oid2; + int idoff; + int id2off; char *path; char *name; char *url; diff --git a/tests/t0106-diff.sh b/tests/t0106-diff.sh index 82b645e..0e45087 100755 --- a/tests/t0106-diff.sh +++ b/tests/t0106-diff.sh @@ -16,4 +16,133 @@ test_expect_success 'find added line' ' grep "<div class=.add.>+5</div>" tmp ' +test_expect_success 'ref picker: from: select present' ' + grep "<select name=.id2." tmp +' + +test_expect_success 'ref picker: to: select present' ' + grep "<select name=.id." tmp +' + +test_expect_success 'ref picker: from: label present' ' + grep "from:" tmp +' + +test_expect_success 'ref picker: to: label present' ' + grep "to:" tmp +' + +test_expect_success 'ref pickers live in a separate refs panel' ' + grep "<b>diff refs</b>" tmp && + grep "<b>diff options</b>" tmp +' + +test_expect_success 'ref picker: master branch listed' ' + grep "master" tmp +' + +test_expect_success 'ref picker: auto-submit on change' ' + grep "onchange=.this.form.submit()." tmp +' + +test_expect_success 'flip link present' ' + grep "<a title=.swap from and to. class=.button. href=./foo/diff/?" tmp +' + +test_expect_success 'flip link swaps the default refs' ' + grep "/foo/diff/?id2=master&idoff=1" tmp +' + +test_expect_success 'compare two refs: foo/diff?id=master&id2=HEAD' ' + cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp +' + +test_expect_success 'diff with explicit refs shows diffstat' ' + grep "diffstat" tmp +' + +test_expect_success 'diff with oid2 parameter uses it for old ref' ' + cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp && + grep "from:" tmp +' + +test_expect_success 'ref picker: to: shows master when id=master' ' + cgit_query "url=foo/diff&id=master" >tmp && + grep "master" tmp +' + +test_expect_success 'ref picker: from: empty when no id2' ' + cgit_query "url=foo/diff" >tmp && + grep "<select name=.id2." tmp +' + +test_expect_success 'compare initial commit with HEAD' ' + root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD) && + cgit_query "url=foo/diff&id=HEAD&id2=$root" >tmp +' + +test_expect_success 'diff with HEAD and root shows content' ' + grep "diffstat" tmp +' + +test_expect_success 'flip link swaps explicit refs' ' + cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp && + grep "/foo/diff/?id=HEAD&id2=master" tmp +' + +test_expect_success 'flip link omits id for a diff against a root commit' ' + root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD) && + cgit_query "url=foo/diff&id=$root" >tmp && + grep "href=./foo/diff/?id2=$root." tmp +' + +test_expect_success 'offset input: to: present' ' + grep "name=.idoff." tmp +' + +test_expect_success 'offset input: from: present' ' + grep "name=.id2off." tmp +' + +test_expect_success 'apply button present' ' + grep "value=.apply." tmp +' + +test_expect_success 'offset: idoff steps back the to ref' ' + cgit_query "url=foo/diff&id=master&idoff=1" >tmp && + grep "a/file-4 b/file-4" tmp +' + +test_expect_success 'offset: id2off steps back the from ref' ' + cgit_query "url=foo/diff&id=master&id2=master&id2off=2" >tmp && + grep "a/file-5 b/file-5" tmp +' + +test_expect_success 'offset: filled input and hidden field' ' + cgit_query "url=foo/diff&id=master&idoff=1" >tmp && + grep "title=.N commits back from the selected ref. value=.1.>" tmp && + grep "type=.hidden. name=.idoff. value=.1./>" tmp +' + +test_expect_success 'offset: too large 404s with the stepped rev' ' + cgit_query "url=foo/diff&id=master&id2=master&id2off=9" >tmp && + grep "Bad object name: master~9" tmp +' + +test_expect_success 'empty id2 does not 404' ' + cgit_query "url=foo/diff&id=master&id2=" >tmp && + grep "diffstat" tmp && + ! grep "Bad object name" tmp +' + +test_expect_success 'file link carries the offsets' ' + cgit_query "url=foo/diff&id=master&id2=master&id2off=2" >tmp && + grep "/foo/diff/file-5?id2=master&id2off=2" tmp +' + +test_expect_success 'flip link carries the offsets' ' + cgit_query "url=foo/diff&id=master&id2=master&idoff=1&id2off=2" >tmp && + grep "/foo/diff/?id2=master&idoff=2&id2off=1" tmp +' + test_done @@ -1,6 +1,10 @@ /* ui-diff.c: show diff between two blobs * * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Select variable refs from web ui for diff * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -336,16 +340,145 @@ static void filepair_cb(struct diff_filepair *pair) cgit_ssdiff_footer(); } +static int print_ref_option(const struct reference *ref, void *cb_data) +{ + const char **sel = (const char **)cb_data; + + html_option(ref->name, ref->name, sel[0]); + return 0; +} + +static void cgit_print_ref_select(const char *name, const char *selected) +{ + /* + * Offer every branch and tag so that any two refs can be compared. + */ + html("<select name='"); + html_attr(name); + html("' onchange='this.form.submit();'>"); + refs_for_each_branch_ref(get_main_ref_store(the_repository), + print_ref_option, (void *)&selected); + if (ctx.repo->enable_remote_branches) + refs_for_each_remote_ref(get_main_ref_store(the_repository), + print_ref_option, (void *)&selected); + refs_for_each_tag_ref(get_main_ref_store(the_repository), + print_ref_option, (void *)&selected); + html("</select>"); +} + +/* + * The number of commits to step back from the selected ref, so that any + * ancestor of a branch or tag can be picked without typing a rev into + * the url by hand. + */ +static void cgit_print_ref_offset(const char *name, int value) +{ + html(" <input type='text' size='2' name='"); + html_attr(name); + html("' title='N commits back from the selected ref'"); + if (value) + htmlf(" value='%d'", value); + html("> <span class='hint'>back</span>"); +} + void cgit_print_diff_ctrls(void) { int i, curr; + html("<form class='diff-ctrls' method='get'>"); + /* + * Emit the same hidden fields as cgit_add_hidden_formfields(), except + * id/id2, which are carried by the ref-picker selects below. One + * form wraps both panels so any submission carries all controls. + */ + if (!ctx.cfg.virtual_root) { + struct strbuf url = STRBUF_INIT; + strbuf_addf(&url, "%s/%s", ctx.qry.repo, ctx.qry.page); + if (ctx.qry.vpath) + strbuf_addf(&url, "/%s", ctx.qry.vpath); + html_hidden("url", url.buf); + strbuf_release(&url); + } + if (ctx.qry.head && ctx.repo->defbranch && + strcmp(ctx.qry.head, ctx.repo->defbranch)) + html_hidden("h", ctx.qry.head); + if (ctx.qry.showmsg) + html_hidden("showmsg", "1"); + + html("<div class='cgit-panel'>"); + html("<b>diff refs</b>"); + html("<table>"); + html("<tr><td colspan='2'/></tr>"); + + /* ref pickers: compare any two refs (branches/tags) */ + { + const char *new_ref = ctx.qry.oid && *ctx.qry.oid ? + ctx.qry.oid : ctx.qry.head; + const char *old_ref = ctx.qry.oid2; + + html("<tr>"); + html("<td class='label'>from:</td>"); + html("<td class='ctrl'>"); + cgit_print_ref_select("id2", old_ref); + cgit_print_ref_offset("id2off", ctx.qry.id2off); + html("</td></tr><tr>"); + html("<td class='label'>to:</td>"); + html("<td class='ctrl'>"); + cgit_print_ref_select("id", new_ref); + cgit_print_ref_offset("idoff", ctx.qry.idoff); + html("</td></tr>"); + + html("<tr><td/><td class='ctrl'>"); + html("<input type='submit' value='apply'>"); + html(" | "); + { + const char *flip_new_ref; + int flip_idoff; + int idoff = ctx.qry.idoff; + int id2off = ctx.qry.id2off; + + if (old_ref && *old_ref) { + /* explicit from ref */ + flip_new_ref = old_ref; + flip_idoff = id2off; + } else if (!is_null_oid(old_rev_oid)) { + /* + * The from rev is the parent of the (stepped) + * to rev, i.e. new_ref~(idoff+1); keep the + * ref name in the url instead of a materialized + * parent sha. + */ + flip_new_ref = new_ref; + flip_idoff = idoff + 1; + } else { + /* root: no from rev to flip to */ + flip_new_ref = NULL; + flip_idoff = 0; + } + + /* + * cgit_diff_link() emits the offsets from ctx.qry; + * swap them around the call so they follow the refs. + */ + ctx.qry.idoff = flip_idoff; + ctx.qry.id2off = idoff; + cgit_diff_link("flip", "swap from and to", "button", + ctx.qry.head, + flip_new_ref, new_ref, + ctx.qry.path); + ctx.qry.idoff = idoff; + ctx.qry.id2off = id2off; + } + html("</td></tr>"); + } + html("</table>"); + html("</div>"); + html("<div class='cgit-panel'>"); html("<b>diff options</b>"); - html("<form method='get'>"); - cgit_add_hidden_formfields(1, 0, ctx.qry.page); html("<table>"); html("<tr><td colspan='2'/></tr>"); + html("<tr>"); html("<td class='label'>context:</td>"); html("<td class='ctrl'>"); @@ -379,8 +512,8 @@ void cgit_print_diff_ctrls(void) html("<tr><td/><td class='ctrl'>"); html("<noscript><input type='submit' value='reload'/></noscript>"); html("</td></tr></table>"); - html("</form>"); html("</div>"); + html("</form>"); } void cgit_print_diff(const char *new_rev, const char *old_rev, @@ -388,6 +521,7 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, { struct commit *commit, *commit2; const struct object_id *old_tree_oid, *new_tree_oid; + struct strbuf new_refbuf = STRBUF_INIT, old_refbuf = STRBUF_INIT; diff_type difftype; /* @@ -402,8 +536,16 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, current_prefix = NULL; } - if (!new_rev) + /* + * An empty rev is the same as no rev; a zero offset means the ref + * itself. + */ + if (!new_rev || !*new_rev) new_rev = ctx.qry.head; + if (ctx.qry.idoff) { + strbuf_addf(&new_refbuf, "%s~%d", new_rev, ctx.qry.idoff); + new_rev = new_refbuf.buf; + } if (repo_get_oid(the_repository, new_rev, new_rev_oid)) { cgit_print_error_page(404, "Not found", "Bad object name: %s", new_rev); @@ -417,7 +559,12 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, } new_tree_oid = get_commit_tree_oid(commit); - if (old_rev) { + if (old_rev && *old_rev) { + if (ctx.qry.id2off) { + strbuf_addf(&old_refbuf, "%s~%d", old_rev, + ctx.qry.id2off); + old_rev = old_refbuf.buf; + } if (repo_get_oid(the_repository, old_rev, old_rev_oid)) { cgit_print_error_page(404, "Not found", "Bad object name: %s", old_rev); diff --git a/ui-shared.c b/ui-shared.c index 686fde0..9eb48a9 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -494,6 +494,21 @@ void cgit_diff_link(const char *name, const char *title, const char *class, html_url_arg(old_rev); delim = "&"; } + /* + * Emit the ref offsets after the refs so that they follow the + * refs through file-level links; a zero offset means the ref + * itself. + */ + if (ctx.qry.idoff) { + html(delim); + htmlf("idoff=%d", ctx.qry.idoff); + delim = "&"; + } + if (ctx.qry.id2off) { + html(delim); + htmlf("id2off=%d", ctx.qry.id2off); + delim = "&"; + } if (ctx.qry.difftype) { html(delim); htmlf("dt=%d", ctx.qry.difftype); @@ -1051,6 +1066,10 @@ void cgit_add_hidden_formfields(int incl_head, int incl_search, html_hidden("id", ctx.qry.oid); if (ctx.qry.oid2) html_hidden("id2", ctx.qry.oid2); + if (ctx.qry.idoff) + html_hidden("idoff", fmt("%d", ctx.qry.idoff)); + if (ctx.qry.id2off) + html_hidden("id2off", fmt("%d", ctx.qry.id2off)); if (ctx.qry.showmsg) html_hidden("showmsg", "1"); |