Commit fb585d01205e1a0b580a39f9cbdc829ff63e02c9

Edward Thomson 2017-07-31T00:58:58

Merge branch '4233'

diff --git a/AUTHORS b/AUTHORS
index 61e2113..458ff06 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -22,6 +22,7 @@ Dmitry Kakurin
 Dmitry Kovega
 Emeric Fermas
 Emmanuel Rodriguez
+Eric Myhre
 Florian Forster
 Holger Weiss
 Ingmar Vanhassel
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b2eba3..43724b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@ v0.26 + 1
 
 ### API additions
 
+* `git_remote_create_detached()` creates a remote that is not associated
+  to any repository (and does not apply configuration like 'insteadof' rules).
+  This is mostly useful for e.g. emulating `git ls-remote` behavior.
+
 ### API removals
 
 ### Breaking API changes
diff --git a/include/git2/remote.h b/include/git2/remote.h
index e9e4e5b..b1e04d5 100644
--- a/include/git2/remote.h
+++ b/include/git2/remote.h
@@ -76,6 +76,24 @@ GIT_EXTERN(int) git_remote_create_anonymous(
 		const char *url);
 
 /**
+ * Create a remote without a connected local repo
+ *
+ * Create a remote with the given url in-memory. You can use this when
+ * you have a URL instead of a remote's name.
+ *
+ * Contrasted with git_remote_create_anonymous, a detached remote
+ * will not consider any repo configuration values (such as insteadof url
+ * substitutions).
+ *
+ * @param out pointer to the new remote objects
+ * @param url the remote repository's URL
+ * @return 0 or an error code
+ */
+GIT_EXTERN(int) git_remote_create_detached(
+		git_remote **out,
+		const char *url);
+
+/**
  * Get the information for a particular remote
  *
  * The name will be checked for validity.
diff --git a/src/remote.c b/src/remote.c
index bd8b3cf..87fc17d 100644
--- a/src/remote.c
+++ b/src/remote.c
@@ -197,10 +197,10 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n
 	git_buf var = GIT_BUF_INIT;
 	int error = -1;
 
-	/* name is optional */
-	assert(out && repo && url);
+	/* repo, name, and fetch are optional */
+	assert(out && url);
 
-	if ((error = git_repository_config_snapshot(&config_ro, repo)) < 0)
+	if (repo && (error = git_repository_config_snapshot(&config_ro, repo)) < 0)
 		return error;
 
 	remote = git__calloc(1, sizeof(git_remote));
@@ -212,7 +212,11 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n
 		(error = canonicalize_url(&canonical_url, url)) < 0)
 		goto on_error;
 
-	remote->url = apply_insteadof(config_ro, canonical_url.ptr, GIT_DIRECTION_FETCH);
+	if (repo) {
+		remote->url = apply_insteadof(config_ro, canonical_url.ptr, GIT_DIRECTION_FETCH);
+	} else {
+		remote->url = git__strdup(canonical_url.ptr);
+	}
 	GITERR_CHECK_ALLOC(remote->url);
 
 	if (name != NULL) {
@@ -222,8 +226,9 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n
 		if ((error = git_buf_printf(&var, CONFIG_URL_FMT, name)) < 0)
 			goto on_error;
 
-		if ((error = git_repository_config__weakptr(&config_rw, repo)) < 0 ||
-			(error = git_config_set_string(config_rw, var.ptr, canonical_url.ptr)) < 0)
+		if (repo &&
+			((error = git_repository_config__weakptr(&config_rw, repo)) < 0 ||
+			(error = git_config_set_string(config_rw, var.ptr, canonical_url.ptr)) < 0))
 			goto on_error;
 	}
 
@@ -235,7 +240,7 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n
 		if (name && (error = write_add_refspec(repo, name, fetch, true)) < 0)
 			goto on_error;
 
-		if ((error = lookup_remote_prune_config(remote, config_ro, name)) < 0)
+		if (repo && (error = lookup_remote_prune_config(remote, config_ro, name)) < 0)
 			goto on_error;
 
 		/* Move the data over to where the matching functions can find them */
@@ -330,6 +335,11 @@ int git_remote_create_anonymous(git_remote **out, git_repository *repo, const ch
 	return create_internal(out, repo, NULL, url, NULL);
 }
 
+int git_remote_create_detached(git_remote **out, const char *url)
+{
+	return create_internal(out, NULL, NULL, url, NULL);
+}
+
 int git_remote_dup(git_remote **dest, git_remote *source)
 {
 	size_t i;
@@ -674,7 +684,9 @@ int git_remote_connect(git_remote *remote, git_direction direction, const git_re
 	url = git_remote__urlfordirection(remote, direction);
 	if (url == NULL) {
 		giterr_set(GITERR_INVALID,
-			"Malformed remote '%s' - missing URL", remote->name);
+			"Malformed remote '%s' - missing %s URL",
+			remote->name ? remote->name : "(anonymous)",
+			direction == GIT_DIRECTION_FETCH ? "fetch" : "push");
 		return -1;
 	}
 
@@ -859,6 +871,11 @@ int git_remote_download(git_remote *remote, const git_strarray *refspecs, const 
 
 	assert(remote);
 
+	if (!remote->repo) {
+		giterr_set(GITERR_INVALID, "cannot download detached remote");
+		return -1;
+	}
+
 	if (opts) {
 		GITERR_CHECK_VERSION(&opts->callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks");
 		cbs = &opts->callbacks;
@@ -2344,6 +2361,11 @@ int git_remote_upload(git_remote *remote, const git_strarray *refspecs, const gi
 
 	assert(remote);
 
+	if (!remote->repo) {
+		giterr_set(GITERR_INVALID, "cannot download detached remote");
+		return -1;
+	}
+
 	if (opts) {
 		cbs = &opts->callbacks;
 		custom_headers = &opts->custom_headers;
@@ -2403,6 +2425,13 @@ int git_remote_push(git_remote *remote, const git_strarray *refspecs, const git_
 	const git_strarray *custom_headers = NULL;
 	const git_proxy_options *proxy = NULL;
 
+	assert(remote);
+
+	if (!remote->repo) {
+		giterr_set(GITERR_INVALID, "cannot download detached remote");
+		return -1;
+	}
+
 	if (opts) {
 		GITERR_CHECK_VERSION(&opts->callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks");
 		cbs = &opts->callbacks;
diff --git a/tests/online/remotes.c b/tests/online/remotes.c
index a86f2d9..d79eb1f 100644
--- a/tests/online/remotes.c
+++ b/tests/online/remotes.c
@@ -1,12 +1,13 @@
 #include "clar_libgit2.h"
 
-static const char *refspec = "refs/heads/first-merge:refs/remotes/origin/first-merge";
+#define URL "git://github.com/libgit2/TestGitRepository"
+#define REFSPEC "refs/heads/first-merge:refs/remotes/origin/first-merge"
 
 static int remote_single_branch(git_remote **out, git_repository *repo, const char *name, const char *url, void *payload)
 {
 	GIT_UNUSED(payload);
 
-	cl_git_pass(git_remote_create_with_fetchspec(out, repo, name, url, refspec));
+	cl_git_pass(git_remote_create_with_fetchspec(out, repo, name, url, REFSPEC));
 
 	return 0;
 }
@@ -22,7 +23,7 @@ void test_online_remotes__single_branch(void)
 	opts.remote_cb = remote_single_branch;
 	opts.checkout_branch = "first-merge";
 
-	cl_git_pass(git_clone(&repo, "git://github.com/libgit2/TestGitRepository", "./single-branch", &opts));
+	cl_git_pass(git_clone(&repo, URL, "./single-branch", &opts));
 	cl_git_pass(git_reference_list(&refs, repo));
 
 	for (i = 0; i < refs.count; i++) {
@@ -37,7 +38,7 @@ void test_online_remotes__single_branch(void)
 	cl_git_pass(git_remote_get_fetch_refspecs(&refs, remote));
 
 	cl_assert_equal_i(1, refs.count);
-	cl_assert_equal_s(refspec, refs.strings[0]);
+	cl_assert_equal_s(REFSPEC, refs.strings[0]);
 
 	git_strarray_free(&refs);
 	git_remote_free(remote);
@@ -51,5 +52,76 @@ void test_online_remotes__restricted_refspecs(void)
 
 	opts.remote_cb = remote_single_branch;
 
-	cl_git_fail_with(GIT_EINVALIDSPEC, git_clone(&repo, "git://github.com/libgit2/TestGitRepository", "./restrict-refspec", &opts));
+	cl_git_fail_with(GIT_EINVALIDSPEC, git_clone(&repo, URL, "./restrict-refspec", &opts));
+}
+
+void test_online_remotes__detached_remote_fails_downloading(void)
+{
+	git_remote *remote;
+
+	cl_git_pass(git_remote_create_detached(&remote, URL));
+	cl_git_pass(git_remote_connect(remote, GIT_DIRECTION_FETCH, NULL, NULL, NULL));
+	cl_git_fail(git_remote_download(remote, NULL, NULL));
+
+	git_remote_free(remote);
+}
+
+void test_online_remotes__detached_remote_fails_uploading(void)
+{
+	git_remote *remote;
+
+	cl_git_pass(git_remote_create_detached(&remote, URL));
+	cl_git_pass(git_remote_connect(remote, GIT_DIRECTION_FETCH, NULL, NULL, NULL));
+	cl_git_fail(git_remote_upload(remote, NULL, NULL));
+
+	git_remote_free(remote);
+}
+
+void test_online_remotes__detached_remote_fails_pushing(void)
+{
+	git_remote *remote;
+
+	cl_git_pass(git_remote_create_detached(&remote, URL));
+	cl_git_pass(git_remote_connect(remote, GIT_DIRECTION_FETCH, NULL, NULL, NULL));
+	cl_git_fail(git_remote_push(remote, NULL, NULL));
+
+	git_remote_free(remote);
+}
+
+void test_online_remotes__detached_remote_succeeds_ls(void)
+{
+	const char *refs[] = {
+	    "HEAD",
+	    "refs/heads/first-merge",
+	    "refs/heads/master",
+	    "refs/heads/no-parent",
+	    "refs/tags/annotated_tag",
+	    "refs/tags/annotated_tag^{}",
+	    "refs/tags/blob",
+	    "refs/tags/commit_tree",
+	    "refs/tags/nearly-dangling",
+	};
+	const git_remote_head **heads;
+	git_remote *remote;
+	size_t i, j, n;
+
+	cl_git_pass(git_remote_create_detached(&remote, URL));
+	cl_git_pass(git_remote_connect(remote, GIT_DIRECTION_FETCH, NULL, NULL, NULL));
+	cl_git_pass(git_remote_ls(&heads, &n, remote));
+
+	cl_assert_equal_sz(n, 9);
+	for (i = 0; i < n; i++) {
+		char found = false;
+
+		for (j = 0; j < ARRAY_SIZE(refs); j++) {
+			if (!strcmp(heads[i]->name, refs[j])) {
+				found = true;
+				break;
+			}
+		}
+
+		cl_assert_(found, heads[i]->name);
+	}
+
+	git_remote_free(remote);
 }