Commit 4b6c9460c99283f193f32bd5177bb09047abacf3

Stefan Sperling 2020-03-05T08:41:12

be helpful when users try to check out work trees without a known branch Provide a useful error message in such cases and explicitly document intentional restrictions in the got(1) man page. Prompted by a question from Adam Steen via bsd.network https://bsd.network/@adams/103768951483318235

diff --git a/got/got.1 b/got/got.1
index 05a4896..ed10056 100644
--- a/got/got.1
+++ b/got/got.1
@@ -177,6 +177,20 @@ An abbreviated hash argument will be expanded to a full SHA1 hash
 automatically, provided the abbreviation is unique.
 If this option is not specified, the most recent commit on the selected
 branch will be used.
+.Pp
+If the specified
+.Ar commit
+is not contained in the selected branch, a different branch which contains
+this commit must be specified with the
+.Fl b
+option.
+If no such branch is known a new branch must be created for this
+commit with
+.Cm got branch
+before
+.Cm got checkout
+can be used.
+Checking out work trees with an unknown branch is intentionally not supported.
 .It Fl p Ar path-prefix
 Restrict the work tree to a subset of the repository's tree hierarchy.
 Only files beneath the specified
diff --git a/got/got.c b/got/got.c
index 021418d..fc50380 100644
--- a/got/got.c
+++ b/got/got.c
@@ -957,6 +957,30 @@ done:
 }
 
 static const struct got_error *
+checkout_ancestry_error(struct got_reference *ref, const char *commit_id_str)
+{
+	static char msg[512];
+	const char *branch_name;
+
+	if (got_ref_is_symbolic(ref))
+		branch_name = got_ref_get_symref_target(ref);
+	else
+		branch_name = got_ref_get_name(ref);
+
+	if (strncmp("refs/heads/", branch_name, 11) == 0)
+		branch_name += 11;
+
+	snprintf(msg, sizeof(msg),
+	    "target commit is not contained in branch '%s'; "
+	    "the branch to use must be specified with -b; "
+	    "if necessary a new branch can be created for "
+	    "this commit with 'got branch -c %s BRANCH_NAME'",
+	    branch_name, commit_id_str);
+
+	return got_error_msg(GOT_ERR_ANCESTRY, msg);
+}
+
+static const struct got_error *
 cmd_checkout(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
@@ -1116,11 +1140,20 @@ cmd_checkout(int argc, char *argv[])
 		    got_worktree_get_base_commit_id(worktree), 0, repo);
 		if (error != NULL) {
 			free(commit_id);
+			if (error->code == GOT_ERR_ANCESTRY) {
+				error = checkout_ancestry_error(
+				    head_ref, commit_id_str);
+			}
 			goto done;
 		}
 		error = check_same_branch(commit_id, head_ref, NULL, repo);
-		if (error)
+		if (error) {
+			if (error->code == GOT_ERR_ANCESTRY) {
+				error = checkout_ancestry_error(
+				    head_ref, commit_id_str);
+			}
 			goto done;
+		}
 		error = got_worktree_set_base_commit_id(worktree, repo,
 		    commit_id);
 		free(commit_id);
diff --git a/regress/cmdline/checkout.sh b/regress/cmdline/checkout.sh
index 6859b9b..6d3a99a 100755
--- a/regress/cmdline/checkout.sh
+++ b/regress/cmdline/checkout.sh
@@ -197,8 +197,14 @@ function test_checkout_commit_from_wrong_branch {
 		return 1
 	fi
 
-	echo  "got: target commit is on a different branch" \
+	echo -n "got: target commit is not contained in branch 'master'; " \
 		> $testroot/stderr.expected
+	echo -n "the branch to use must be specified with -b; if necessary " \
+		>> $testroot/stderr.expected
+	echo -n "a new branch can be created for this commit with "\
+		>> $testroot/stderr.expected
+	echo "'got branch -c $head_rev BRANCH_NAME'" \
+		>> $testroot/stderr.expected
 	cmp -s $testroot/stderr.expected $testroot/stderr
 	ret="$?"
 	if [ "$ret" != "0" ]; then