Commit 9d88300a8ee9f184dd0299d0c1ba8487a6c2fb1a

Edward Thomson 2022-02-05T12:35:46

fetch: support oids in fetch specs

diff --git a/src/fetch.c b/src/fetch.c
index 117c8f2..03d3845 100644
--- a/src/fetch.c
+++ b/src/fetch.c
@@ -11,6 +11,7 @@
 #include "git2/refs.h"
 #include "git2/revwalk.h"
 #include "git2/transport.h"
+#include "git2/sys/remote.h"
 
 #include "remote.h"
 #include "refspec.h"
@@ -19,7 +20,7 @@
 #include "repository.h"
 #include "refs.h"
 
-static int maybe_want(git_remote *remote, git_remote_head *head, git_odb *odb, git_refspec *tagspec, git_remote_autotag_option_t tagopt)
+static int maybe_want(git_remote *remote, git_remote_head *head, git_refspec *tagspec, git_remote_autotag_option_t tagopt)
 {
 	int match = 0, valid;
 
@@ -44,23 +45,57 @@ static int maybe_want(git_remote *remote, git_remote_head *head, git_odb *odb, g
 	if (!match)
 		return 0;
 
-	/* If we have the object, mark it so we don't ask for it */
-	if (git_odb_exists(odb, &head->oid)) {
-		head->local = 1;
+	return git_vector_insert(&remote->refs, head);
+}
+
+static int mark_local(git_remote *remote)
+{
+	git_remote_head *head;
+	git_odb *odb;
+	size_t i;
+
+	if (git_repository_odb__weakptr(&odb, remote->repo) < 0)
+		return -1;
+
+	git_vector_foreach(&remote->refs, i, head) {
+		/* If we have the object, mark it so we don't ask for it */
+		if (git_odb_exists(odb, &head->oid))
+			head->local = 1;
+		else
+			remote->need_pack = 1;
 	}
-	else
-		remote->need_pack = 1;
 
-	return git_vector_insert(&remote->refs, head);
+	return 0;
+}
+
+static int maybe_want_oid(git_remote *remote, git_refspec *spec)
+{
+	git_remote_head *oid_head;
+
+	oid_head = git__calloc(1, sizeof(git_remote_head));
+	GIT_ERROR_CHECK_ALLOC(oid_head);
+
+	git_oid_fromstr(&oid_head->oid, spec->src);
+	oid_head->name = git__strdup(spec->dst);
+	GIT_ERROR_CHECK_ALLOC(oid_head->name);
+
+	if (git_vector_insert(&remote->local_heads, oid_head) < 0 ||
+	    git_vector_insert(&remote->refs, oid_head) < 0)
+		return -1;
+
+	return 0;
 }
 
 static int filter_wants(git_remote *remote, const git_fetch_options *opts)
 {
 	git_remote_head **heads;
-	git_refspec tagspec, head;
+	git_refspec tagspec, head, *spec;
 	int error = 0;
 	git_odb *odb;
 	size_t i, heads_len;
+	unsigned int remote_caps;
+	unsigned int oid_mask = GIT_REMOTE_CAPABILITY_TIP_OID |
+	                        GIT_REMOTE_CAPABILITY_REACHABLE_OID;
 	git_remote_autotag_option_t tagopt = remote->download_tags;
 
 	if (opts && opts->download_tags != GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED)
@@ -90,14 +125,33 @@ static int filter_wants(git_remote *remote, const git_fetch_options *opts)
 	if ((error = git_repository_odb__weakptr(&odb, remote->repo)) < 0)
 		goto cleanup;
 
-	if ((error = git_remote_ls((const git_remote_head ***)&heads, &heads_len, remote)) < 0)
+	if ((error = git_remote_ls((const git_remote_head ***)&heads, &heads_len, remote)) < 0 ||
+	    (error = git_remote_capabilities(&remote_caps, remote)) < 0)
 		goto cleanup;
 
+	/* Handle remote heads */
 	for (i = 0; i < heads_len; i++) {
-		if ((error = maybe_want(remote, heads[i], odb, &tagspec, tagopt)) < 0)
-			break;
+		if ((error = maybe_want(remote, heads[i], &tagspec, tagopt)) < 0)
+			goto cleanup;
+	}
+
+	/* Handle explicitly specified OID specs */
+	git_vector_foreach(&remote->active_refspecs, i, spec) {
+		if (!git_oid__is_hexstr(spec->src))
+			continue;
+
+		if (!(remote_caps & oid_mask)) {
+			git_error_set(GIT_ERROR_INVALID, "cannot fetch a specific object from the remote repository");
+			error = -1;
+			goto cleanup;
+		}
+
+		if ((error = maybe_want_oid(remote, spec)) < 0)
+			goto cleanup;
 	}
 
+	error = mark_local(remote);
+
 cleanup:
 	git_refspec__dispose(&tagspec);
 
@@ -115,10 +169,8 @@ int git_fetch_negotiate(git_remote *remote, const git_fetch_options *opts)
 
 	remote->need_pack = 0;
 
-	if (filter_wants(remote, opts) < 0) {
-		git_error_set(GIT_ERROR_NET, "failed to filter the reference list for wants");
+	if (filter_wants(remote, opts) < 0)
 		return -1;
-	}
 
 	/* Don't try to negotiate when we don't want anything */
 	if (!remote->need_pack)
diff --git a/src/remote.c b/src/remote.c
index baec1ed..038afc6 100644
--- a/src/remote.c
+++ b/src/remote.c
@@ -1871,7 +1871,7 @@ static int update_tips_for_spec(
 	const char *log_message)
 {
 	git_refspec tagspec;
-	git_remote_head *head;
+	git_remote_head *head, oid_head;
 	git_vector update_heads;
 	int error = 0;
 	size_t i;
@@ -1885,11 +1885,27 @@ static int update_tips_for_spec(
 	if (git_vector_init(&update_heads, 16, NULL) < 0)
 		return -1;
 
+	/* Update tips based on the remote heads */
 	git_vector_foreach(refs, i, head) {
 		if (update_one_tip(&update_heads, remote, spec, head, &tagspec, tagopt, log_message, callbacks) < 0)
 			goto on_error;
 	}
 
+	/* Handle specified oid sources */
+	if (git_oid__is_hexstr(spec->src)) {
+		git_oid id;
+
+		if ((error = git_oid_fromstr(&id, spec->src)) < 0 ||
+		    (error = update_ref(remote, spec->dst, &id, log_message, callbacks)) < 0)
+			goto on_error;
+
+		git_oid_cpy(&oid_head.oid, &id);
+		oid_head.name = spec->src;
+
+		if ((error = git_vector_insert(&update_heads, &oid_head)) < 0)
+			goto on_error;
+	}
+
 	if (update_fetchhead &&
 	    (error = git_remote_write_fetchhead(remote, spec, &update_heads)) < 0)
 		goto on_error;
@@ -2107,6 +2123,17 @@ int git_remote_disconnect(git_remote *remote)
 	return 0;
 }
 
+static void free_heads(git_vector *heads)
+{
+	git_remote_head *head;
+	size_t i;
+
+	git_vector_foreach(heads, i, head) {
+		git__free(head->name);
+		git__free(head);
+	}
+}
+
 void git_remote_free(git_remote *remote)
 {
 	if (remote == NULL)
@@ -2130,6 +2157,9 @@ void git_remote_free(git_remote *remote)
 	free_refspecs(&remote->passive_refspecs);
 	git_vector_free(&remote->passive_refspecs);
 
+	free_heads(&remote->local_heads);
+	git_vector_free(&remote->local_heads);
+
 	git_push_free(remote->push);
 	git__free(remote->url);
 	git__free(remote->pushurl);
diff --git a/src/remote.h b/src/remote.h
index 0952c7c..ea9c7d1 100644
--- a/src/remote.h
+++ b/src/remote.h
@@ -27,6 +27,7 @@ struct git_remote {
 	git_vector refspecs;
 	git_vector active_refspecs;
 	git_vector passive_refspecs;
+	git_vector local_heads;
 	git_transport *transport;
 	git_repository *repo;
 	git_push *push;
diff --git a/tests/fetch/local.c b/tests/fetch/local.c
index b4583b2..20bd7ad 100644
--- a/tests/fetch/local.c
+++ b/tests/fetch/local.c
@@ -34,3 +34,34 @@ void test_fetch_local__defaults(void)
 	git_object_free(obj);
 	git_remote_free(remote);
 }
+
+void test_fetch_local__reachable_commit(void)
+{
+	git_remote *remote;
+	git_strarray refspecs;
+	git_object *obj;
+	git_oid expected_id;
+	git_str fetchhead = GIT_STR_INIT;
+	char *refspec = "+5b5b025afb0b4c913b4c338a42934a3863bf3644:refs/success";
+
+	refspecs.strings = &refspec;
+	refspecs.count = 1;
+
+	git_oid_fromstr(&expected_id, "5b5b025afb0b4c913b4c338a42934a3863bf3644");
+
+	cl_git_pass(git_remote_create(&remote, repo, "test",
+		cl_fixture("testrepo.git")));
+	cl_git_pass(git_remote_fetch(remote, &refspecs, NULL, NULL));
+
+	cl_git_pass(git_revparse_single(&obj, repo, "refs/success"));
+	cl_assert_equal_oid(&expected_id, git_object_id(obj));
+
+	cl_git_pass(git_futils_readbuffer(&fetchhead, "./fetch/.git/FETCH_HEAD"));
+	cl_assert_equal_strn(fetchhead.ptr,
+		"5b5b025afb0b4c913b4c338a42934a3863bf3644\t\t'5b5b025afb0b4c913b4c338a42934a3863bf3644' of ",
+		strlen("5b5b025afb0b4c913b4c338a42934a3863bf3644\t\t'5b5b025afb0b4c913b4c338a42934a3863bf3644' of "));
+
+	git_str_dispose(&fetchhead);
+	git_object_free(obj);
+	git_remote_free(remote);
+}
diff --git a/tests/online/fetch.c b/tests/online/fetch.c
index 2be9683..7334f7e 100644
--- a/tests/online/fetch.c
+++ b/tests/online/fetch.c
@@ -1,4 +1,5 @@
 #include "clar_libgit2.h"
+#include "futils.h"
 
 static git_repository *_repo;
 static int counter;
@@ -290,3 +291,33 @@ void test_online_fetch__redirect_config(void)
 	cl_git_fail(do_redirected_fetch(_remote_redirect_initial, "initial", "false"));
 	cl_git_fail(do_redirected_fetch(_remote_redirect_subsequent, "subsequent", "false"));
 }
+
+void test_online_fetch__reachable_commit(void)
+{
+	git_remote *remote;
+	git_strarray refspecs;
+	git_object *obj;
+	git_oid expected_id;
+	git_str fetchhead = GIT_STR_INIT;
+	char *refspec = "+2c349335b7f797072cf729c4f3bb0914ecb6dec9:refs/success";
+
+	refspecs.strings = &refspec;
+	refspecs.count = 1;
+
+	git_oid_fromstr(&expected_id, "2c349335b7f797072cf729c4f3bb0914ecb6dec9");
+
+	cl_git_pass(git_remote_create(&remote, _repo, "test",
+		"https://github.com/libgit2/TestGitRepository"));
+	cl_git_pass(git_remote_fetch(remote, &refspecs, NULL, NULL));
+
+	cl_git_pass(git_revparse_single(&obj, _repo, "refs/success"));
+	cl_assert_equal_oid(&expected_id, git_object_id(obj));
+
+	cl_git_pass(git_futils_readbuffer(&fetchhead, "./fetch/.git/FETCH_HEAD"));
+	cl_assert_equal_s(fetchhead.ptr,
+		"2c349335b7f797072cf729c4f3bb0914ecb6dec9\t\t'2c349335b7f797072cf729c4f3bb0914ecb6dec9' of https://github.com/libgit2/TestGitRepository\n");
+
+	git_str_dispose(&fetchhead);
+	git_object_free(obj);
+	git_remote_free(remote);
+}