Commit a9b5270b9a015dfbba572c9a3840432da90c7476

Patrick Steinhardt 2019-10-07T21:18:19

examples: checkout: implement guess heuristic for remote branches

diff --git a/examples/checkout.c b/examples/checkout.c
index d552eec..2aa1fbf 100644
--- a/examples/checkout.c
+++ b/examples/checkout.c
@@ -112,9 +112,10 @@ static void print_perf_data(const git_checkout_perfdata *perfdata, void *payload
  * This is the main "checkout <branch>" function, responsible for performing
  * a branch-based checkout.
  */
-static int perform_checkout_ref(git_repository *repo, git_annotated_commit *target, checkout_options *opts)
+static int perform_checkout_ref(git_repository *repo, git_annotated_commit *target, const char *target_ref, checkout_options *opts)
 {
 	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_reference *ref = NULL, *branch = NULL;
 	git_commit *target_commit = NULL;
 	int err;
 
@@ -156,10 +157,25 @@ static int perform_checkout_ref(git_repository *repo, git_annotated_commit *targ
 	 * we might need to detach HEAD.
 	 */
 	if (git_annotated_commit_ref(target)) {
-		err = git_repository_set_head(repo, git_annotated_commit_ref(target));
+		const char *target_head;
+
+		if ((err = git_reference_lookup(&ref, repo, git_annotated_commit_ref(target))) < 0)
+			goto error;
+
+		if (git_reference_is_remote(ref)) {
+			if ((err = git_branch_create_from_annotated(&branch, repo, target_ref, target, 0)) < 0)
+				goto error;
+			target_head = git_reference_name(branch);
+		} else {
+			target_head = git_annotated_commit_ref(target);
+		}
+
+		err = git_repository_set_head(repo, target_head);
 	} else {
 		err = git_repository_set_head_detached_from_annotated(repo, target);
 	}
+
+error:
 	if (err != 0) {
 		fprintf(stderr, "failed to update HEAD reference: %s\n", git_error_last()->message);
 		goto cleanup;
@@ -167,10 +183,67 @@ static int perform_checkout_ref(git_repository *repo, git_annotated_commit *targ
 
 cleanup:
 	git_commit_free(target_commit);
+	git_reference_free(branch);
+	git_reference_free(ref);
 
 	return err;
 }
 
+/**
+ * This corresponds to `git switch --guess`: if a given ref does
+ * not exist, git will by default try to guess the reference by
+ * seeing whether any remote has a branch called <ref>. If there
+ * is a single remote only that has it, then it is assumed to be
+ * the desired reference and a local branch is created for it.
+ *
+ * The following is a simplified implementation. It will not try
+ * to check whether the ref is unique across all remotes.
+ */
+static int guess_refish(git_annotated_commit **out, git_repository *repo, const char *ref)
+{
+	git_strarray remotes = { NULL, 0 };
+	git_reference *remote_ref = NULL;
+	int error;
+	size_t i;
+
+	if ((error = git_remote_list(&remotes, repo)) < 0)
+		goto out;
+
+	for (i = 0; i < remotes.count; i++) {
+		char *refname = NULL;
+		size_t reflen;
+
+		reflen = snprintf(refname, 0, "refs/remotes/%s/%s", remotes.strings[i], ref);
+		if ((refname = malloc(reflen + 1)) == NULL) {
+			error = -1;
+			goto next;
+		}
+		snprintf(refname, reflen + 1, "refs/remotes/%s/%s", remotes.strings[i], ref);
+
+		if ((error = git_reference_lookup(&remote_ref, repo, refname)) < 0)
+			goto next;
+
+		break;
+next:
+		free(refname);
+		if (error < 0 && error != GIT_ENOTFOUND)
+			break;
+	}
+
+	if (!remote_ref) {
+		error = GIT_ENOTFOUND;
+		goto out;
+	}
+
+	if ((error = git_annotated_commit_from_ref(out, repo, remote_ref)) < 0)
+		goto out;
+
+out:
+	git_reference_free(remote_ref);
+	git_strarray_free(&remotes);
+	return error;
+}
+
 /** That example's entry point */
 int lg2_checkout(git_repository *repo, int argc, char **argv)
 {
@@ -207,12 +280,12 @@ int lg2_checkout(git_repository *repo, int argc, char **argv)
 		/**
 		 * Try to resolve a "refish" argument to a target libgit2 can use
 		 */
-		err = resolve_refish(&checkout_target, repo, args.argv[args.pos]);
-		if (err != 0) {
+		if ((err = resolve_refish(&checkout_target, repo, args.argv[args.pos])) < 0 &&
+		    (err = guess_refish(&checkout_target, repo, args.argv[args.pos])) < 0) {
 			fprintf(stderr, "failed to resolve %s: %s\n", args.argv[args.pos], git_error_last()->message);
 			goto cleanup;
 		}
-		err = perform_checkout_ref(repo, checkout_target, &opts);
+		err = perform_checkout_ref(repo, checkout_target, args.argv[args.pos], &opts);
 	}
 
 cleanup: