Commit Diff


commit - 481d9ea65dc2884473253a84cee23e8a562ac3b9
commit + fb8851205d3a6b7f0c457b2091c3b335d0f7e4e3
blob - 88a4286fff9c196d6fcc50a06ce49f886eb81d91
blob + 6850047d13f8afebd1c2166d9b5f9d3e689eab3b
--- got/got.1
+++ got/got.1
@@ -570,6 +570,43 @@ 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, with both resolving to the
+repository's HEAD reference, or, if the
+.Fl b
+option is used, the head of the checked-out
+.Ar branch .
+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 foo:-3
+will denote the 3rd generation ancestor of the commit resolved by the
+.Qq foo
+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 .
 If this option is not specified, the most recent commit on the selected
 branch will be used.
 .Pp
@@ -1364,6 +1401,46 @@ The expected
 .Ar commit
 argument is a commit ID SHA1 hash or an existing reference
 or tag name which will be resolved to a commit ID.
+.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 foobar:+3
+will denote the 3rd generation descendant of the commit resolved by the
+.Qq foobar
+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 d Ar name
 Delete the branch with the specified
 .Ar name
@@ -1687,6 +1764,49 @@ are as follows:
 Attempt to locate files within the specified
 .Ar commit
 for use as a merge-base for 3-way merges.
+The expected
+.Ar commit
+argument is a commit ID SHA1 hash or an existing reference
+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.
+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 flan:+3
+will denote the 3rd generation descendant of the commit resolved by the
+.Qq flan
+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 .
 Ideally, the specified
 .Ar commit
 should contain versions of files which the changes contained in the
@@ -2165,6 +2285,43 @@ should be on a different branch than the work tree's b
 The expected argument is a reference or a commit ID SHA1 hash.
 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.
+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 barbaz:+3
+will denote the 3rd generation descendant of the commit resolved by the
+.Qq barbaz
+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 .
 .Pp
 Show the status of each affected file, using the following status codes:
 .Bl -column YXZ description
@@ -2274,6 +2431,43 @@ The expected argument is a reference or a commit ID SH
 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.
+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 wip:+5
+will denote the 5th generation descendant of the commit resolved by the
+.Qq wip
+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 .
+.Pp
 Show the status of each affected file, using the following status codes:
 .Bl -column YXZ description
 .It G Ta file was merged
blob - c10750066ae9c0758aa2f1f088ec8fc28322b045
blob + a6d84137e09637ed152c2663015d87771e4be8d3
--- got/got.c
+++ got/got.c
@@ -2980,7 +2980,7 @@ cmd_checkout(int argc, char *argv[])
 	char *worktree_path = NULL;
 	const char *path_prefix = "";
 	const char *branch_name = GOT_REF_HEAD, *refname = NULL;
-	char *commit_id_str = NULL;
+	char *commit_id_str = NULL, *keyword_idstr = NULL;
 	struct got_object_id *commit_id = NULL;
 	char *cwd = NULL;
 	int ch, same_path_prefix, allow_nonempty = 0, verbosity = 0;
@@ -3129,7 +3129,17 @@ cmd_checkout(int argc, char *argv[])
 		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) {
+			free(commit_id_str);
+			commit_id_str = keyword_idstr;
+		}
+
 		error = got_repo_match_object_id(&commit_id, NULL,
 		    commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo);
 		got_ref_list_free(&refs);
@@ -6973,7 +6983,7 @@ cmd_branch(int argc, char *argv[])
 	struct got_reference *ref = NULL;
 	struct got_pathlist_head paths;
 	struct got_object_id *commit_id = NULL;
-	char *commit_id_str = NULL;
+	char *commit_id_str = NULL, *keyword_idstr = NULL;;
 	int *pack_fds = NULL;
 
 	TAILQ_INIT(&paths);
@@ -7101,6 +7111,14 @@ cmd_branch(int argc, char *argv[])
 			commit_id_arg = worktree ?
 			    got_worktree_get_head_ref_name(worktree) :
 			    GOT_REF_HEAD;
+		else {
+			error = got_keyword_to_idstr(&keyword_idstr,
+			    commit_id_arg, repo, worktree);
+			if (error != NULL)
+				goto done;
+			if (keyword_idstr != NULL)
+				commit_id_arg = keyword_idstr;
+		}
 		error = got_repo_match_object_id(&commit_id, NULL,
 		    commit_id_arg, GOT_OBJ_TYPE_COMMIT, &refs, repo);
 		got_ref_list_free(&refs);
@@ -7152,6 +7170,7 @@ cmd_branch(int argc, char *argv[])
 		}
 	}
 done:
+	free(keyword_idstr);
 	if (ref)
 		got_ref_close(ref);
 	if (repo) {
@@ -8261,7 +8280,7 @@ cmd_patch(int argc, char *argv[])
 	const char *commit_id_str = NULL;
 	struct stat sb;
 	const char *errstr;
-	char *cwd = NULL;
+	char *cwd = NULL, *keyword_idstr = NULL;
 	int ch, nop = 0, strip = -1, reverse = 0;
 	int patchfd;
 	int *pack_fds = NULL;
@@ -8350,8 +8369,14 @@ cmd_patch(int argc, char *argv[])
 		goto done;
 
 	if (commit_id_str != NULL) {
+		error = got_keyword_to_idstr(&keyword_idstr, commit_id_str,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+
 		error = got_repo_match_object_id(&commit_id, NULL,
-		    commit_id_str, GOT_OBJ_TYPE_COMMIT, &refs, repo);
+		    keyword_idstr != NULL ? keyword_idstr : commit_id_str,
+		    GOT_OBJ_TYPE_COMMIT, &refs, repo);
 		if (error)
 			goto done;
 	}
@@ -8362,6 +8387,7 @@ cmd_patch(int argc, char *argv[])
 	print_patch_progress_stats(&ppa);
 done:
 	got_ref_list_free(&refs);
+	free(keyword_idstr);
 	free(commit_id);
 	if (repo) {
 		close_error = got_repo_close(repo);
@@ -10193,7 +10219,7 @@ cmd_cherrypick(int argc, char *argv[])
 	const struct got_error *error = NULL;
 	struct got_worktree *worktree = NULL;
 	struct got_repository *repo = NULL;
-	char *cwd = NULL, *commit_id_str = NULL;
+	char *cwd = NULL, *commit_id_str = NULL, *keyword_idstr = NULL;
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	struct got_object_qid *pid;
@@ -10273,7 +10299,12 @@ cmd_cherrypick(int argc, char *argv[])
 		goto done;
 	}
 
-	error = got_repo_match_object_id(&commit_id, NULL, argv[0],
+	error = got_keyword_to_idstr(&keyword_idstr, argv[0], repo, worktree);
+	if (error != NULL)
+		goto done;
+
+	error = got_repo_match_object_id(&commit_id, NULL,
+	    keyword_idstr != NULL ? keyword_idstr : argv[0],
 	    GOT_OBJ_TYPE_COMMIT, NULL, repo);
 	if (error)
 		goto done;
@@ -10302,6 +10333,7 @@ cmd_cherrypick(int argc, char *argv[])
 	print_merge_progress_stats(&upa);
 done:
 	free(cwd);
+	free(keyword_idstr);
 	if (commit)
 		got_object_commit_close(commit);
 	free(commit_id_str);
@@ -10335,7 +10367,7 @@ cmd_backout(int argc, char *argv[])
 	const struct got_error *error = NULL;
 	struct got_worktree *worktree = NULL;
 	struct got_repository *repo = NULL;
-	char *cwd = NULL, *commit_id_str = NULL;
+	char *cwd = NULL, *commit_id_str = NULL, *keyword_idstr = NULL;
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	struct got_object_qid *pid;
@@ -10415,7 +10447,12 @@ cmd_backout(int argc, char *argv[])
 		goto done;
 	}
 
-	error = got_repo_match_object_id(&commit_id, NULL, argv[0],
+	error = got_keyword_to_idstr(&keyword_idstr, argv[0], repo, worktree);
+	if (error != NULL)
+		goto done;
+
+	error = got_repo_match_object_id(&commit_id, NULL,
+	    keyword_idstr != NULL ? keyword_idstr : argv[0],
 	    GOT_OBJ_TYPE_COMMIT, NULL, repo);
 	if (error)
 		goto done;
@@ -10448,6 +10485,7 @@ cmd_backout(int argc, char *argv[])
 	print_merge_progress_stats(&upa);
 done:
 	free(cwd);
+	free(keyword_idstr);
 	if (commit)
 		got_object_commit_close(commit);
 	free(commit_id_str);
blob - 86b8cddd9616882e06b859c2bd3e8de87a317150
blob + 76bc77fd0afea94f3494da3f05e3f617d0019853
--- regress/cmdline/backout.sh
+++ regress/cmdline/backout.sh
@@ -533,13 +533,108 @@ test_backout_logmsg_ref() {
 	ymd=`date -u -r $b2_commit_time2 +"%F"`
 	echo "Deleted: $ymd newbranch2 $b2_logmsg2" > $testroot/stdout.expected
 	(cd $testroot/repo && got backout -X > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_backout_commit_keywords() {
+	local testroot=$(test_init backout_commit_keywords)
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "new" > $testroot/wt/new
+	(cd $testroot/wt && got add new > /dev/null)
+	echo "modified alpha" > $testroot/wt/alpha
+	(cd $testroot/wt && got rm epsilon/zeta > /dev/null)
+	(cd $testroot/wt && got commit -m "bad changes" > /dev/null)
+
+	local bad_commit=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got update > /dev/null)
+
+	echo "modified beta" > $testroot/wt/beta
+	(cd $testroot/wt && got commit -m "changing beta" > /dev/null)
+	echo "modified beta again" > $testroot/wt/beta
+	(cd $testroot/wt && got commit -m "changing beta again" > /dev/null)
+
+	(cd $testroot/wt && got update > /dev/null)
+
+	(cd $testroot/wt && got bo :head:-2 > $testroot/stdout)
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "A  epsilon/zeta" >> $testroot/stdout.expected
+	echo "D  new" >> $testroot/stdout.expected
+	echo "Backed out commit $bad_commit" >> $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
 
+	echo "alpha" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if [ -e "$testroot/wt/new" ]; then
+		echo "file '$testroot/wt/new' still exists on disk" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if [ ! -e "$testroot/wt/epsilon/zeta" ]; then
+		echo "file '$testroot/wt/epsilon/zeta' is missing on disk" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo 'M  alpha' > $testroot/stdout.expected
+	echo 'A  epsilon/zeta' >> $testroot/stdout.expected
+	echo 'D  new' >> $testroot/stdout.expected
+	(cd $testroot/wt && got status > $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
 
+	local next_backout=`git_show_head $testroot/repo`
+
+	(cd "$testroot/wt" && got ci -m "backed-out bad commit" > /dev/null)
+	(cd "$testroot/wt" && got up > /dev/null)
+
+	echo "G  beta" > $testroot/stdout.expected
+	echo "Backed out commit $next_backout" >> $testroot/stdout.expected
+	(cd "$testroot/wt" && got bo :base:- > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
 	test_done "$testroot" "$ret"
 }
 
@@ -549,3 +644,4 @@ run_test test_backout_edits_for_file_since_deleted
 run_test test_backout_next_commit
 run_test test_backout_umask
 run_test test_backout_logmsg_ref
+run_test test_backout_commit_keywords
blob - cab7171d5632c0bdf72e14e1d6dda98d3a4ce810
blob + 71213582275291870b3a3f5941f02a76914cc1c1
--- regress/cmdline/branch.sh
+++ regress/cmdline/branch.sh
@@ -527,6 +527,52 @@ test_branch_packed_ref_collision() {
 	echo "* zoo: $commit_id2" > $testroot/stdout.expected
 	echo "  master: $commit_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
+
+	test_done "$testroot" "$ret"
+}
+
+test_branch_commit_keywords() {
+	local testroot=$(test_init branch_commit_keywords)
+
+	set -A ids "$(git_show_head $testroot/repo)"
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for i in $(seq 4); do
+		echo "beta change $i" > "$testroot/wt/beta"
+
+		(cd "$testroot/wt" && got ci -m "commit number $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+		set -- "$ids" "$(git_show_head $testroot/repo)"
+		ids=$*
+	done
+
+	(cd "$testroot/wt" && got up > /dev/null)
+
+	echo "  kwbranch: $(pop_id 3 $ids)" > $testroot/stdout.expected
+	echo "  master: $(pop_id 5 $ids)" >> $testroot/stdout.expected
+
+	(cd "$testroot/wt" && got br -nc :head:-2 kwbranch > /dev/null)
+	got br -r "$testroot/repo" -l > "$testroot/stdout"
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
 	ret=$?
 	if [ $ret -ne 0 ]; then
 		diff -u $testroot/stdout.expected $testroot/stdout
@@ -534,6 +580,17 @@ test_branch_packed_ref_collision() {
 		return 1
 	fi
 
+	echo "  kwbranch2: $(pop_id 4 $ids)" > $testroot/stdout.expected
+
+	got br -r "$testroot/repo" -c master:- kwbranch2 > /dev/null
+	got br -r "$testroot/repo" -l | grep kwbranch2 > "$testroot/stdout"
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
 	test_done "$testroot" "$ret"
 }
 
@@ -545,3 +602,4 @@ run_test test_branch_delete_current_branch
 run_test test_branch_delete_packed
 run_test test_branch_show
 run_test test_branch_packed_ref_collision
+run_test test_branch_commit_keywords
blob - 9403e4715c6ed077315fb8b1bcb68bccd4247176
blob + 9bdce2b8a07cd2893ca474983afc996ca236d190
--- regress/cmdline/checkout.sh
+++ regress/cmdline/checkout.sh
@@ -928,7 +928,82 @@ test_checkout_ulimit_n() {
 	ret=$?
 	if [ $ret -ne 0 ]; then
 		diff -u $testroot/content.expected $testroot/content
+	fi
+	test_done "$testroot" "$ret"
+}
+
+test_checkout_commit_keywords() {
+	local testroot=$(test_init checkout_commit_keywords)
+
+	set -A ids "$(git_show_head $testroot/repo)"
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for i in $(seq 4); do
+		echo "zeta change $i" > "$testroot/wt/epsilon/zeta"
+
+		(cd "$testroot/wt" && got ci -m "commit number $i" > /dev/null)
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			echo "commit failed unexpectedly" >&2
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+		set -- "$ids" "$(git_show_head $testroot/repo)"
+		ids=$*
+	done
+
+	echo "A  $testroot/wt2/alpha" > $testroot/stdout.expected
+	echo "A  $testroot/wt2/beta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt2/epsilon/zeta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt2/gamma/delta" >> $testroot/stdout.expected
+	echo "Checked out refs/heads/master: $(pop_id 4 $ids)" \
+		>> $testroot/stdout.expected
+	echo "Now shut up and hack" >> $testroot/stdout.expected
+
+	got co -c :head:- $testroot/repo $testroot/wt2 > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
 	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "A  $testroot/wt3/alpha" > $testroot/stdout.expected
+	echo "A  $testroot/wt3/beta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt3/epsilon/zeta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt3/gamma/delta" >> $testroot/stdout.expected
+	echo "Checked out refs/heads/master: $(pop_id 4 $ids)" \
+		>> $testroot/stdout.expected
+	echo "Now shut up and hack" >> $testroot/stdout.expected
+
+	got co -bmaster -c:base:- $testroot/repo $testroot/wt3 > \
+	    $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
 	test_done "$testroot" "$ret"
 }
 
@@ -948,3 +1023,4 @@ run_test test_checkout_repo_with_unknown_extension
 run_test test_checkout_quiet
 run_test test_checkout_umask
 run_test test_checkout_ulimit_n
+run_test test_checkout_commit_keywords
blob - 364fe8dd78d0449f3b9718c310a7ead6bddbe3a6
blob + 8de61842e9e87808d61b276b6d639e2f1b7d5e09
--- regress/cmdline/cherrypick.sh
+++ regress/cmdline/cherrypick.sh
@@ -2028,6 +2028,68 @@ test_cherrypick_logmsg_ref() {
 	test_done "$testroot" "$ret"
 }
 
+test_cherrypick_commit_keywords() {
+	local testroot=`test_init cherrypick_commit_keywords`
+
+	set -A ids "$(git_show_head $testroot/repo)"
+
+	(cd $testroot/repo && git checkout -q -b branch-1)
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "changed on branch-1" >> "$testroot/repo/alpha"
+	git_commit $testroot/repo -m "alpha changed on branch-1"
+	set -- "$ids" "$(git_show_head $testroot/repo)"
+	ids=$*
+
+	for i in $(seq 4); do
+		echo "branch-1 change $i" >> "$testroot/repo/gamma/delta"
+
+		git_commit $testroot/repo -m "commit number $i"
+		set -- "$ids" "$(git_show_head $testroot/repo)"
+		ids=$*
+	done
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "Merged commit $(pop_id 2 $ids)" >> $testroot/stdout.expected
+
+	(cd "$testroot/wt" && got cy master:+ > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd "$testroot/wt" && got rv alpha > /dev/null)
+
+	echo "C  gamma/delta" > $testroot/stdout.expected
+	echo "Merged commit $(pop_id 5 $ids)" >> $testroot/stdout.expected
+	echo "Files with new merge conflicts: 1" >> $testroot/stdout.expected
+	(cd "$testroot/wt" && got cy branch-1:- > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_cherrypick_basic
 run_test test_cherrypick_root_commit
@@ -2047,3 +2109,4 @@ run_test test_cherrypick_dot_on_a_line_by_itself
 run_test test_cherrypick_binary_file
 run_test test_cherrypick_umask
 run_test test_cherrypick_logmsg_ref
+run_test test_cherrypick_commit_keywords
blob - 96004ce8b8a631507d11bbb3ba545448a1a16429
blob + 58e79bdb23ae3a08aa668ad20b439963c6eda8d1
--- regress/cmdline/log.sh
+++ regress/cmdline/log.sh
@@ -1043,8 +1043,111 @@ test_log_commit_keywords() {
 	ret=$?
 	if [ $ret -ne 0 ]; then
 		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
 	fi
+
+	echo "got: '::base:+': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -c::base:+ 2> $testroot/stderr)
 
+	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 "got: ':head:-:': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -c:head:-: 2> $testroot/stderr)
+
+	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 "got: 'master::+': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -cmaster::+ 2> $testroot/stderr)
+
+	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 "got: 'master:1+': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -cmaster:1+ 2> $testroot/stderr)
+
+	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 "got: ':base:-1:base:-1': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -c:base:-1:base:-1 2> $testroot/stderr)
+
+	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 "got: 'main:-main:-': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -cmain:-main:- 2> $testroot/stderr)
+
+	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 "got: ':base:*1': invalid commit keyword" > \
+	    $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -c:base:*1 2> $testroot/stderr)
+
+	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 "got: reference null not found" > $testroot/stderr.expected
+
+	(cd $testroot/wt && got log -cnull:+ 2> $testroot/stderr)
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+
 	test_done "$testroot" "$ret"
 }
 
blob - 17db471e5c3aa81e0eb9233b406861cdccd08fcd
blob + f95aac7980de13e20863266dcd1ae971cb702364
--- regress/cmdline/patch.sh
+++ regress/cmdline/patch.sh
@@ -1955,7 +1955,91 @@ test_patch_remove_binary_file() {
 
 	test_done $testroot 0
 }
+
+test_patch_commit_keywords() {
+	local testroot=`test_init patch_commit_keywords`
+
+	got checkout $testroot/repo $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 > $testroot/wt/numbers
+	(cd $testroot/wt && got add numbers && got commit -m +numbers) \
+		>/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed s/4/four/ > $testroot/wt/numbers
+
+	# get rid of the metadata
+	(cd $testroot/wt && got diff | sed -n '/^---/,$p' > patch) \
+		>/dev/null
 
+	jot 10 | sed s/6/six/ > $testroot/wt/numbers
+	(cd $testroot/wt && got commit -m 'edit numbers') >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	(cd $testroot/wt && got patch -c :head:- patch) >$testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo 'G  numbers' > $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout $testroot/stdout.expected
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed -e s/4/four/ -e s/6/six/ > $testroot/wt/numbers.expected
+	cmp -s $testroot/wt/numbers $testroot/wt/numbers.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/wt/numbers $testroot/wt/numbers.expected
+	fi
+
+	(cd "$testroot/wt" && got rv numbers > /dev/null)
+
+	(cd $testroot/wt && got patch -c :base:- patch) >$testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo 'G  numbers' > $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout $testroot/stdout.expected
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed -e s/4/four/ -e s/6/six/ > $testroot/wt/numbers.expected
+	cmp -s $testroot/wt/numbers $testroot/wt/numbers.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/wt/numbers $testroot/wt/numbers.expected
+	fi
+
+	test_done $testroot $ret
+}
+
 test_parseargs "$@"
 run_test test_patch_basic
 run_test test_patch_dont_apply
@@ -1986,3 +2070,4 @@ run_test test_patch_newfile_xbit_got_diff
 run_test test_patch_newfile_xbit_git_diff
 run_test test_patch_umask
 run_test test_patch_remove_binary_file
+run_test test_patch_commit_keywords