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

From:
Mark Jamsek <mark@jamsek.com>
Subject:
Re: expand keyword support to more commands
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Tue, 18 Jul 2023 22:20:47 +1000

Download raw body.

Thread
Stefan Sperling <stsp@stsp.name> wrote:
> On Tue, Jul 18, 2023 at 12:38:14AM +1000, Mark Jamsek wrote:
> > Mark Jamsek <mark@jamsek.com> wrote:
> > > This adds commit keywords to backout, branch, checkout, cherrypick, and
> > > patch. I've added some basic tests for the new commands, but also added
> > > some less trivial test cases to log regress.
> > > 
> > > The special case here is 'got checkout' as it creates a work tree based
> > > on either the repository's HEAD or the head of the branch specified with
> > > -b; therefore, irrespective of whether -b is used, the :base and :head
> > > keywords are equivalent as the work tree's base commit will always be
> > > the head of the checked-out branch.
> > > 
> > > I don't think this is a problem, but worth pointing out. Please see how
> > > I've noted this in the manual.
> > > 
> > > Of these new additions, this is especially useful for both the backout
> > > and cherrypick commands, but I think they will also get good use with
> > > the branch command too. I still want to add support to the blame, cat,
> > > tag, and tree commands but I've run out of time tonight so will do it
> > > tomorrow. It's trivial to make the change in got.c, but I want to have
> > > tests for them. And I would also like to support keywords in tog too;
> > > it will be nice to have that consistency across both UIs.
> > > 
> > 
> > I had a case of using HEAD to refer to both the repository and
> > work tree branch head; revised diff below:
> 
> Reads fine to me, ok.

Here's the diff for adding commit keywords to the blame, cat, tag, and
tree commands, which includes some basic test coverage too.

To help review, the man page bits are literal copypastas of the other
keyword paragraphs with a slight tweak to example reference names.

I forgot about the ref command, which I'll do next, but that's all that
remains in got :)

Before starting on temporal expressions, do we want to support various
formats (e.g., relative terms like yesterday, last month), or keep it
simple and stick to perhaps YYYYMMDD or full ISO 8601? We can always
grow the supported keywords, but it would be good to have an idea of
what we might want to allow. The initial plan was to start with
YYYYMMDD and take it from there.


-----------------------------------------------
commit 55b9b1a0275498b141f1f0eb9560f13958ef23eb (main)
from: Mark Jamsek <mark@jamsek.dev>
date: Tue Jul 18 11:54:32 2023 UTC
 
 got: enable more commands to accept commit keywords
 
 More work adding commit keyword support to the blame, cat, tag, and tree
 commands. Just the ref command remains in got before adding more expressions,
 and introducing keywords to tog.
 
 M  got/got.1                 |  160+  0-
 M  got/got.c                 |   48+  7-
 M  regress/cmdline/blame.sh  |  130+  0-
 M  regress/cmdline/cat.sh    |   98+  0-
 M  regress/cmdline/tag.sh    |  104+  0-
 M  regress/cmdline/tree.sh   |  123+  0-

6 files changed, 663 insertions(+), 7 deletions(-)

diff c8d1a97c8f1deb9cb6fae25376d41d8549d28de4 55b9b1a0275498b141f1f0eb9560f13958ef23eb
commit - c8d1a97c8f1deb9cb6fae25376d41d8549d28de4
commit + 55b9b1a0275498b141f1f0eb9560f13958ef23eb
blob - 6850047d13f8afebd1c2166d9b5f9d3e689eab3b
blob + 4f7c4da31597ec9d46d0d8637b8be5d257cf6855
--- got/got.1
+++ got/got.1
@@ -1196,6 +1196,46 @@ 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.
+.Pp
+The special
+.Ar commit
+keywords
+.Qq :base
+and
+.Qq :head
+can also be used to represent the work tree's base commit
+and branch head, 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 and references may be appended with
+.Qq :+
+or
+.Qq :-
+modifiers and an optional integer N to denote the
+Nth descendant or antecedent by first parent traversal, respectively;
+for example,
+.Sy :head:-2
+denotes the work tree branch head's 2nd generation ancestor, and
+.Sy :base:+4
+denotes the 4th generation descendant of the work tree's base commit.
+Similarly,
+.Sy xyz:-5
+will denote the 5th generation ancestor of the commit resolved by the
+.Qq xyz
+reference.
+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 .
 .It Fl r Ar repository-path
 Use the repository at the specified path.
 If not specified, assume the repository is located at or above the current
@@ -1243,6 +1283,46 @@ 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.
+.Pp
+The special
+.Ar commit
+keywords
+.Qq :base
+and
+.Qq :head
+can also be used to represent the work tree's base commit
+and branch head, 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 and references may be appended with
+.Qq :+
+or
+.Qq :-
+modifiers and an optional integer N to denote the
+Nth descendant or antecedent by first parent traversal, respectively;
+for example,
+.Sy :head:-2
+denotes the work tree branch head's 2nd generation ancestor, and
+.Sy :base:+4
+denotes the 4th generation descendant of the work tree's base commit.
+Similarly,
+.Sy spam:-3
+will denote the 3rd generation ancestor of the commit resolved by the
+.Qq spam
+reference.
+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 .
 .It Fl i
 Show object IDs of files (blob objects) and directories (tree objects).
 .It Fl R
@@ -1529,6 +1609,46 @@ automatically, provided the abbreviation is unique.
 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.
+.Pp
+The special
+.Ar commit
+keywords
+.Qq :base
+and
+.Qq :head
+can also be used to represent the work tree's base commit
+and branch head, 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 and references may be appended with
+.Qq :+
+or
+.Qq :-
+modifiers and an optional integer N to denote the
+Nth descendant or antecedent by first parent traversal, respectively;
+for example,
+.Sy :head:-2
+denotes the work tree branch head's 2nd generation ancestor, and
+.Sy :base:+4
+denotes the 4th generation descendant of the work tree's base commit.
+Similarly,
+.Sy eggs:-3
+will denote the 3rd generation ancestor of the commit resolved by the
+.Qq eggs
+reference.
+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 .
 .It Fl l
 List all existing tags in the repository instead of creating a new tag.
 If a
@@ -3490,6 +3610,46 @@ 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.
+.Pp
+The special
+.Ar commit
+keywords
+.Qq :base
+and
+.Qq :head
+can also be used to represent the work tree's base commit
+and branch head, 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 and references may be appended with
+.Qq :+
+or
+.Qq :-
+modifiers and an optional integer N to denote the
+Nth descendant or antecedent by first parent traversal, respectively;
+for example,
+.Sy :head:-2
+denotes the work tree branch head's 2nd generation ancestor, and
+.Sy :base:+4
+denotes the 4th generation descendant of the work tree's base commit.
+Similarly,
+.Sy quux:-8
+will denote the 8th generation ancestor of the commit resolved by the
+.Qq quux
+reference.
+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 .
 .It Fl P
 Interpret all arguments as paths only.
 This option can be used to resolve ambiguity in cases where paths
blob - f0cea264f130ba792c3c19b55d2632c8a81f9735
blob + 63e6b694eb21da3f0d1d6c5489c8f548efaee615
--- got/got.c
+++ got/got.c
@@ -5721,7 +5721,7 @@ cmd_blame(int argc, char *argv[])
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	struct got_blob_object *blob = NULL;
-	char *commit_id_str = NULL;
+	char *commit_id_str = NULL, *keyword_idstr = NULL;
 	struct blame_cb_args bca;
 	int ch, obj_type, i, fd1 = -1, fd2 = -1, fd3 = -1;
 	off_t filesize;
@@ -5840,11 +5840,20 @@ cmd_blame(int argc, char *argv[])
 			goto done;
 	} else {
 		struct got_reflist_head refs;
+
 		TAILQ_INIT(&refs);
 		error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name,
 		    NULL);
 		if (error)
 			goto done;
+
+		error = got_keyword_to_idstr(&keyword_idstr, commit_id_str,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL)
+			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);
@@ -5937,6 +5946,7 @@ done:
 	    repo, GOT_DIFF_ALGORITHM_PATIENCE, blame_cb, &bca,
 	    check_cancelled, NULL, fd2, fd3, f1, f2);
 done:
+	free(keyword_idstr);
 	free(in_repo_path);
 	free(link_target);
 	free(repo_path);
@@ -6112,7 +6122,7 @@ cmd_tree(int argc, char *argv[])
 	char *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL;
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
-	char *commit_id_str = NULL;
+	char *commit_id_str = NULL, *keyword_idstr = NULL;
 	int show_ids = 0, recurse = 0;
 	int ch;
 	int *pack_fds = NULL;
@@ -6239,11 +6249,20 @@ cmd_tree(int argc, char *argv[])
 			goto done;
 	} else {
 		struct got_reflist_head refs;
+
 		TAILQ_INIT(&refs);
 		error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name,
 		    NULL);
 		if (error)
 			goto done;
+
+		error = got_keyword_to_idstr(&keyword_idstr, commit_id_str,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL)
+			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);
@@ -6264,6 +6283,7 @@ done:
 	error = print_tree(in_repo_path, commit, show_ids, recurse,
 	    in_repo_path, repo);
 done:
+	free(keyword_idstr);
 	free(in_repo_path);
 	free(repo_path);
 	free(cwd);
@@ -7629,7 +7649,7 @@ cmd_tag(int argc, char *argv[])
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL, *commit_id_str = NULL;
-	char *gitconfig_path = NULL, *tagger = NULL;
+	char *gitconfig_path = NULL, *tagger = NULL, *keyword_idstr = NULL;
 	char *allowed_signers = NULL, *revoked_signers = NULL;
 	const char *signer_id = NULL;
 	const char *tag_name = NULL, *commit_id_arg = NULL, *tagmsg = NULL;
@@ -7815,6 +7835,12 @@ cmd_tag(int argc, char *argv[])
 			free(commit_id);
 			if (error)
 				goto done;
+		} else {
+			error = got_keyword_to_idstr(&keyword_idstr,
+			    commit_id_arg, repo, worktree);
+			if (error != NULL)
+				goto done;
+			commit_id_str = keyword_idstr;
 		}
 
 		if (worktree) {
@@ -14043,6 +14069,7 @@ cmd_cat(int argc, char *argv[])
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL, *label = NULL;
+	char *keyword_idstr = NULL;
 	const char *commit_id_str = NULL;
 	struct got_object_id *id = NULL, *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
@@ -14104,9 +14131,11 @@ cmd_cat(int argc, char *argv[])
 				goto done;
 			}
 
-			/* Release work tree lock. */
-			got_worktree_close(worktree);
-			worktree = NULL;
+			if (commit_id_str == NULL) {
+				/* Release work tree lock. */
+				got_worktree_close(worktree);
+				worktree = NULL;
+			}
 		}
 	}
 
@@ -14129,7 +14158,18 @@ cmd_cat(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	if (commit_id_str == NULL)
+	if (commit_id_str != NULL) {
+		error = got_keyword_to_idstr(&keyword_idstr, commit_id_str,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL)
+			commit_id_str = keyword_idstr;
+		if (worktree != NULL) {
+			got_worktree_close(worktree);
+			worktree = NULL;
+		}
+	} else
 		commit_id_str = GOT_REF_HEAD;
 	error = got_repo_match_object_id(&commit_id, NULL,
 	    commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo);
@@ -14193,6 +14233,7 @@ done:
 	free(label);
 	free(id);
 	free(commit_id);
+	free(keyword_idstr);
 	if (commit)
 		got_object_commit_close(commit);
 	if (worktree)
blob - 106ff741cc1c94c5becd70905a3da2249b9dc826
blob + a52ad3f5f3953de8ae74d10f7a9f2b3d9f880cc1
--- regress/cmdline/blame.sh
+++ regress/cmdline/blame.sh
@@ -991,6 +991,135 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_blame_commit_keywords() {
+	local testroot=$(test_init blame_commit_keywords)
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+	local id=$(git_show_head "$repo")
+
+	set -A ids "$(trim_obj_id 32 $id)"
+
+	# :base requires work tree
+	echo "got: '-c :base' requires work tree" > "$testroot/stderr.expected"
+	got blame -r "$repo" -c:base alpha 2> "$testroot/stderr"
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "blame command succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s "$testroot/stderr.expected" "$testroot/stderr"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/stderr.expected" "$testroot/stderr"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got checkout "$repo" "$wt" > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo -n > "$wt/alpha"
+
+	for i in $(seq 8); do
+		echo "change $i" >> "$wt/alpha"
+
+		(cd "$wt" && got ci -m "commit $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+
+		id=$(git_show_head "$repo")
+		set -- "$ids" "$(trim_obj_id 32 $id)"
+		ids=$*
+	done
+
+	local author_time=$(git_show_author_time "$repo")
+	local d=$(date -u -r $author_time +"%G-%m-%d")
+
+	got blame -r "$repo" -c:head:-8 alpha > "$testroot/stdout"
+	echo "1) $(pop_id 1 $ids) $d $GOT_AUTHOR_8 alpha" > \
+	    "$testroot/stdout.expected"
+
+	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 "$wt" && got blame -cmaster:-5 alpha > "$testroot/stdout")
+
+	echo "1) $(pop_id 2 $ids) $d $GOT_AUTHOR_8 change 1" > \
+	    "$testroot/stdout.expected"
+	echo "2) $(pop_id 3 $ids) $d $GOT_AUTHOR_8 change 2" >> \
+	    "$testroot/stdout.expected"
+	echo "3) $(pop_id 4 $ids) $d $GOT_AUTHOR_8 change 3" >> \
+	    "$testroot/stdout.expected"
+
+	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 "$wt" && got blame -c:head:-4 alpha > "$testroot/stdout")
+
+	echo "1) $(pop_id 2 $ids) $d $GOT_AUTHOR_8 change 1" > \
+	    "$testroot/stdout.expected"
+	echo "2) $(pop_id 3 $ids) $d $GOT_AUTHOR_8 change 2" >> \
+	    "$testroot/stdout.expected"
+	echo "3) $(pop_id 4 $ids) $d $GOT_AUTHOR_8 change 3" >> \
+	    "$testroot/stdout.expected"
+	echo "4) $(pop_id 5 $ids) $d $GOT_AUTHOR_8 change 4" >> \
+	    "$testroot/stdout.expected"
+
+	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 "$wt" && got up -c:head:-8 > /dev/null)
+	(cd "$wt" && got blame -c:base:+5 alpha > "$testroot/stdout")
+
+	echo "1) $(pop_id 2 $ids) $d $GOT_AUTHOR_8 change 1" > \
+	    "$testroot/stdout.expected"
+	echo "2) $(pop_id 3 $ids) $d $GOT_AUTHOR_8 change 2" >> \
+	    "$testroot/stdout.expected"
+	echo "3) $(pop_id 4 $ids) $d $GOT_AUTHOR_8 change 3" >> \
+	    "$testroot/stdout.expected"
+	echo "4) $(pop_id 5 $ids) $d $GOT_AUTHOR_8 change 4" >> \
+	    "$testroot/stdout.expected"
+	echo "5) $(pop_id 6 $ids) $d $GOT_AUTHOR_8 change 5" >> \
+	    "$testroot/stdout.expected"
+
+	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
+
+	blame_cmp "$testroot" "alpha"
+	ret=$?
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_blame_basic
 run_test test_blame_tag
@@ -1005,3 +1134,4 @@ run_test test_blame_lines_shifted_skip
 run_test test_blame_submodule
 run_test test_blame_symlink
 run_test test_blame_lines_shifted_skip
+run_test test_blame_commit_keywords
blob - 2e37e870ce0e06673c3f701e940ebdd2438b8c38
blob + 050c09307ac69c50f17c243968c77d344f8cf5ac
--- regress/cmdline/cat.sh
+++ regress/cmdline/cat.sh
@@ -338,9 +338,107 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_cat_commit_keywords() {
+	local testroot=$(test_init cat_commit_keywords)
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+
+	# :base requires work tree
+	echo "got: '-c :base' requires work tree" > "$testroot/stderr.expected"
+	got cat -r "$repo" -c:base alpha 2> "$testroot/stderr"
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "cat command succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s "$testroot/stderr.expected" "$testroot/stderr"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/stderr.expected" "$testroot/stderr"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got checkout "$repo" "$wt" > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for i in $(seq 8); do
+		echo "change $i" > "$wt/alpha"
+		echo "delta $i" > "$wt/gamma/delta"
+
+		(cd "$wt" && got ci -m "commit $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+
+		local delta_id=$(got tree -r "$repo" -i gamma | \
+		    grep 'delta$' | cut -d' ' -f 1)
+		set -- "$delta_ids" "$delta_id"
+		delta_ids=$*
+	done
+
+	# cat blob by path
+	echo "change 6" > "$testroot/stdout.expected"
+	$(cd "$wt" && got cat -c:head:-2 alpha > "$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
+
+	# cat blob by path with -r repo
+	echo "delta 7" > "$testroot/stdout.expected"
+	got cat -r "$repo" -c:head:- gamma/delta > "$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
+
+	# cat tree by path
+	echo "$(pop_id 4 $delta_ids) 0100644 delta" > \
+	    "$testroot/stdout.expected"
+	$(cd "$wt" && got cat -c:base:-4 gamma > "$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
+
+	# cat blob by path with -P
+	echo "delta 4" > "$testroot/stdout.expected"
+	$(cd "$wt" && got up -c:base:-8 > /dev/null)
+	$(cd "$wt" && got cat -c:base:+4 -P gamma/delta > "$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
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_cat_basic
 run_test test_cat_path
 run_test test_cat_submodule
 run_test test_cat_submodule_of_same_repo
 run_test test_cat_symlink
+run_test test_cat_commit_keywords
blob - 20ccfdcdf9a3a29fed86ad247da1be233bf3945a
blob + 58cc28c89680ed685a25bf71155506a5c55715a3
--- regress/cmdline/tag.sh
+++ regress/cmdline/tag.sh
@@ -466,9 +466,113 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_tag_commit_keywords() {
+	local testroot=$(test_init tag_commit_keywords)
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+	local commit_id=$(git_show_head "$repo")
+	local tag=1.0.0
+	local tag2=2.0.0
+
+	echo "alphas" > "$repo/alpha"
+	git_commit "$repo" -m "alphas"
+
+	# create tag based on first gen ancestor of the repository's HEAD
+	got tag -m 'v1.0.0' -r "$repo" -c:head:- "$tag" > "$testroot/stdout"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got ref command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_id=$(got ref -r "$repo" -l \
+	    | grep "^refs/tags/$tag" | tr -d ' ' | cut -d: -f2)
+	echo "Created tag $tag_id" > "$testroot/stdout.expected"
+	cmp -s "$testroot/stdout" "$testroot/stdout.expected"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/stdout.expected" "$testroot/stdout"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_commit=$(got cat -r "$repo" "$tag" | grep ^object | cut -d' ' -f2)
+	if [ "$tag_commit" != "$commit_id" ]; then
+		echo "wrong commit was tagged" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	got checkout -c "$tag" "$repo" "$wt" >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create new tag based on the base commit's 2nd gen descendant
+	(cd "$wt" && got up > /dev/null)
+	echo 'foo' > "$wt/alpha"
+	echo 'boo' > "$wt/beta"
+	echo 'hoo' > "$wt/gamma/delta"
+	(cd "$wt" && got commit -m foo alpha > /dev/null)
+	(cd "$wt" && got commit -m boo beta > /dev/null)
+	(cd "$wt" && got commit -m hoo gamma/delta > /dev/null)
+	local head_id=$(git_show_branch_head "$repo")
+	(cd "$wt" && got up -c:base:-2 > /dev/null)
+	local base_id=$(cd "$wt" && got info | grep base | cut -d' ' -f5)
+
+	(cd "$wt" && got tag -m 'v2.0.0' -c:base:+2 $tag2 > "$testroot/stdout")
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_id2=$(got ref -r "$repo" -l \
+	    | grep "^refs/tags/$tag2" | tr -d ' ' | cut -d: -f2)
+	echo "Created tag $tag_id2" > $testroot/stdout.expected
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag2_commit=$(got cat -r "$repo" "$tag2" | grep ^object | cut -d' ' -f2)
+	if [ "$tag2_commit" != "$head_id" ]; then
+		echo "wrong commit was tagged" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "HEAD: refs/heads/master" > $testroot/stdout.expected
+	echo -n "refs/got/worktree/base-" >> $testroot/stdout.expected
+	cat "$wt/.got/uuid" | tr -d '\n' >> $testroot/stdout.expected
+	echo ": $base_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $head_id" >> $testroot/stdout.expected
+	echo "refs/tags/$tag: $tag_id" >> $testroot/stdout.expected
+	echo "refs/tags/$tag2: $tag_id2" >> $testroot/stdout.expected
+
+	got ref -r "$repo" -l > $testroot/stdout
+
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_tag_create
 run_test test_tag_list
 run_test test_tag_list_lightweight
 run_test test_tag_create_ssh_signed
 run_test test_tag_create_ssh_signed_missing_key
+run_test test_tag_commit_keywords
blob - 5fa15d25ce341431bf7f64d2a7236abc04edb893
blob + 62eb987ba62876f4026cd0d830472dd2a3060c20
--- regress/cmdline/tree.sh
+++ regress/cmdline/tree.sh
@@ -146,8 +146,131 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_tree_commit_keywords() {
+	local testroot=$(test_init tree_commit_keywords)
+	local wt="$testroot/wt"
+
+	# :base requires work tree
+	echo "got: '-c :base' requires work tree" > "$testroot/stderr.expected"
+	got tree -r "$testroot/repo" -c:base 2> "$testroot/stderr"
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "tree command succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s "$testroot/stderr.expected" "$testroot/stderr"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/stderr.expected" "$testroot/stderr"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo 'alpha' > $testroot/stdout.expected
+	echo 'beta' >> $testroot/stdout.expected
+	echo 'epsilon/' >> $testroot/stdout.expected
+	echo 'gamma/' >> $testroot/stdout.expected
+
+	got tree -r "$testroot/repo" -c:head > "$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
+
+	got checkout "$testroot/repo" "$wt" > /dev/null
+
+	(
+		cd "$wt"
+		mkdir bing
+		echo "foo" > foo
+		echo "bar" > bar
+		echo "baz" > baz
+		echo "bob" > bing/bob
+		got add foo bar baz bing/bob > /dev/null
+		got commit -m "add foo" foo > /dev/null
+		got commit -m "add bar" bar > /dev/null
+		got commit -m "add baz" baz > /dev/null
+		got commit -m "add bing/bob" > /dev/null
+	)
+
+	echo 'alpha' > $testroot/stdout.expected
+	echo 'beta' >> $testroot/stdout.expected
+	echo 'epsilon/' >> $testroot/stdout.expected
+	echo 'foo' >> $testroot/stdout.expected
+	echo 'gamma/' >> $testroot/stdout.expected
+
+	(cd "$wt" && got tree -c:base:-3 > $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
+
+	echo 'alpha' > $testroot/stdout.expected
+	echo 'bar' >> $testroot/stdout.expected
+	echo 'beta' >> $testroot/stdout.expected
+	echo 'epsilon/' >> $testroot/stdout.expected
+	echo 'foo' >> $testroot/stdout.expected
+	echo 'gamma/' >> $testroot/stdout.expected
+
+	(cd "$wt" && got tree -cmaster:-2 > $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
+
+	echo 'alpha' > $testroot/stdout.expected
+	echo 'bar' >> $testroot/stdout.expected
+	echo 'baz' >> $testroot/stdout.expected
+	echo 'beta' >> $testroot/stdout.expected
+	echo 'epsilon/' >> $testroot/stdout.expected
+	echo 'foo' >> $testroot/stdout.expected
+	echo 'gamma/' >> $testroot/stdout.expected
+
+	(cd "$wt" && got tree -c:head:- > $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
+
+	echo 'alpha' > $testroot/stdout.expected
+	echo 'bar' >> $testroot/stdout.expected
+	echo 'baz' >> $testroot/stdout.expected
+	echo 'beta' >> $testroot/stdout.expected
+	echo 'bing/' >> $testroot/stdout.expected
+	echo 'epsilon/' >> $testroot/stdout.expected
+	echo 'foo' >> $testroot/stdout.expected
+	echo 'gamma/' >> $testroot/stdout.expected
+
+	(cd "$wt" && got up -c:base:-4 > $testroot/stdout)
+	(cd "$wt" && got tree -c:base:+4 > $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
+
+	test_done "$testroot" "0"
+}
+
 test_parseargs "$@"
 run_test test_tree_basic
 run_test test_tree_branch
 run_test test_tree_submodule
 run_test test_tree_submodule_of_same_repo
+run_test test_tree_commit_keywords


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