"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Mark Jamsek <mark@jamsek.com>
Subject:
got {diff,log,update} -c KEYWORD (cf. svn revision keywords)
To:
gameoftrees@openbsd.org
Date:
Thu, 13 Jul 2023 01:42:42 +1000

Download raw body.

Thread
As discussed not too long ago, the below diff implements the special
keywords BASE and HEAD, which can be used as the <commit> argument to
the -c option in the diff, log, and update commands. This is similar to
svn's revision keywords and fossil's special tags, but with a bit more
functionality.

An optional modifier can be appended in the form:

  KEYWORD(+|-)[N]

where '+' and '-' indicates [the Nth] descendant and antecedent
of KEYWORD, respectively; for example:

  got diff -cBASE -cHEAD	# work tree base and head commit
  got update -cHEAD-2		# 2nd generation ancestor of HEAD
  got log -cBASE+		# 1st generation descendant of BASE
  got update -cHEAD-		# 1st generation ancestor of HEAD
  got diff -cBASE

if the optional integer N is omitted, a "1" is implicitly appended
(i.e., BASE+ == BASE+1).

See the man page changes for the complete interface.

Inspiration for the syntax came from mblaze; I think +/-N is really
intuitive--and I've always found git's ^^ too abstruse!

The implementation is pretty simple, and I think this will be quite a
convenient feature. The diff includes coverage for the new keywords, and
regress is still happy.


diffstat b161263abf48e69eb1f05028b633808dfe337357 44cff8d1b214c553155bce70226c4aa341b173db
 M  got/got.1                  |   96+  0-
 M  got/got.c                  |  255+  1-
 M  regress/cmdline/common.sh  |    6+  0-
 M  regress/cmdline/diff.sh    |  249+  0-
 M  regress/cmdline/log.sh     |  104+  0-
 M  regress/cmdline/update.sh  |  224+  0-

6 files changed, 934 insertions(+), 1 deletion(-)

diff b161263abf48e69eb1f05028b633808dfe337357 44cff8d1b214c553155bce70226c4aa341b173db
commit - b161263abf48e69eb1f05028b633808dfe337357
commit + 44cff8d1b214c553155bce70226c4aa341b173db
blob - ea3b833f1bc15d5ea3e276e75da0a16f1e29702c
blob + a177fa9631eb618bf1545c030b68113d5128b005
--- got/got.1
+++ got/got.1
@@ -698,6 +698,36 @@ If this option is not specified, the most recent commi
 or tag name which will be resolved to a commit ID.
 An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
+The special
+.Ar commit
+keywords
+.Qq BASE
+and
+.Qq HEAD
+can also be used to represent the work tree's base commit
+and HEAD reference, respectively.
+Keywords may be appended with
+.Qq +
+or
+.Qq -
+modifiers and an optional integer N to denote its
+Nth descendant or antecedent, respectively; for example,
+.Sy HEAD-2
+denotes the HEAD reference's 2nd generation ancestor, and
+.Sy BASE+4
+denotes the 4th generation descendant of the work tree's base commit.
+If an integer does not follow the
+.Qq +
+or
+.Qq -
+modifier, a
+.Qq 1
+is implicitly appended
+.Po e.g.,
+.Sy HEAD-
+is equivalent to
+.Sy HEAD-1
+.Pc .
 If this option is not specified, the most recent commit on the work tree's
 branch will be used.
 .It Fl q
@@ -857,6 +887,39 @@ If this option is not specified, default to the work t
 or tag name which will be resolved to a commit ID.
 An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
+The special
+.Ar commit
+keywords
+.Qq BASE
+and
+.Qq HEAD
+can also be used to represent the work tree's base commit
+and HEAD reference, respectively.
+The former is only valid if invoked in a work tree, while the latter will
+resolve to the tip of the work tree's current branch if invoked in a
+work tree, otherwise it will resolve to the repository's HEAD reference.
+Keywords may be appended with
+.Qq +
+or
+.Qq -
+modifiers and an optional integer N to denote its
+Nth descendant or antecedent, respectively; for example,
+.Sy HEAD-2
+denotes the HEAD reference's 2nd generation ancestor, and
+.Sy BASE+4
+denotes the 4th generation descendant of the work tree's base commit.
+A
+.Qq +
+or
+.Qq -
+modifier without a trailing integer has an implicit
+.Qq 1
+appended
+.Po e.g.,
+.Sy BASE+
+is equivalent to
+.Sy BASE+1
+.Pc .
 If this option is not specified, default to the work tree's current branch
 if invoked in a work tree, or to the repository's HEAD reference.
 .It Fl d
@@ -981,6 +1044,39 @@ automatically, provided the abbreviation is unique.
 or tag name which will be resolved to a commit ID.
 An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
+The special
+.Ar commit
+keywords
+.Qq BASE
+and
+.Qq HEAD
+can also be used to represent the work tree's base commit
+and HEAD reference, respectively.
+The former is only valid if invoked in a work tree, while the latter will
+resolve to the tip of the work tree's current branch if invoked in a
+work tree, otherwise it will resolve to the repository's HEAD reference.
+Keywords may be appended with
+.Qq +
+or
+.Qq -
+modifiers and an optional integer N to denote its
+Nth descendant or antecedent, respectively; for example,
+.Sy HEAD-2
+denotes the HEAD reference's 2nd generation ancestor, and
+.Sy BASE+4
+denotes the 4th generation descendant of the work tree's base commit.
+If an integer does not follow the
+.Qq +
+or
+.Qq -
+modifier, a
+.Qq 1
+is implicitly appended
+.Po e.g.,
+.Sy HEAD-
+is equivalent to
+.Sy HEAD-1
+.Pc .
 .Pp
 If the
 .Fl c
blob - 01fe4299c82e150fcd8020915536f48fc73bacbc
blob + 1341ca295e1cc9fcf986ebde3e9e70b6ea29d351
--- got/got.c
+++ got/got.c
@@ -32,6 +32,7 @@
 #include <sha2.h>
 #include <signal.h>
 #include <stdio.h>
+#include <stddef.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
@@ -3465,7 +3466,226 @@ static const struct got_error *
 	return err;
 }
 
+/*
+ * Commit keywords to specify references in the repository
+ * (cf. svn keywords, fossil special tags, hg revsets).
+ */
+#define GOT_KEYWORD_BASE	"BASE"	/* work tree base commit */
+#define GOT_KEYWORD_HEAD	"HEAD"	/* worktree/repo HEAD commit */
+#define GOT_KEYWORD_BASE_LEN	(sizeof(GOT_KEYWORD_BASE) - 1)
+#define GOT_KEYWORD_HEAD_LEN	(sizeof(GOT_KEYWORD_HEAD) - 1)
+
+struct keyword_mod {
+	uint64_t	n;
+	uint8_t		sym;
+};
+
+#define GOT_KEYWORD_DESCENDANT	'+'
+#define GOT_KEYWORD_ANCESTOR	'-'
+
 static const struct got_error *
+keyword_modifier(struct keyword_mod *kwm, const char *keyword, size_t kwlen)
+{
+	const char	*p = keyword + kwlen - 1;
+	uint8_t		 symbol = 0;
+
+	/* find symbol at start of keyword modifier substring: (+|-)N */
+	while (p > keyword && *p >= '0' && *p <= '9')
+		--p;
+	if (*p == GOT_KEYWORD_DESCENDANT || *p == GOT_KEYWORD_ANCESTOR)
+		symbol = *p;
+
+	if (symbol) {
+		unsigned long long	max;
+		unsigned long long	n = 0;
+		size_t			len = p - keyword;
+
+		max = ((unsigned long long)LLONG_MIN) / 10 + 1;
+
+		/*
+		 * Don't overflow. Max valid request is whichever is greatest
+		 * between the commit corresponding to keyword and either HEAD
+		 * or ROOT, but just make sure not to overflow. If n extends
+		 * beyond HEAD/ROOT, use HEAD/ROOT instead of a hard error.
+		 */
+		while (++p < keyword + kwlen) {
+			if (n > max)
+				return got_error_fmt(GOT_ERR_NO_SPACE,
+				    "keyword modifier too large: %s", keyword);
+			n = n * 10 + (*p - '0');
+		}
+
+		if (n == 0 && len == kwlen - 1)
+			n = 1;	/* keyword(+/-) with no N == keyword(+/-)1 */
+
+		kwm->n = n;
+		kwm->sym = symbol;
+	}
+
+	return NULL;
+}
+
+/*
+ * Check if keyword matches "BASE" or "HEAD" with optional trailing modifier:
+ *	KEYWORD(+/-)N	Nth generation descendant/antecedent of KEYWORD
+ *	KEYWORD(+/-)	1st generation descendant/antecedent of KEYWORD
+ * If a match is found, return the corresponding commit id string in *ret,
+ * of which the caller takes ownership and must free.
+ * Otherwise return NULL, which indicates a match was not found.
+ * If the modifier is greater than the number of ancestors/descendants,
+ * return the id string of the oldest/most recent commit (i.e., ROOT/HEAD).
+ */
+static const struct got_error *
+keyword_to_idstr(char **ret, const char *keyword, struct got_repository *repo,
+    struct got_worktree *wt)
+{
+	const struct got_error		*err = NULL;
+	struct got_commit_graph		*graph = NULL;
+	struct got_object_id		*head_id = NULL, *kwid = NULL;
+	struct got_object_id		 iter_id;
+	struct got_reflist_head		 refs;
+	struct got_object_id_queue	 commits;
+	struct got_object_qid		*qid;
+	struct keyword_mod		 kwm;
+	char				*kwid_str = NULL;
+	uint64_t			 n = 0;
+
+	*ret = NULL;
+	TAILQ_INIT(&refs);
+	STAILQ_INIT(&commits);
+	memset(&kwm, 0, sizeof(kwm));
+
+	if (strpbrk(keyword, "+-") != NULL) {
+		err = keyword_modifier(&kwm, keyword, strlen(keyword));
+		if (err != NULL)
+			return err;
+	}
+
+	if (strncmp(keyword, GOT_KEYWORD_BASE, GOT_KEYWORD_BASE_LEN) == 0) {
+		if (wt == NULL)
+			return got_error_msg(GOT_ERR_NOT_WORKTREE,
+			    "'-c "GOT_KEYWORD_BASE"' requires work tree");
+
+		err = got_object_id_str(&kwid_str,
+		    got_worktree_get_base_commit_id(wt));
+		if (err != NULL)
+			return err;
+	} else if (strncmp(keyword,
+	    GOT_KEYWORD_HEAD, GOT_KEYWORD_HEAD_LEN) == 0) {
+		struct got_reference *head_ref;
+
+		err = got_ref_open(&head_ref, repo, wt != NULL ?
+		    got_worktree_get_head_ref_name(wt) : GOT_REF_HEAD, 0);
+		if (err != NULL)
+			return err;
+
+		kwid_str = got_ref_to_str(head_ref);
+		got_ref_close(head_ref);
+		if (kwid_str == NULL)
+			return got_error_from_errno("got_ref_to_str");
+	}
+
+	if (kwm.n == 0)
+		goto done;	/* unmodified keyword */
+
+	err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL);
+	if (err)
+		goto done;
+
+	err = got_repo_match_object_id(&kwid, NULL, kwid_str,
+	    GOT_OBJ_TYPE_COMMIT, &refs, repo);
+	if (err != NULL)
+		goto done;
+
+	/*
+	 * If looking for a descendant, we need to iterate from
+	 * HEAD so grab its id now if it's not already in kwid.
+	 */
+	if (kwm.sym == GOT_KEYWORD_DESCENDANT &&
+	    strncmp(keyword, GOT_KEYWORD_HEAD, GOT_KEYWORD_HEAD_LEN) != 0) {
+		struct got_reference *head_ref;
+
+		err = got_ref_open(&head_ref, repo, wt != NULL ?
+		    got_worktree_get_head_ref_name(wt) : GOT_REF_HEAD, 0);
+		if (err != NULL)
+			goto done;
+		err = got_ref_resolve(&head_id, repo, head_ref);
+		got_ref_close(head_ref);
+		if (err != NULL)
+			goto done;
+	}
+
+	err = got_commit_graph_open(&graph, "/", 1);
+	if (err)
+		goto done;
+
+	err = got_commit_graph_iter_start(graph,
+	    head_id != NULL ? head_id : kwid, repo, check_cancelled, NULL);
+	if (err)
+		goto done;
+
+	while (n <= kwm.n) {
+		err = got_commit_graph_iter_next(&iter_id, graph, repo,
+		    check_cancelled, NULL);
+		if (err) {
+			if (err->code == GOT_ERR_ITER_COMPLETED)
+				err = NULL;
+			break;
+		}
+
+		if (kwm.sym == GOT_KEYWORD_DESCENDANT) {
+			/*
+			 * We want the Nth generation descendant of KEYWORD,
+			 * so queue all commits from HEAD to KEYWORD then we
+			 * can walk from KEYWORD to its Nth gen descendent.
+			 */
+			err = got_object_qid_alloc(&qid, &iter_id);
+			if (err)
+				goto done;
+			STAILQ_INSERT_HEAD(&commits, qid, entry);
+
+			if (got_object_id_cmp(&iter_id, kwid) == 0)
+				break;
+			continue;
+		}
+		++n;
+	}
+
+	if (kwm.sym == GOT_KEYWORD_DESCENDANT) {
+		n = 0;
+
+		STAILQ_FOREACH(qid, &commits, entry) {
+			if (qid == STAILQ_LAST(&commits, got_object_qid, entry)
+			    || n == kwm.n)
+				break;
+			++n;
+		}
+
+		memcpy(&iter_id, &qid->id, sizeof(iter_id));
+
+	}
+
+	free(kwid_str);
+	err = got_object_id_str(&kwid_str, &iter_id);
+
+done:
+	free(kwid);
+	free(head_id);
+	got_ref_list_free(&refs);
+	got_object_id_queue_free(&commits);
+	if (graph != NULL)
+		got_commit_graph_close(graph);
+
+	if (err != NULL) {
+		free(kwid_str);
+		return err;
+	}
+
+	*ret = kwid_str;
+	return NULL;
+}
+
+static const struct got_error *
 cmd_update(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
@@ -3565,11 +3785,24 @@ cmd_update(int argc, char *argv[])
 			goto done;
 	} else {
 		struct got_reflist_head refs;
+		char *keyword_idstr = NULL;
+
 		TAILQ_INIT(&refs);
+
 		error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name,
 		    NULL);
 		if (error)
 			goto done;
+
+		error = keyword_to_idstr(&keyword_idstr, commit_id_str, repo,
+		    worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL) {
+			free(commit_id_str);
+			commit_id_str = keyword_idstr;
+		}
+
 		error = got_repo_match_object_id(&commit_id, NULL,
 		    commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo);
 		got_ref_list_free(&refs);
@@ -4730,8 +4963,18 @@ cmd_log(int argc, char *argv[])
 			goto done;
 		got_object_commit_close(commit);
 	} else {
+		char *keyword_idstr = NULL;
+
+		error = keyword_to_idstr(&keyword_idstr, start_commit, repo,
+		    worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL)
+			start_commit = keyword_idstr;
+
 		error = got_repo_match_object_id(&start_id, NULL,
 		    start_commit, GOT_OBJ_TYPE_COMMIT, &refs, repo);
+		free(keyword_idstr);
 		if (error != NULL)
 			goto done;
 	}
@@ -5232,13 +5475,24 @@ cmd_diff(int argc, char *argv[])
 		if (error)
 			goto done;
 		for (i = 0; i < (ncommit_args > 0 ? ncommit_args : argc); i++) {
-			const char *arg;
+			const char	*arg;
+			char		*keyword_idstr = NULL;
+
 			if (ncommit_args > 0)
 				arg = commit_args[i];
 			else
 				arg = argv[i];
+
+			error = keyword_to_idstr(&keyword_idstr, arg, repo,
+			    worktree);
+			if (error != NULL)
+				goto done;
+			if (keyword_idstr != NULL)
+				arg = keyword_idstr;
+
 			error = got_repo_match_object_id(&ids[i], &labels[i],
 			    arg, obj_type, &refs, repo);
+			free(keyword_idstr);
 			if (error) {
 				if (error->code != GOT_ERR_NOT_REF &&
 				    error->code != GOT_ERR_NO_OBJ)
blob - f8c6bfd3e10294f056b420f98add07f8c8d0dd33
blob + fa848632bdd2c520f0e5186a325f388de3cc8c4b
--- regress/cmdline/common.sh
+++ regress/cmdline/common.sh
@@ -140,6 +140,12 @@ git_commit_tree()
 	echo ${id%$pat}
 }
 
+pop_id()
+{
+	shift "$1"
+	printf '%s' "${1:-index-out-of-bounds}"
+}
+
 git_commit_tree()
 {
 	local repo="$1"
blob - 03e05611a400a2ffc9c91e307d0d5c048fdc6cf3
blob + 504ba3f570d0fdde044054abca4df7672bbb7ca3
--- regress/cmdline/diff.sh
+++ regress/cmdline/diff.sh
@@ -2022,6 +2022,254 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_diff_commit_keywords() {
+	local testroot=`test_init diff_commit_keywords`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	set -A ids "$(git_show_head $testroot/repo)"
+	set -A alpha_ids "$(get_blob_id $testroot/repo "" alpha)"
+	set -A beta_ids "$(get_blob_id $testroot/repo "" beta)"
+
+	for i in `seq 8`; do
+		if [ $(( i % 2 )) -eq 0 ]; then
+			echo "alpha change $i" > "$testroot/wt/alpha"
+		else
+			echo "beta change $i" > "$testroot/wt/beta"
+		fi
+
+		(cd "$testroot/wt" && got ci -m "commit number $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+
+		if [ $(( i % 2 )) -eq 0 ]; then
+			set -- "$alpha_ids" \
+			    "$(get_blob_id $testroot/repo "" alpha)"
+			alpha_ids=$*
+		else
+			set -- "$beta_ids" \
+			    "$(get_blob_id $testroot/repo "" beta)"
+			beta_ids=$*
+		fi
+
+		set -- "$ids" "$(git_show_head $testroot/repo)"
+		ids=$*
+	done
+
+	echo "diff $(pop_id 7 $ids) $(pop_id 8 $ids)" > \
+	    $testroot/stdout.expected
+	echo "commit - $(pop_id 7 $ids)" >> $testroot/stdout.expected
+	echo "commit + $(pop_id 8 $ids)" >> $testroot/stdout.expected
+	echo "blob - $(pop_id 4 $beta_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 5 $beta_ids)" >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ beta' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-beta change 5' >> $testroot/stdout.expected
+	echo '+beta change 7' >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got diff -cHEAD- > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "diff failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got update -cHEAD-6 > /dev/null)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "diff $(pop_id 1 $ids) $(pop_id 2 $ids)" > \
+	    $testroot/stdout.expected
+	echo "commit - $(pop_id 1 $ids)" >> $testroot/stdout.expected
+	echo "commit + $(pop_id 2 $ids)" >> $testroot/stdout.expected
+	echo "blob - $(pop_id 1 $beta_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 2 $beta_ids)" >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ beta' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-beta' >> $testroot/stdout.expected
+	echo '+beta change 1' >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got diff -cBASE- > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "diff failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "diff $(pop_id 3 $ids) $(pop_id 4 $ids)" > \
+	    $testroot/stdout.expected
+	echo "commit - $(pop_id 3 $ids)" >> $testroot/stdout.expected
+	echo "commit + $(pop_id 4 $ids)" >> $testroot/stdout.expected
+	echo "blob - $(pop_id 2 $beta_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 3 $beta_ids)" >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ beta' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-beta change 1' >> $testroot/stdout.expected
+	echo '+beta change 3' >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got diff -cBASE+ > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "diff failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# if modifier extends beyond HEAD, we should use HEAD ref
+	echo "diff $(pop_id 8 $ids) $(pop_id 9 $ids)" > \
+	    $testroot/stdout.expected
+	echo "commit - $(pop_id 8 $ids)" >> $testroot/stdout.expected
+	echo "commit + $(pop_id 9 $ids)" >> $testroot/stdout.expected
+	echo "blob - $(pop_id 4 $alpha_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 5 $alpha_ids)" >> $testroot/stdout.expected
+	echo '--- alpha' >> $testroot/stdout.expected
+	echo '+++ alpha' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha change 6' >> $testroot/stdout.expected
+	echo '+alpha change 8' >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got diff -cBASE+20 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "diff failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "diff $(pop_id 3 $ids) $(pop_id 9 $ids)" > \
+	    $testroot/stdout.expected
+	echo "commit - $(pop_id 3 $ids)" >> $testroot/stdout.expected
+	echo "commit + $(pop_id 9 $ids)" >> $testroot/stdout.expected
+	echo "blob - $(pop_id 2 $alpha_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 5 $alpha_ids)" >> $testroot/stdout.expected
+	echo '--- alpha' >> $testroot/stdout.expected
+	echo '+++ alpha' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha change 2' >> $testroot/stdout.expected
+	echo '+alpha change 8' >> $testroot/stdout.expected
+	echo "blob - $(pop_id 2 $beta_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 5 $beta_ids)" >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ beta' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-beta change 1' >> $testroot/stdout.expected
+	echo '+beta change 7' >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got diff -cBASE -cHEAD > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "diff failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "diff $(pop_id 6 $ids) $(pop_id 8 $ids)" > \
+	    $testroot/stdout.expected
+	echo "commit - $(pop_id 6 $ids)" >> $testroot/stdout.expected
+	echo "commit + $(pop_id 8 $ids)" >> $testroot/stdout.expected
+	echo "blob - $(pop_id 3 $alpha_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 4 $alpha_ids)" >> $testroot/stdout.expected
+	echo '--- alpha' >> $testroot/stdout.expected
+	echo '+++ alpha' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha change 4' >> $testroot/stdout.expected
+	echo '+alpha change 6' >> $testroot/stdout.expected
+	echo "blob - $(pop_id 4 $beta_ids)" >> $testroot/stdout.expected
+	echo "blob + $(pop_id 5 $beta_ids)" >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ beta' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-beta change 5' >> $testroot/stdout.expected
+	echo '+beta change 7' >> $testroot/stdout.expected
+
+	got diff -r "$testroot/repo" -cHEAD-3 -cHEAD-1 > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "diff failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "'-c BASE' requires work tree" > "$testroot/stderr.expected"
+
+	got diff -r "$testroot/repo" -cBASE -cHEAD 2> $testroot/stderr
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_diff_basic
 run_test test_diff_shows_conflict
@@ -2041,3 +2289,4 @@ run_test test_diff_path_in_root_commit
 run_test test_diff_file_to_dir
 run_test test_diff_dir_to_file
 run_test test_diff_path_in_root_commit
+run_test test_diff_commit_keywords
blob - aa6a545fe216654b76ca23bbe186211d313c59a2
blob + b1e41502d904314c64204bc13d48f0eae5456e7f
--- regress/cmdline/log.sh
+++ regress/cmdline/log.sh
@@ -900,6 +900,109 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_log_commit_keywords() {
+	local testroot=$(test_init log_commit_keywords)
+	local commit_time=`git_show_author_time $testroot/repo`
+	local d=`date -u -r $commit_time +"%G-%m-%d"`
+
+	set -A ids "$(git_show_head $testroot/repo)"
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for i in $(seq 16); do
+		echo "alpha change $i" > "$testroot/wt/alpha"
+
+		(cd "$testroot/wt" && got ci -m "commit number $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+		set -- "$ids" "$(git_show_head $testroot/repo)"
+		ids=$*
+	done
+
+	for i in $(seq 16 2); do
+		printf '%s %.7s commit number %s\n' \
+		    "$d" $(pop_id $i $ids) "$(( i-1 ))" \
+		    >> $testroot/stdout.expected
+	done
+
+	got log -r "$testroot/repo" -scHEAD- -l15 > $testroot/stdout
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got update -cHEAD-8 > /dev/null)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo -n > "$testroot/stdout.expected"
+
+	for i in $(seq 9 2); do
+		printf '%s %.7s commit number %s\n' \
+		    "$d" $(pop_id $i $ids) "$(( i-1 ))" \
+		    >> $testroot/stdout.expected
+	done
+	printf '%s %.7s adding the test tree\n' "$d" $(pop_id 1 $ids) >> \
+	    $testroot/stdout.expected
+
+	(cd $testroot/wt && got log -scBASE > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# if + modifier is too great, use HEAD commit
+	printf '%s %-7s commit number %s\n' "$d" master 16 > \
+	    $testroot/stdout.expected
+	printf '%s %.7s commit number %s\n' "$d" $(pop_id 16 $ids) 15 >> \
+	    $testroot/stdout.expected
+
+	(cd $testroot/wt && got log -scBASE+20 -l2 > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# if - modifier is too great, use root commit
+	printf '%s %.7s adding the test tree\n' "$d" $(pop_id 1 $ids) > \
+	    $testroot/stdout.expected
+
+	(cd $testroot/wt && got log -scBASE-10 > $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_log_in_repo
 run_test test_log_in_bare_repo
@@ -916,3 +1019,4 @@ run_test test_log_diffstat
 run_test test_log_changed_paths
 run_test test_log_submodule
 run_test test_log_diffstat
+run_test test_log_commit_keywords
blob - c08237c6cd1b14453b8e4f8e39789a3c6c017f1c
blob + 81c278e72e90c473d1b05b26e3a71704b6ae623f
--- regress/cmdline/update.sh
+++ regress/cmdline/update.sh
@@ -3230,6 +3230,229 @@ test_parseargs "$@"
 	test_done "$testroot" 0
 }
 
+test_update_commit_keywords() {
+	local testroot=`test_init update_commit_keywords`
+
+	set -A ids "$(git_show_head $testroot/repo)"
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for i in `seq 8`; do
+		if [ $(( i % 2 )) -eq 0 ]; then
+			echo "alpha change $i" > "$testroot/wt/alpha"
+		else
+			echo "beta change $i" > "$testroot/wt/beta"
+		fi
+
+		(cd "$testroot/wt" && got ci -m "commit number $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+		set -- "$ids" "$(git_show_head $testroot/repo)"
+		ids=$*
+	done
+
+	echo "U  alpha" > $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: " >> $testroot/stdout.expected
+	echo $(pop_id 8 $ids) >> "$testroot/stdout.expected"
+
+	(cd $testroot/wt && got update -cHEAD- > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "alpha change 6" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "U  alpha" > $testroot/stdout.expected
+	echo "U  beta" >> $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: " >> $testroot/stdout.expected
+	echo $(pop_id 2 $ids) >> "$testroot/stdout.expected"
+
+	(cd $testroot/wt && got update -cBASE-6 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "beta change 1" > $testroot/content.expected
+	cat $testroot/wt/beta > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "U  alpha" > $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: " >> $testroot/stdout.expected
+	echo $(pop_id 3 $ids) >> "$testroot/stdout.expected"
+
+	(cd $testroot/wt && got update -cBASE+ > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "alpha change 2" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "U  alpha" > $testroot/stdout.expected
+	echo "U  beta" >> $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: " >> $testroot/stdout.expected
+	echo $(pop_id 7 $ids) >> "$testroot/stdout.expected"
+
+	(cd $testroot/wt && got update -cHEAD-2 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "alpha change 6" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# if - modifier is too great, use root commit
+	echo "U  alpha" > $testroot/stdout.expected
+	echo "U  beta" >> $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: " >> $testroot/stdout.expected
+	echo $(pop_id 1 $ids) >> "$testroot/stdout.expected"
+
+	(cd $testroot/wt && got update -cBASE-20 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "alpha" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# if + modifier is too great, use HEAD commit
+	echo "U  alpha" > $testroot/stdout.expected
+	echo "U  beta" >> $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: " >> $testroot/stdout.expected
+	echo $(pop_id 9 $ids) >> "$testroot/stdout.expected"
+
+	(cd $testroot/wt && got update -cHEAD+10 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "update failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "alpha change 8" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_update_basic
 run_test test_update_adds_file
@@ -3278,3 +3501,4 @@ run_test test_update_umask
 run_test test_update_quiet
 run_test test_update_binary_file
 run_test test_update_umask
+run_test test_update_commit_keywords

-- 
Mark Jamsek <https://bsdbox.org>
GPG: F2FF 13DE 6A06 C471 CA80  E6E2 2930 DC66 86EE CF68