Commit 896e9b6f30150bb0dfff1345dc5935a4b65b6838

Stefan Sperling 2019-08-26T15:10:12

add support for path arguments to 'got cat'

diff --git a/got/got.1 b/got/got.1
index 3697535..e4ba153 100644
--- a/got/got.1
+++ b/got/got.1
@@ -1151,19 +1151,38 @@ file instead of prompting interactively.
 .It Cm ug
 Short alias for
 .Cm unstage .
-.It Cm cat Oo Fl r Ar repository-path Oc Ar object ...
-Parse and print contents of specified objects to standard output
-in a line-based text format.
-Treat each argument as a reference, a tag name, or an object ID SHA1 hash.
+.It Cm cat Oo Fl c Ar commit Oc Oo Fl r Ar repository-path Oc Ar arg ...
+Parse and print contents of objects to standard output in a line-based
+text format.
+Content of commit, tree, and tag objects is printed in a way similar
+to the actual content stored in such objects.
+Blob object contents are printed as they would appear in files on disk.
+.Pp
+Attempt to interpret each argument as a reference, a tag name, or
+an object ID SHA1 hash.
 References will be resolved to an object ID.
 Tag names will resolved to a tag object.
 An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
 .Pp
+If none of the above interpretations produce a valid result, or if the
+.Fl P
+option is used, attempt to interpret the argument as a path which will
+be resolved to the ID of an object found at this path in the repository.
+.Pp
 The options for
 .Cm got cat
 are as follows:
 .Bl -tag -width Ds
+.It Fl c Ar commit
+Look up paths in the specified
+.Ar commit .
+If this option is not used, paths are looked up in the commit resolved
+via the repository's HEAD reference.
+The expected 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.
 .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
@@ -1171,6 +1190,10 @@ working directory.
 If this directory is a
 .Nm
 work tree, use the repository path associated with this work tree.
+.It Fl P
+Interpret all arguments as paths only.
+This option can be used to resolve ambiguity in cases where paths
+look like tag names, reference names, or object IDs.
 .El
 .El
 .Sh ENVIRONMENT
diff --git a/got/got.c b/got/got.c
index 178ad9f..54e235a 100644
--- a/got/got.c
+++ b/got/got.c
@@ -6544,8 +6544,8 @@ done:
 __dead static void
 usage_cat(void)
 {
-	fprintf(stderr, "usage: %s cat [-r repository ] object1 "
-	    "[object2 ...]\n", getprogname());
+	fprintf(stderr, "usage: %s cat [-r repository ] [ -c commit ] [ -P ] "
+	    "arg1 [arg2 ...]\n", getprogname());
 	exit(1);
 }
 
@@ -6703,8 +6703,9 @@ cmd_cat(int argc, char *argv[])
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL, *label = NULL;
-	struct got_object_id *id = NULL;
-	int ch, obj_type, i;
+	const char *commit_id_str = NULL;
+	struct got_object_id *id = NULL, *commit_id = NULL;
+	int ch, obj_type, i, force_path = 0;
 
 #ifndef PROFILE
 	if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil",
@@ -6712,14 +6713,20 @@ cmd_cat(int argc, char *argv[])
 		err(1, "pledge");
 #endif
 
-	while ((ch = getopt(argc, argv, "r:")) != -1) {
+	while ((ch = getopt(argc, argv, "c:r:P")) != -1) {
 		switch (ch) {
+		case 'c':
+			commit_id_str = optarg;
+			break;
 		case 'r':
 			repo_path = realpath(optarg, NULL);
 			if (repo_path == NULL)
 				err(1, "-r option");
 			got_path_strip_trailing_slashes(repo_path);
 			break;
+		case 'P':
+			force_path = 1;
+			break;
 		default:
 			usage_cat();
 			/* NOTREACHED */
@@ -6763,11 +6770,31 @@ cmd_cat(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	if (commit_id_str == NULL)
+		commit_id_str = GOT_REF_HEAD;
+	error = resolve_commit_arg(&commit_id, commit_id_str, repo);
+	if (error)
+		goto done;
+
 	for (i = 0; i < argc; i++) {
-		error = match_object_id(&id, &label, argv[i],
-		    GOT_OBJ_TYPE_ANY, 0, repo);
-		if (error)
-			break;
+		if (force_path) {
+			error = got_object_id_by_path(&id, repo, commit_id,
+			    argv[i]);
+			if (error)
+				break;
+		} else {
+			error = match_object_id(&id, &label, argv[i],
+			    GOT_OBJ_TYPE_ANY, 0, repo);
+			if (error) {
+				if (error->code != GOT_ERR_BAD_OBJ_ID_STR &&
+				    error->code != GOT_ERR_NOT_REF)
+					break;
+				error = got_object_id_by_path(&id, repo,
+				    commit_id, argv[i]);
+				if (error)
+					break;
+			}
+		}
 
 		error = got_object_get_type(&obj_type, repo, id);
 		if (error)
@@ -6801,6 +6828,7 @@ cmd_cat(int argc, char *argv[])
 done:
 	free(label);
 	free(id);
+	free(commit_id);
 	if (worktree)
 		got_worktree_close(worktree);
 	if (repo) {
diff --git a/regress/cmdline/cat.sh b/regress/cmdline/cat.sh
index ff28334..e94f002 100755
--- a/regress/cmdline/cat.sh
+++ b/regress/cmdline/cat.sh
@@ -52,7 +52,8 @@ function test_cat_basic {
 	echo >> $testroot/stdout.expected
 	echo "numparents 0" >> $testroot/stdout.expected
 	echo "author $GOT_AUTHOR $author_time +0000" >> $testroot/stdout.expected
-	echo "committer Flan Hacker <flan_hacker@openbsd.org> $author_time +0000" >> $testroot/stdout.expected
+	echo "committer $GOT_AUTHOR $author_time +0000" \
+		>> $testroot/stdout.expected
 	echo "messagelen 22" >> $testroot/stdout.expected
 	printf "\nadding the test tree\n" >> $testroot/stdout.expected
 
@@ -71,4 +72,135 @@ function test_cat_basic {
 	
 }
 
+function test_cat_path {
+	local testroot=`test_init cat_path`
+	local commit_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local alpha_id=`got tree -r $testroot/repo -i | grep 'alpha$' | cut -d' ' -f 1`
+	local gamma_id=`got tree -r $testroot/repo -i | grep 'gamma/$' | cut -d' ' -f 1`
+	local delta_id=`got tree -r $testroot/repo -i gamma | grep 'delta$' | cut -d' ' -f 1`
+
+	# cat blob by path
+	echo "alpha" > $testroot/stdout.expected
+	got cat -r $testroot/repo alpha > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# cat tree by path
+	echo "$delta_id 0100644 delta" > $testroot/stdout.expected
+	got cat -r $testroot/repo gamma > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot && got checkout repo wt > /dev/null)
+	echo "modified alpha" > $testroot/wt/alpha
+	(cd $testroot/wt && got commit -m "changed alpha" > /dev/null)
+	local commit_id2=`git_show_head $testroot/repo`
+	local author_time2=`git_show_author_time $testroot/repo`
+	local tree_commit2=`git_show_tree $testroot/repo`
+
+	# cat blob by path in specific commit
+	echo "alpha" > $testroot/stdout.expected
+	got cat -r $testroot/repo -c $commit_id alpha > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo "modified alpha" > $testroot/stdout.expected
+	got cat -r $testroot/repo -c $commit_id2 alpha > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# resolve ambiguities between paths and other arguments
+	echo "new file called master" > $testroot/wt/master
+	echo "new file called $commit_id2" > $testroot/wt/$commit_id2
+	(cd $testroot/wt && got add master $commit_id2 > /dev/null)
+	(cd $testroot/wt && got commit -m "added clashing paths" > /dev/null)
+	local commit_id3=`git_show_head $testroot/repo`
+	local author_time3=`git_show_author_time $testroot/repo`
+
+	# references and object IDs override paths:
+	echo -n "tree " > $testroot/stdout.expected
+	git_show_tree $testroot/repo >> $testroot/stdout.expected
+	echo >> $testroot/stdout.expected
+	echo "numparents 1" >> $testroot/stdout.expected
+	echo "parent $commit_id2" >> $testroot/stdout.expected
+	echo "author $GOT_AUTHOR $author_time3 +0000" >> $testroot/stdout.expected
+	echo "committer $GOT_AUTHOR $author_time3 +0000" \
+		>> $testroot/stdout.expected
+	echo "messagelen 22" >> $testroot/stdout.expected
+	printf "\nadded clashing paths\n" >> $testroot/stdout.expected
+
+	for arg in master $commit_id3; do
+		got cat -r $testroot/repo $arg > $testroot/stdout
+		cmp -s $testroot/stdout.expected $testroot/stdout
+		ret="$?"
+		if [ "$ret" != "0" ]; then
+			diff -u $testroot/stdout.expected $testroot/stdout
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+	done
+
+	echo "tree $tree_commit2" > $testroot/stdout.expected
+	echo "numparents 1" >> $testroot/stdout.expected
+	echo "parent $commit_id" >> $testroot/stdout.expected
+	echo "author $GOT_AUTHOR $author_time2 +0000" >> $testroot/stdout.expected
+	echo "committer $GOT_AUTHOR $author_time2 +0000" \
+		>> $testroot/stdout.expected
+	echo "messagelen 15" >> $testroot/stdout.expected
+	printf "\nchanged alpha\n" >> $testroot/stdout.expected
+
+	got cat -r $testroot/repo $commit_id2 > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# force resolution of path 'master'
+	echo "new file called master" > $testroot/stdout.expected
+	got cat -r $testroot/repo -P master > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# force resolution of path "$commit_id2"
+	echo "new file called $commit_id2" > $testroot/stdout.expected
+	got cat -r $testroot/repo -P $commit_id2 > $testroot/stdout
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_cat_basic
+run_test test_cat_path