"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:
Omar Polo <op@omarpolo.com>
Cc:
Stefan Sperling <stsp@stsp.name>, gameoftrees@openbsd.org
Date:
Wed, 19 Jul 2023 00:00:57 +1000

Download raw body.

Thread
Omar Polo <op@omarpolo.com> wrote:
> On 2023/07/18 22:20:47 +1000, Mark Jamsek <mark@jamsek.com> wrote:
> > 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.
> 
> reads fine, ok for me

Thanks, op!

> > 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.
> 
> full ISO8601 is quite large and probably not really useful in
> practice.  YYYYMMDD is used in other parts of the got UI and should be
> fine IMHO.
> 
> I've never used CVS date specifications so I'm not sure how much "last
> friday" or "a fortnigh ago" is really useful in practice.  (have to
> admit that the _seem_ to be useful.)

I agree with you; I think YYYYMMDD should suffice--at least for the time
being. As you mention, this format will also be consistent with other
bits like got's and tog's log date field, so it will be easy to
cross-reference when building commands.

While the relative terms do indeed sound like they might be useful, I'm
not sure how much I'd use them in practice. More frequent cvs users
would have a more informed opinion of their utility compared to me,
though, so I'm happy to defer to their better judgement.

Here's the diff adding keyword support to 'got ref', I've rebased it and
the above diff you've already reviewed on top of main:


-----------------------------------------------
commit c7fbe39e782036117bb95e46766f600b9f49c5c8 (main)
from: Mark Jamsek <mark@jamsek.dev>
date: Tue Jul 18 13:57:41 2023 UTC
 
 add commit keywords to 'got ref' command
 
 With this, all Got commands that take a <commit> option argument
 or operand now support keywords.
 
 M  got/got.1               |  40+  2-
 M  got/got.c               |  10+  1-
 M  regress/cmdline/ref.sh  |  79+  0-

3 files changed, 129 insertions(+), 3 deletions(-)

diff 4373213e55d98f170d7251838f9ec1ebe3626f2d c7fbe39e782036117bb95e46766f600b9f49c5c8
commit - 4373213e55d98f170d7251838f9ec1ebe3626f2d
commit + c7fbe39e782036117bb95e46766f600b9f49c5c8
blob - d7d91763a0a7f459379b8ea104739e7c38f7eba5
blob + 490b0fc0306064290fd3f914cb132d0add57258c
--- got/got.1
+++ got/got.1
@@ -1356,10 +1356,48 @@ The expected
 .Ar name
 will point at the specified
 .Ar object .
+.Pp
 The expected
 .Ar object
-argument is a ID SHA1 hash or an existing reference or tag name which will
-be resolved to the ID of a corresponding commit, tree, tag, or blob object.
+argument is an ID SHA1 hash or an existing reference or tag name
+or a keyword which will be resolved to the ID of a corresponding commit,
+tree, tag, or blob object.
+An abbreviated hash argument will be expanded to a full SHA1 hash
+automatically, provided the abbreviation is unique.
+The keywords
+.Qq :base
+and
+.Qq :head
+resolve to 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 reference names 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 tagged:-3
+will denote the 3rd generation ancestor of the commit resolved by the
+.Qq tagged
+reference.
+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
 Cannot be used together with any other options except
 .Fl r .
 .It Fl d
blob - 63e6b694eb21da3f0d1d6c5489c8f548efaee615
blob + d762fe811564e7d6320b7826fb5a9680e3900517
--- got/got.c
+++ got/got.c
@@ -6605,7 +6605,7 @@ cmd_ref(int argc, char *argv[])
 	char *cwd = NULL, *repo_path = NULL;
 	int ch, do_list = 0, do_delete = 0, sort_by_time = 0;
 	const char *obj_arg = NULL, *symref_target= NULL;
-	char *refname = NULL;
+	char *refname = NULL, *keyword_idstr = NULL;
 	int *pack_fds = NULL;
 
 #ifndef PROFILE
@@ -6744,6 +6744,14 @@ cmd_ref(int argc, char *argv[])
 	else {
 		if (obj_arg == NULL)
 			usage_ref();
+
+		error = got_keyword_to_idstr(&keyword_idstr, obj_arg,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL)
+			obj_arg = keyword_idstr;
+
 		error = add_ref(repo, refname, obj_arg);
 	}
 done:
@@ -6763,6 +6771,7 @@ done:
 	}
 	free(cwd);
 	free(repo_path);
+	free(keyword_idstr);
 	return error;
 }
 
blob - b86a51bf2a102808072428b8f575dc6a449b6769
blob + 25a0030729467b7d7fd8a9e364331c723db3a4e6
--- regress/cmdline/ref.sh
+++ regress/cmdline/ref.sh
@@ -438,7 +438,86 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_ref_commit_keywords() {
+	local testroot=$(test_init ref_commit_keywords)
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+
+	got checkout "$repo" "$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
+		echo "alpha change $i" > "$wt/alpha"
+
+		(cd "$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 $repo)"
+		ids=$*
+	done
+
+	$(cd "$wt" && got ref -c:head:-4 refs/heads/head-4)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got ref command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd "$wt" && got up -c head-4 > /dev/null)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	$(cd "$wt" && got ref -c:base:+2 refs/heads/base+2)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got ref command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd "$wt" && got ref -cmaster:- refs/heads/master-)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got ref command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		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 ": $(pop_id 4 $ids)" >> $testroot/stdout.expected
+	echo "refs/heads/base+2: $(pop_id 6 $ids)" >> $testroot/stdout.expected
+	echo "refs/heads/head-4: $(pop_id 4 $ids)" >> $testroot/stdout.expected
+	echo "refs/heads/master: $(pop_id 8 $ids)" >> $testroot/stdout.expected
+	echo "refs/heads/master-: $(pop_id 7 $ids)" >> $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_ref_create
 run_test test_ref_delete
 run_test test_ref_list
+run_test test_ref_commit_keywords

-----------------------------------------------
commit 4373213e55d98f170d7251838f9ec1ebe3626f2d
from: Mark Jamsek <mark@jamsek.dev>
date: Tue Jul 18 13:57:41 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.
 
 ok op@
 
 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 6b483b319568902f913006779dad34b89cb4c979 4373213e55d98f170d7251838f9ec1ebe3626f2d
commit - 6b483b319568902f913006779dad34b89cb4c979
commit + 4373213e55d98f170d7251838f9ec1ebe3626f2d
blob - 086e9968300ec8161ed6529d40471881657f0b98
blob + d7d91763a0a7f459379b8ea104739e7c38f7eba5
--- got/got.1
+++ got/got.1
@@ -1191,6 +1191,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
@@ -1238,6 +1278,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
@@ -1520,6 +1600,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
@@ -3475,6 +3595,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