Commit Diff


commit - ad10f64ebeae9e8625205b8ef468ce58e03db95b
commit + 66b04f8f59f931fc5e30d76b7a9032f336ec5556
blob - 93c8f6f920ce4b2013710cddd1e94d1b83b0a7ad
blob + a329dca28bf36978b73b04fe71d37578bfbc712f
--- regress/tog/blame.sh
+++ regress/tog/blame.sh
@@ -76,5 +76,234 @@ EOF
 	test_done "$testroot" "$ret"
 }
 
+test_blame_commit_keywords()
+{
+	test_init blame_commit_keywords 80 10
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+	local id=$(git_show_head "$repo")
+	local author_time=$(git_show_author_time "$repo")
+	local ymd=$(date -u -r $author_time +"%G-%m-%d")
+
+	set -A ids "$id"
+	set -A short_ids "$(trim_obj_id 32 $id)"
+
+	cat <<-EOF >$TOG_TEST_SCRIPT
+	WAIT_FOR_UI	wait for blame to finish
+	SCREENDUMP
+	EOF
+
+	# :base requires work tree
+	echo "tog: '-c :base' requires work tree" > "$testroot/stderr.expected"
+	tog 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
+
+	# :head keyword in repo
+	cat <<-EOF >$testroot/view.expected
+	commit $id
+	[1/1] /alpha
+	$(pop_id 1 $short_ids) alpha
+
+
+
+
+
+
+
+	EOF
+
+	tog blame -r "$repo" -c:head alpha
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "blame command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got checkout "$repo" "$wt" > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# move into the work tree (test is run in a subshell)
+	cd "$wt"
+	echo -n > alpha
+
+	for i in $(seq 8); do
+		echo "alpha $i" >> alpha
+
+		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" "$id"
+		ids=$*
+		set -- "$short_ids" "$(trim_obj_id 32 $id)"
+		short_ids=$*
+	done
+
+	author_time=$(git_show_author_time "$repo")
+	ymd=$(date -u -r $author_time +"%G-%m-%d")
+
+	# :base:- keyword in work tree
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 8 $ids)
+	[1/7] /alpha
+	$(pop_id 2 $short_ids) alpha 1
+	$(pop_id 3 $short_ids) alpha 2
+	$(pop_id 4 $short_ids) alpha 3
+	$(pop_id 5 $short_ids) alpha 4
+	$(pop_id 6 $short_ids) alpha 5
+	$(pop_id 7 $short_ids) alpha 6
+	$(pop_id 8 $short_ids) alpha 7
+
+	EOF
+
+	tog blame -c:base:- alpha
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "blame command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# :head:-4 keyword in work tree
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 5 $ids)
+	[1/4] /alpha
+	$(pop_id 2 $short_ids) alpha 1
+	$(pop_id 3 $short_ids) alpha 2
+	$(pop_id 4 $short_ids) alpha 3
+	$(pop_id 5 $short_ids) alpha 4
+
+
+
+
+	EOF
+
+	tog blame -c:head:-4 alpha
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "blame command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# :base:+2 keyword in work tree
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 5 $ids)
+	[1/4] /alpha
+	$(pop_id 2 $short_ids) alpha 1
+	$(pop_id 3 $short_ids) alpha 2
+	$(pop_id 4 $short_ids) alpha 3
+	$(pop_id 5 $short_ids) alpha 4
+
+
+
+
+	EOF
+
+	got up -c:head:-6 > /dev/null
+	if [ $ret -ne 0 ]; then
+		echo "update command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	tog blame -c:base:+2 alpha
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "blame command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# master:-99 keyword in work tree
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 1 $ids)
+	[1/1] /alpha
+	$(pop_id 1 $short_ids) alpha
+
+
+
+
+
+
+
+	EOF
+
+	tog blame -cmaster:-99 alpha
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "blame command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_blame_basic
+run_test test_blame_commit_keywords
blob - 53edc5c3c79e0951bfdaf8315a353c3430a7b579
blob + 6ab39dc440fb2afb20ecaabc0425b545530d6c6d
--- regress/tog/diff.sh
+++ regress/tog/diff.sh
@@ -205,7 +205,178 @@ EOF
 	test_done "$testroot" "$ret"
 }
 
+test_diff_commit_keywords()
+{
+	test_init diff_commit_keywords 120 24
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+	local id=$(git_show_head "$repo")
+	local author_time=$(git_show_author_time "$repo")
+	local date=$(date -u -r $author_time +"%a %b %e %X %Y UTC")
+
+	set -A ids "$id"
+	set -A alpha_ids $(get_blob_id "$repo" "" alpha)
+
+	got checkout "$repo" "$wt" > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# move into the work tree (test is run in a subshell)
+	cd "$wt"
+
+	for i in $(seq 8); do
+		echo "alpha $i" > alpha
+
+		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" "$id"
+		ids=$*
+		set -- "$alpha_ids" "$(get_blob_id "$repo" "" alpha)"
+		alpha_ids=$*
+	done
+
+	cat <<-EOF >$TOG_TEST_SCRIPT
+	SCREENDUMP
+	EOF
+
+	# diff consecutive commits with keywords
+	local lhs_id=$(pop_id 1 $ids)
+	local rhs_id=$(pop_id 2 $ids)
+
+	cat <<-EOF >$testroot/view.expected
+	[1/20] diff $lhs_id $rhs_id
+	commit $rhs_id
+	from: Flan Hacker <flan_hacker@openbsd.org>
+	date: $date
+
+	commit 1
+
+	M  alpha  |  1+  1-
+
+	1 file changed, 1 insertion(+), 1 deletion(-)
+
+	commit - $lhs_id
+	commit + $rhs_id
+	blob - $(pop_id 1 $alpha_ids)
+	blob + $(pop_id 2 $alpha_ids)
+	--- alpha
+	+++ alpha
+	@@ -1 +1 @@
+	-alpha
+	+alpha 1
+
+
+
+	(END)
+	EOF
+
+	tog diff :base:-99 :head:-7
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# diff arbitrary commits with keywords
+	lhs_id=$(pop_id 5 $ids)
+	rhs_id=$(pop_id 8 $ids)
+
+	cat <<-EOF >$testroot/view.expected
+	[1/10] diff $lhs_id $rhs_id
+	commit - $lhs_id
+	commit + $rhs_id
+	blob - $(pop_id 5 $alpha_ids)
+	blob + $(pop_id 8 $alpha_ids)
+	--- alpha
+	+++ alpha
+	@@ -1 +1 @@
+	-alpha 4
+	+alpha 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+	(END)
+	EOF
+
+	tog diff master:-4 :head:-
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# diff consecutive commits using keywords with -r repository
+	lhs_id=$(pop_id 8 $ids)
+	rhs_id=$(pop_id 9 $ids)
+	author_time=$(git_show_author_time "$repo")
+	date=$(date -u -r $author_time +"%a %b %e %X %Y UTC")
+
+	cat <<-EOF >$testroot/view.expected
+	[1/20] diff $lhs_id refs/heads/master
+	commit $rhs_id (master)
+	from: Flan Hacker <flan_hacker@openbsd.org>
+	date: $date
+
+	commit 8
+
+	M  alpha  |  1+  1-
+
+	1 file changed, 1 insertion(+), 1 deletion(-)
+
+	commit - $lhs_id
+	commit + $rhs_id
+	blob - $(pop_id 8 $alpha_ids)
+	blob + $(pop_id 9 $alpha_ids)
+	--- alpha
+	+++ alpha
+	@@ -1 +1 @@
+	-alpha 7
+	+alpha 8
+
+
+
+	(END)
+	EOF
+
+	tog diff -r "$repo" :head:- master
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_diff_contiguous_commits
 run_test test_diff_arbitrary_commits
 run_test test_diff_J_keymap_on_last_loaded_commit
+run_test test_diff_commit_keywords
blob - 67469c1eae523eaf0c2b46922e300c93c42a323f
blob + 2a28c65c9bf9e227cba943ce259438976ac67c02
--- regress/tog/log.sh
+++ regress/tog/log.sh
@@ -347,10 +347,155 @@ EOF
 	ret=$?
 	if [ $ret -ne 0 ]; then
 		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_commit_keywords()
+{
+	test_init log_commit_keywords 120 10
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+	local id=$(git_show_head "$repo")
+	local author_time=$(git_show_author_time "$repo")
+	local ymd=$(date -u -r $author_time +"%G-%m-%d")
+
+	set -A ids "$id"
+	set -A short_ids "$(trim_obj_id 32 $id)"
+
+	got checkout "$repo" "$wt" > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# move into the work tree (test is run in a subshell)
+	cd "$wt"
+	echo -n > alpha
+
+	for i in $(seq 8); do
+		echo "alpha $i" >> alpha
+
+		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" "$id"
+		ids=$*
+		set -- "$short_ids" "$(trim_obj_id 32 $id)"
+		short_ids=$*
+	done
+
+	cat <<-EOF >$TOG_TEST_SCRIPT
+	SCREENDUMP
+	EOF
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 5 $ids) [1/5]
+	$ymd $(pop_id 5 $short_ids) flan_hacker  commit 4
+	$ymd $(pop_id 4 $short_ids) flan_hacker  commit 3
+	$ymd $(pop_id 3 $short_ids) flan_hacker  commit 2
+	$ymd $(pop_id 2 $short_ids) flan_hacker  commit 1
+	$ymd $(pop_id 1 $short_ids) flan_hacker  adding the test tree
+
+
+
+
+	EOF
+
+	tog log -c:base:-4
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 7 $ids) [1/7]
+	$ymd $(pop_id 7 $short_ids) flan_hacker  commit 6
+	$ymd $(pop_id 6 $short_ids) flan_hacker  commit 5
+	$ymd $(pop_id 5 $short_ids) flan_hacker  commit 4
+	$ymd $(pop_id 4 $short_ids) flan_hacker  commit 3
+	$ymd $(pop_id 3 $short_ids) flan_hacker  commit 2
+	$ymd $(pop_id 2 $short_ids) flan_hacker  commit 1
+	$ymd $(pop_id 1 $short_ids) flan_hacker  adding the test tree
+
+
+	EOF
+
+	tog log -r "$repo" -c:head:-2
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
 		test_done "$testroot" "$ret"
 		return 1
 	fi
 
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 5 $ids) [1/5]
+	$ymd $(pop_id 5 $short_ids) flan_hacker  commit 4
+	$ymd $(pop_id 4 $short_ids) flan_hacker  commit 3
+	$ymd $(pop_id 3 $short_ids) flan_hacker  commit 2
+	$ymd $(pop_id 2 $short_ids) flan_hacker  commit 1
+	$ymd $(pop_id 1 $short_ids) flan_hacker  adding the test tree
+
+
+
+
+	EOF
+
+	got up -c:base:-6 > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got update failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tog log -c:base:+2
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 1 $ids) [1/1]
+	$ymd $(pop_id 1 $short_ids) flan_hacker  adding the test tree
+
+
+
+
+
+
+
+
+	EOF
+
+	tog log -c:base:-99
+	cmp -s "$testroot/view.expected" "$testroot/view"
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u "$testroot/view.expected" "$testroot/view"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
 	test_done "$testroot" "$ret"
 }
 
@@ -362,3 +507,4 @@ run_test test_log_scroll_right
 run_test test_log_hsplit_ref
 run_test test_log_hsplit_tree
 run_test test_log_logmsg_widechar
+run_test test_log_commit_keywords
blob - c26b425e144352ef6828dcd4df36836b60aee6c9
blob + ab5481d9ca9da400e86ac14e7511cf21b1e24863
--- regress/tog/tree.sh
+++ regress/tog/tree.sh
@@ -178,8 +178,173 @@ EOF
 	test_done "$testroot" "$ret"
 }
 
+test_tree_commit_keywords()
+{
+	test_init tree_commit_keywords 48 11
+	local repo="$testroot/repo"
+	local wt="$testroot/wt"
+	local id=$(git_show_head "$repo")
+
+	set -A ids "$id"
+
+	got checkout "$repo" "$wt" > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# move into the work tree (test is run in a subshell)
+	cd "$wt"
+
+	for i in $(seq 8); do
+		if [ $(( i % 2 )) -eq 0 ]; then
+			echo "file${i}" > "file${i}"
+			got add "file${i}" > /dev/null
+		else
+			echo "alpha $i" > alpha
+		fi
+
+		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" "$id"
+		ids=$*
+	done
+
+
+	cat <<-EOF >$TOG_TEST_SCRIPT
+	SCREENDUMP
+	EOF
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 8 $ids)
+	[1/7] /
+
+	  alpha
+	  beta
+	  epsilon/
+	  file2
+	  file4
+	  file6
+	  gamma/
+
+	EOF
+
+	tog tree -c:base:-
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 6 $ids)
+	[1/6] /
+
+	  alpha
+	  beta
+	  epsilon/
+	  file2
+	  file4
+	  gamma/
+
+
+	EOF
+
+	tog tree -cmaster:-3
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 9 $ids)
+	[1/8] /
+
+	  alpha
+	  beta
+	  epsilon/
+	  file2
+	  file4
+	  file6
+	  file8
+	  gamma/
+	EOF
+
+	got up -c:head:-99 > /dev/null
+	tog tree -c:base:+99
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 4 $ids)
+	[1/5] /
+
+	  alpha
+	  beta
+	  epsilon/
+	  file2
+	  gamma/
+
+
+
+	EOF
+
+	tog tree -c:head:-5
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cat <<-EOF >$testroot/view.expected
+	commit $(pop_id 1 $ids)
+	[1/4] /
+
+	  alpha
+	  beta
+	  epsilon/
+	  gamma/
+
+
+
+
+	EOF
+
+	tog tree -r "$repo" -cmaster:-99
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_tree_basic
 run_test test_tree_vsplit_blame
 run_test test_tree_hsplit_blame
 run_test test_tree_symlink
+run_test test_tree_commit_keywords
blob - 667bfe283076f37b6cb5690da6b2a98d9aae516a
blob + 816fab05d74cd0ab51f96eb3cad17d26d95b56e3
--- tog/tog.1
+++ tog/tog.1
@@ -274,11 +274,46 @@ key binding can be used to toggle display of merged co
 .It Fl c Ar commit
 Start traversing history at the specified
 .Ar commit .
-The expected argument is the name of a branch or a commit ID SHA1 hash.
+The expected argument is a commit ID SHA1 hash, or a reference name or keyword
+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.
-If this option is not specified, default to the work tree's current branch
-if invoked in a work tree, or to the repository's HEAD reference.
+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 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 r Ar repository-path
 Use the repository at the specified path.
 If not specified, assume the repository is located at or above the current
@@ -296,11 +331,48 @@ work tree, use the repository path associated with thi
 .Ar object2
 .Xc
 Display the differences between two objects in the repository.
-Treat each of the two arguments as a reference, a tag name, or an object
-ID SHA1 hash, and display differences between the corresponding objects.
+Treat each of the two arguments as a reference, a tag name, an object
+ID SHA1 hash, or a keyword and display differences between the corresponding
+objects.
 Both objects must be of the same type (blobs, trees, or commits).
 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 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 .
 .Pp
 The key bindings for
 .Cm tog diff
@@ -484,9 +556,46 @@ are as follows:
 .It Fl c Ar commit
 Start traversing history at the specified
 .Ar commit .
-The expected argument is the name of a branch or a commit ID SHA1 hash.
+The expected argument is a commit ID SHA1 hash, or a reference name or keyword
+which will be resolved to a commit ID.
 An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
+The 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 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 r Ar repository-path
 Use the repository at the specified path.
 If not specified, assume the repository is located at or above the current
@@ -584,9 +693,46 @@ are as follows:
 .It Fl c Ar commit
 Start traversing history at the specified
 .Ar commit .
-The expected argument is the name of a branch or a commit ID SHA1 hash.
+The expected argument is a commit ID SHA1 hash, or a reference name or keyword
+which will be resolved to a commit ID.
 An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
+The 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 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 r Ar repository-path
 Use the repository at the specified path.
 If not specified, assume the repository is located at or above the current
blob - abf8c21edb245a96720b8a92f3d1ea6fe1c6a36b
blob + 904d4efc56bf3771d09c35206b797c2c65e35e07
--- tog/tog.c
+++ tog/tog.c
@@ -58,6 +58,7 @@
 #include "got_privsep.h"
 #include "got_path.h"
 #include "got_worktree.h"
+#include "got_keyword.h"
 
 #ifndef MIN
 #define	MIN(_a,_b) ((_a) < (_b) ? (_a) : (_b))
@@ -4310,7 +4311,7 @@ init_mock_term(const char *test_script_path)
 	screen_dump_path = getenv("TOG_SCR_DUMP");
 	if (screen_dump_path == NULL || *screen_dump_path == '\0')
 		return got_error_msg(GOT_ERR_IO, "TOG_SCR_DUMP not defined");
-	tog_io.sdump = fopen(screen_dump_path, "wex");
+	tog_io.sdump = fopen(screen_dump_path, "we");
 	if (tog_io.sdump == NULL) {
 		err = got_error_from_errno2("fopen", screen_dump_path);
 		goto done;
@@ -4408,7 +4409,7 @@ cmd_log(int argc, char *argv[])
 	struct got_worktree *worktree = NULL;
 	struct got_object_id *start_id = NULL;
 	char *in_repo_path = NULL, *repo_path = NULL, *cwd = NULL;
-	char *start_commit = NULL, *label = NULL;
+	char *keyword_idstr = NULL, *start_commit = NULL, *label = NULL;
 	struct got_reference *ref = NULL;
 	const char *head_ref_name = NULL;
 	int ch, log_branches = 0;
@@ -4494,6 +4495,13 @@ cmd_log(int argc, char *argv[])
 			goto done;
 		head_ref_name = label;
 	} else {
+		error = got_keyword_to_idstr(&keyword_idstr, start_commit,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr != NULL)
+			start_commit = keyword_idstr;
+
 		error = got_ref_open(&ref, repo, start_commit, 0);
 		if (error == NULL)
 			head_ref_name = got_ref_get_name(ref);
@@ -4521,6 +4529,7 @@ cmd_log(int argc, char *argv[])
 	}
 	error = view_loop(view);
 done:
+	free(keyword_idstr);
 	free(in_repo_path);
 	free(repo_path);
 	free(cwd);
@@ -5922,6 +5931,7 @@ cmd_diff(int argc, char *argv[])
 	struct got_object_id *id1 = NULL, *id2 = NULL;
 	char *repo_path = NULL, *cwd = NULL;
 	char *id_str1 = NULL, *id_str2 = NULL;
+	char *keyword_idstr1 = NULL, *keyword_idstr2 = NULL;
 	char *label1 = NULL, *label2 = NULL;
 	int diff_context = 3, ignore_whitespace = 0;
 	int ch, force_text_diff = 0;
@@ -6004,6 +6014,23 @@ cmd_diff(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	if (id_str1 != NULL) {
+		error = got_keyword_to_idstr(&keyword_idstr1, id_str1,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr1 != NULL)
+			id_str1 = keyword_idstr1;
+	}
+	if (id_str2 != NULL) {
+		error = got_keyword_to_idstr(&keyword_idstr2, id_str2,
+		    repo, worktree);
+		if (error != NULL)
+			goto done;
+		if (keyword_idstr2 != NULL)
+			id_str2 = keyword_idstr2;
+	}
+
 	error = got_repo_match_object_id(&id1, &label1, id_str1,
 	    GOT_OBJ_TYPE_ANY, &tog_refs, repo);
 	if (error)
@@ -6025,6 +6052,8 @@ cmd_diff(int argc, char *argv[])
 		goto done;
 	error = view_loop(view);
 done:
+	free(keyword_idstr1);
+	free(keyword_idstr2);
 	free(label1);
 	free(label2);
 	free(repo_path);
@@ -7056,7 +7085,7 @@ cmd_blame(int argc, char *argv[])
 	char *link_target = NULL;
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
-	char *commit_id_str = NULL;
+	char *keyword_idstr = NULL, *commit_id_str = NULL;
 	int ch;
 	struct tog_view *view = NULL;
 	int *pack_fds = NULL;
@@ -7134,6 +7163,13 @@ cmd_blame(int argc, char *argv[])
 		error = got_ref_resolve(&commit_id, repo, head_ref);
 		got_ref_close(head_ref);
 	} else {
+		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, &tog_refs, repo);
 	}
@@ -7174,6 +7210,7 @@ done:
 	free(link_target);
 	free(cwd);
 	free(commit_id);
+	free(keyword_idstr);
 	if (commit)
 		got_object_commit_close(commit);
 	if (worktree)
@@ -8033,7 +8070,7 @@ cmd_tree(int argc, char *argv[])
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	const char *commit_id_arg = NULL;
-	char *label = NULL;
+	char *keyword_idstr = NULL, *label = NULL;
 	struct got_reference *ref = NULL;
 	const char *head_ref_name = NULL;
 	int ch;
@@ -8112,6 +8149,13 @@ cmd_tree(int argc, char *argv[])
 			goto done;
 		head_ref_name = label;
 	} 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_ref_open(&ref, repo, commit_id_arg, 0);
 		if (error == NULL)
 			head_ref_name = got_ref_get_name(ref);
@@ -8149,6 +8193,7 @@ cmd_tree(int argc, char *argv[])
 	}
 	error = view_loop(view);
 done:
+	free(keyword_idstr);
 	free(repo_path);
 	free(cwd);
 	free(commit_id);
@@ -9990,7 +10035,9 @@ main(int argc, char *argv[])
 	    error->code != GOT_ERR_EOF &&
 	    error->code != GOT_ERR_PRIVSEP_EXIT &&
 	    error->code != GOT_ERR_PRIVSEP_PIPE &&
-	    !(error->code == GOT_ERR_ERRNO && errno == EINTR))
+	    !(error->code == GOT_ERR_ERRNO && errno == EINTR)) {
 		fprintf(stderr, "%s: %s\n", getprogname(), error->msg);
+		return 1;
+	}
 	return 0;
 }