Commit Diff


commit - a290e4b40845ee5d251cdf5e4d9b31693f37ea49
commit + 3795e2b655387a472683ffdeb63c6ff53b9b1e14
blob - 086e9968300ec8161ed6529d40471881657f0b98
blob + 490b0fc0306064290fd3f914cb132d0add57258c
--- got/got.1
+++ got/got.1
@@ -1191,6 +1191,46 @@ The expected argument is a commit ID SHA1 hash or an e
 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 @@ The expected argument is a commit ID SHA1 hash or an e
 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
@@ -1276,10 +1356,48 @@ The reference with the specified
 .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
@@ -1520,6 +1638,46 @@ argument is a commit ID SHA1 hash or an existing refer
 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 +3633,46 @@ The expected argument is a commit ID SHA1 hash or an e
 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 - a6d84137e09637ed152c2663015d87771e4be8d3
blob + 722e3f26795dd133e885ae9c4d0ddd32ae4ef4b8
--- got/got.c
+++ got/got.c
@@ -5720,7 +5720,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;
@@ -5839,11 +5839,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);
@@ -5936,6 +5945,7 @@ cmd_blame(int argc, char *argv[])
 	    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);
@@ -6111,7 +6121,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;
@@ -6238,11 +6248,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);
@@ -6263,6 +6282,7 @@ cmd_tree(int argc, char *argv[])
 	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);
@@ -6584,7 +6604,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
@@ -6723,6 +6743,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:
@@ -6742,6 +6770,7 @@ done:
 	}
 	free(cwd);
 	free(repo_path);
+	free(keyword_idstr);
 	return error;
 }
 
@@ -7628,7 +7657,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;
@@ -7813,7 +7842,13 @@ cmd_tag(int argc, char *argv[])
 			error = got_object_id_str(&commit_id_str, commit_id);
 			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) {
@@ -14042,6 +14077,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;
@@ -14103,9 +14139,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;
+			}
 		}
 	}
 
@@ -14128,7 +14166,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);
@@ -14192,6 +14241,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
@@ -982,10 +982,139 @@ EOF
 	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_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"
@@ -1005,3 +1134,4 @@ run_test test_blame_added_on_branch
 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 + 4b561d845788314e3f478551421310e1934634f2
--- regress/cmdline/cat.sh
+++ regress/cmdline/cat.sh
@@ -331,10 +331,107 @@ test_cat_symlink() {
 	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_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"
 }
 
@@ -344,3 +441,4 @@ 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 - b86a51bf2a102808072428b8f575dc6a449b6769
blob + 40d49081040f0fd5396d77e82ad19b2f5c960858
--- regress/cmdline/ref.sh
+++ regress/cmdline/ref.sh
@@ -438,7 +438,86 @@ test_ref_list() {
 	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
blob - 20ccfdcdf9a3a29fed86ad247da1be233bf3945a
blob + 58cc28c89680ed685a25bf71155506a5c55715a3
--- regress/cmdline/tag.sh
+++ regress/cmdline/tag.sh
@@ -462,7 +462,110 @@ test_tag_create_ssh_signed_missing_key() {
 	ret=$?
 	if [ $ret -ne 0 ]; then
 		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+	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"
 }
 
@@ -472,3 +575,4 @@ 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_tree_submodule_of_same_repo() {
 	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