aboutsummaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
authorGravatar Saya Andy <saya.andy@posteo.com> 2026-09-08 00:11:57 +0700
committerGravatar Saya Andy <saya.andy@posteo.com> 2026-09-08 00:11:57 +0700
commit3e40b846456e4efc06ece7180ef152e053114385 (patch)
treefcf80f1d06756472f996bf306ee1fdf1dc9a3b98
parent3b18a7d8c9608ed0bf13d546018f44e703c62e11 (diff)
downloadcgitext-3e40b846456e4efc06ece7180ef152e053114385.tar.gz
cgitext-3e40b846456e4efc06ece7180ef152e053114385.zip
diff: allow stepping back N commits from the selected refsHEADv1.3.1+16masterdevelop
The ref pickers can only select the exact ref that is shown. Add an offset input next to each picker (idoff and id2off), whose value is the number of commits to step back, so any ancestor of a branch or tag can be compared without typing a rev into the url by hand. An apply button submits the values typed there, since text inputs cannot be auto-submitted on their own; the flip link swaps the offsets along with the refs, and the file-level links carry them through. By default both offsets are empty, meaning the selected refs themselves. An empty ref submitted from the form is likewise treated as no ref instead of triggering a bad object name error.
-rw-r--r--cgit.c8
-rw-r--r--cgit.css13
-rw-r--r--cgit.h2
-rwxr-xr-xtests/t0106-diff.sh55
-rw-r--r--ui-diff.c83
-rw-r--r--ui-shared.c19
6 files changed, 167 insertions, 13 deletions
diff --git a/cgit.c b/cgit.c
index bb32dc7..ff4b8d9 100644
--- a/cgit.c
+++ b/cgit.c
@@ -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;
}
}
diff --git a/cgit.css b/cgit.css
index 8563462..911cc45 100644
--- a/cgit.css
+++ b/cgit.css
@@ -674,11 +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 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%;
}
diff --git a/cgit.h b/cgit.h
index e2b8cbc..6911d16 100644
--- a/cgit.h
+++ b/cgit.h
@@ -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 1100596..0e45087 100755
--- a/tests/t0106-diff.sh
+++ b/tests/t0106-diff.sh
@@ -50,8 +50,7 @@ test_expect_success 'flip link present' '
'
test_expect_success 'flip link swaps the default refs' '
- old=$(git --git-dir="$PWD/repos/foo/.git" rev-parse master~1) &&
- grep "/foo/diff/?id=$old&amp;id2=master" tmp
+ grep "/foo/diff/?id2=master&amp;idoff=1" tmp
'
test_expect_success 'compare two refs: foo/diff?id=master&id2=HEAD' '
@@ -88,8 +87,7 @@ test_expect_success 'diff with HEAD and root shows content' '
test_expect_success 'flip link swaps explicit refs' '
cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp &&
- new=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD) &&
- grep "/foo/diff/?id=$new&amp;id2=master" tmp
+ grep "/foo/diff/?id=HEAD&amp;id2=master" tmp
'
test_expect_success 'flip link omits id for a diff against a root commit' '
@@ -98,4 +96,53 @@ test_expect_success 'flip link omits id for a diff against a root commit' '
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&amp;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&amp;idoff=2&amp;id2off=1" tmp
+'
+
test_done
diff --git a/ui-diff.c b/ui-diff.c
index 13c1e32..c77ca2f 100644
--- a/ui-diff.c
+++ b/ui-diff.c
@@ -366,6 +366,21 @@ static void cgit_print_ref_select(const char *name, const char *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;
@@ -397,25 +412,63 @@ void cgit_print_diff_ctrls(void)
/* ref pickers: compare any two refs (branches/tags) */
{
- const char *new_ref = ctx.qry.oid ? ctx.qry.oid : ctx.qry.head;
+ 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'>");
- cgit_diff_link("flip", "swap from and to", "button",
- ctx.qry.head,
- is_null_oid(old_rev_oid) ? NULL :
- oid_to_hex(old_rev_oid),
- new_ref, ctx.qry.path);
+ 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>");
@@ -468,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;
/*
@@ -482,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);
@@ -497,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 = "&amp;";
}
+ /*
+ * 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 = "&amp;";
+ }
+ if (ctx.qry.id2off) {
+ html(delim);
+ htmlf("id2off=%d", ctx.qry.id2off);
+ delim = "&amp;";
+ }
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");