threads / patch / 34637

patch, 5 partswork around "transport-take-over" hack

Subject: [PATCH 0/5] work around "transport-take-over" hack

## tl;dr

6 messages between Aug 7, 2013 and Aug 7, 2013. Diffs are folded; open one to read it.

replies: 5people: 1as markdown or json

Junio C Hamano· Aug 7, 2013, 23:30 UTC · lore

"git fetch" sometimes needs to make a real request to the transport after a single "fetch_refs" request, in order to follow tags that the other end should have sent as part of the primary transfer with "follow-tags" request.

However, a transport that defines "connect" has a gross hack that destroys its internal state to render it unusable after processing a single request, breaking this.

This is my attempt to work it around. I am not too proud of it, but after trying two other approaches to fix it closer to the real cause (i.e. the implementation of Git-aware transport helper) and seeing that both of them ended up being even less appealing and not worth posting, I think this is probably the best fix to the issue.

Junio C Hamano (5):
  t5802: add test for connect helper
  fetch: rename file-scope global "transport" to "gtransport"
  fetch: refactor code that prepares a transport
  fetch: refactor code that fetches leftover tags
  fetch: work around "transport-take-over" hack
 builtin/fetch.c           | 85 ++++++++++++++++++++++++++++++-----------------
 t/t5802-connect-helper.sh | 72 +++++++++++++++++++++++++++++++++++++++
 transport.c               |  2 ++
 transport.h               |  6 ++++
 4 files changed, 134 insertions(+), 31 deletions(-)
 create mode 100755 t/t5802-connect-helper.sh
-- 
1.8.4-rc1-210-gf6d87e2
Junio C Hamano· Aug 7, 2013, 23:30 UTC · re: Junio C Hamano · lore

[PATCH 1/5] t5802: add test for connect helper

This is an attempt to reproduce a problem reported for a third-party custom "connect" remote helper. The conjecture is that sometimes "git fetch" wants to make two connections (one for the primary transfer with 'follow-tags' option set, and then after noticing that some tags are not packed because the primary transfer did not have to send any commit that is pointed by them, another to explicitly ask for the missing tags), and their "connect" helper is not called in the second request, breaking the "fetch" as a whole.

Unfortunately this test script does not trigger the alleged failure and happily passes when talking to upload-pack from git-core (see patch 5/5 for details).

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 t/t5802-connect-helper.sh | 72 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 72 insertions(+)
 create mode 100755 t/t5802-connect-helper.sh
Show changes to t/t5802-connect-helper.sh +72 −0
diff --git a/t/t5802-connect-helper.sh b/t/t5802-connect-helper.sh
new file mode 100755
index 0000000..878faf2
--- /dev/null
+++ b/t/t5802-connect-helper.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+test_description='ext::cmd remote "connect" helper'
+. ./test-lib.sh
+
+test_expect_success setup '
+	test_tick &&
+	git commit --allow-empty -m initial &&
+	test_tick &&
+	git commit --allow-empty -m second &&
+	test_tick &&
+	git commit --allow-empty -m third &&
+	test_tick &&
+	git tag -a -m "tip three" three &&
+
+	test_tick &&
+	git commit --allow-empty -m fourth
+'
+
+test_expect_success clone '
+	cmd=$(echo "echo >&2 ext::sh invoked && %S .." | sed -e "s/ /% /g") &&
+	git clone "ext::sh -c %S% ." dst &&
+	git for-each-ref refs/heads/ refs/tags/ >expect &&
+	(
+		cd dst &&
+		git config remote.origin.url "ext::sh -c $cmd" &&
+		git for-each-ref refs/heads/ refs/tags/
+	) >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'update following tag' '
+	test_tick &&
+	git commit --allow-empty -m fifth &&
+	test_tick &&
+	git tag -a -m "tip five" five &&
+	git for-each-ref refs/heads/ refs/tags/ >expect &&
+	(
+		cd dst &&
+		git pull &&
+		git for-each-ref refs/heads/ refs/tags/ >../actual
+	) &&
+	test_cmp expect actual
+'
+
+test_expect_success 'update backfilled tag' '
+	test_tick &&
+	git commit --allow-empty -m sixth &&
+	test_tick &&
+	git tag -a -m "tip two" two three^1 &&
+	git for-each-ref refs/heads/ refs/tags/ >expect &&
+	(
+		cd dst &&
+		git pull &&
+		git for-each-ref refs/heads/ refs/tags/ >../actual
+	) &&
+	test_cmp expect actual
+'
+
+test_expect_success 'update backfilled tag without primary transfer' '
+	test_tick &&
+	git tag -a -m "tip one " one two^1 &&
+	git for-each-ref refs/heads/ refs/tags/ >expect &&
+	(
+		cd dst &&
+		git pull &&
+		git for-each-ref refs/heads/ refs/tags/ >../actual
+	) &&
+	test_cmp expect actual
+'
+
+test_done
-- 
1.8.4-rc1-210-gf6d87e2
Junio C Hamano· Aug 7, 2013, 23:30 UTC · re: Junio C Hamano · lore

[PATCH 2/5] fetch: rename file-scope global "transport" to "gtransport"

Although many functions in this file take a "struct transport" as a parameter, "fetch_one()" assigns to the global singleton instance which is a file-scope static, in order to allow a parameterless signal handler unlock_pack() to access it.

Rename the variable to gtransport to make sure these uses stand out.
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/fetch.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
Show changes to builtin/fetch.c +11 −11
diff --git a/builtin/fetch.c b/builtin/fetch.c
index d784b2e..636b47f 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -36,7 +36,7 @@ static int tags = TAGS_DEFAULT, unshallow;
 static const char *depth;
 static const char *upload_pack;
 static struct strbuf default_rla = STRBUF_INIT;
-static struct transport *transport;
+static struct transport *gtransport;
 static const char *submodule_prefix = "";
 static const char *recurse_submodules_default;
 
@@ -95,8 +95,8 @@ static struct option builtin_fetch_options[] = {
 
 static void unlock_pack(void)
 {
-	if (transport)
-		transport_unlock_pack(transport);
+	if (gtransport)
+		transport_unlock_pack(gtransport);
 }
 
 static void unlock_pack_on_signal(int signo)
@@ -818,13 +818,13 @@ static int do_fetch(struct transport *transport,
 
 static void set_option(const char *name, const char *value)
 {
-	int r = transport_set_option(transport, name, value);
+	int r = transport_set_option(gtransport, name, value);
 	if (r < 0)
 		die(_("Option \"%s\" value \"%s\" is not valid for %s"),
-			name, value, transport->url);
+		    name, value, gtransport->url);
 	if (r > 0)
 		warning(_("Option \"%s\" is ignored for %s\n"),
-			name, transport->url);
+			name, gtransport->url);
 }
 
 static int get_one_remote_for_fetch(struct remote *remote, void *priv)
@@ -949,8 +949,8 @@ static int fetch_one(struct remote *remote, int argc, const char **argv)
 		die(_("No remote repository specified.  Please, specify either a URL or a\n"
 		    "remote name from which new revisions should be fetched."));
 
-	transport = transport_get(remote, NULL);
-	transport_set_verbosity(transport, verbosity, progress);
+	gtransport = transport_get(remote, NULL);
+	transport_set_verbosity(gtransport, verbosity, progress);
 	if (upload_pack)
 		set_option(TRANS_OPT_UPLOADPACK, upload_pack);
 	if (keep)
@@ -983,10 +983,10 @@ static int fetch_one(struct remote *remote, int argc, const char **argv)
 	sigchain_push_common(unlock_pack_on_signal);
 	atexit(unlock_pack);
 	refspec = parse_fetch_refspec(ref_nr, refs);
-	exit_code = do_fetch(transport, refspec, ref_nr);
+	exit_code = do_fetch(gtransport, refspec, ref_nr);
 	free_refspec(ref_nr, refspec);
-	transport_disconnect(transport);
-	transport = NULL;
+	transport_disconnect(gtransport);
+	gtransport = NULL;
 	return exit_code;
 }
 
-- 
1.8.4-rc1-210-gf6d87e2
Junio C Hamano· Aug 7, 2013, 23:30 UTC · re: Junio C Hamano · lore

[PATCH 3/5] fetch: refactor code that prepares a transport

Make a helper function prepare_transport() that returns a transport to talk to a given remote.

The set_option() helper that used to always affect the file-scope global "gtransport" now takes a transport as its parameter.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/fetch.c | 46 ++++++++++++++++++++++++++--------------------
 1 file changed, 26 insertions(+), 20 deletions(-)
Show changes to builtin/fetch.c +26 −20
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 636b47f..39a3fc8 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -720,6 +720,31 @@ static int truncate_fetch_head(void)
 	return 0;
 }
 
+static void set_option(struct transport *transport, const char *name, const char *value)
+{
+	int r = transport_set_option(transport, name, value);
+	if (r < 0)
+		die(_("Option \"%s\" value \"%s\" is not valid for %s"),
+		    name, value, transport->url);
+	if (r > 0)
+		warning(_("Option \"%s\" is ignored for %s\n"),
+			name, transport->url);
+}
+
+struct transport *prepare_transport(struct remote *remote)
+{
+	struct transport *transport;
+	transport = transport_get(remote, NULL);
+	transport_set_verbosity(transport, verbosity, progress);
+	if (upload_pack)
+		set_option(transport, TRANS_OPT_UPLOADPACK, upload_pack);
+	if (keep)
+		set_option(transport, TRANS_OPT_KEEP, "yes");
+	if (depth)
+		set_option(transport, TRANS_OPT_DEPTH, depth);
+	return transport;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *refs, int ref_count)
 {
@@ -816,17 +841,6 @@ static int do_fetch(struct transport *transport,
 	return retcode;
 }
 
-static void set_option(const char *name, const char *value)
-{
-	int r = transport_set_option(gtransport, name, value);
-	if (r < 0)
-		die(_("Option \"%s\" value \"%s\" is not valid for %s"),
-		    name, value, gtransport->url);
-	if (r > 0)
-		warning(_("Option \"%s\" is ignored for %s\n"),
-			name, gtransport->url);
-}
-
 static int get_one_remote_for_fetch(struct remote *remote, void *priv)
 {
 	struct string_list *list = priv;
@@ -949,15 +963,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv)
 		die(_("No remote repository specified.  Please, specify either a URL or a\n"
 		    "remote name from which new revisions should be fetched."));
 
-	gtransport = transport_get(remote, NULL);
-	transport_set_verbosity(gtransport, verbosity, progress);
-	if (upload_pack)
-		set_option(TRANS_OPT_UPLOADPACK, upload_pack);
-	if (keep)
-		set_option(TRANS_OPT_KEEP, "yes");
-	if (depth)
-		set_option(TRANS_OPT_DEPTH, depth);
-
+	gtransport = prepare_transport(remote);
 	if (argc > 0) {
 		int j = 0;
 		refs = xcalloc(argc + 1, sizeof(const char *));
-- 
1.8.4-rc1-210-gf6d87e2
Junio C Hamano· Aug 7, 2013, 23:30 UTC · re: Junio C Hamano · lore

[PATCH 4/5] fetch: refactor code that fetches leftover tags

Usually the upload-pack process running on the other side will give us all the reachable tags we need during the primary object transfer in do_fetch(). If that does not happen (e.g. the other side may be running a third-party implementation of upload-pack), we will run another fetch to pick up leftover tags that we know point at the commits reachable from our updated tips.

Separate out the code to run this second fetch into a helper function.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/fetch.c | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)
Show changes to builtin/fetch.c +9 −5
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 39a3fc8..0b21f07 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -745,6 +745,13 @@ struct transport *prepare_transport(struct remote *remote)
 	return transport;
 }
 
+static void backfill_tags(struct transport *transport, struct ref *ref_map)
+{
+	transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
+	transport_set_option(transport, TRANS_OPT_DEPTH, "0");
+	fetch_refs(transport, ref_map);
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *refs, int ref_count)
 {
@@ -828,11 +835,8 @@ static int do_fetch(struct transport *transport,
 		struct ref **tail = &ref_map;
 		ref_map = NULL;
 		find_non_local_tags(transport, &ref_map, &tail);
-		if (ref_map) {
-			transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
-			transport_set_option(transport, TRANS_OPT_DEPTH, "0");
-			fetch_refs(transport, ref_map);
-		}
+		if (ref_map)
+			backfill_tags(transport, ref_map);
 		free_refs(ref_map);
 	}
 
-- 
1.8.4-rc1-210-gf6d87e2
Junio C Hamano· Aug 7, 2013, 23:30 UTC · re: Junio C Hamano · lore

[PATCH 5/5] fetch: work around "transport-take-over" hack

A Git-aware "connect" transport allows the "transport_take_over" to redirect generic transport requests like fetch(), push_refs() and get_refs_list() to the native Git transport handling methods. The take-over process replaces transport->data with a fake data that these method implementations understand.

While this hack works OK for a single request, it breaks when the transport needs to make more than one requests. transport->data that used to hold necessary information for the specific helper to work correctly is destroyed during the take-over process.

One codepath that this matters is "git fetch" in auto-follow mode; when it does not get all the tags that ought to point at the history it got (which can be determined by looking at the peeled tags in the initial advertisement) from the primary transfer, it internally makes a second request to complete the fetch. Because "take-over" hack has already destroyed the data necessary to talk to the transport helper by the time this happens, the second request cannot make a request to the helper to make another connection to fetch these additional tags.

Mark such a transport as "cannot_reuse", and use a separate transport to perform the backfill fetch in order to work around this breakage.

Note that this problem does not manifest itself when running t5802, because our upload-pack gives you all the necessary auto-followed tags during the primary transfer. You would need to step through "git fetch" in a debugger, stop immediately after the primary transfer finishes and writes these auto-followed tags, remove the tag references and repack/prune the repository to convince the "find-non-local-tags" procedure that the primary transfer failed to give us all the necessary tags, and then let it continue, in order to trigger the bug in the secondary transfer this patch fixes.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/fetch.c | 13 +++++++++++++
 transport.c     |  2 ++
 transport.h     |  6 ++++++
 3 files changed, 21 insertions(+)
Show changes to 3 files +21 −0

builtin/fetch.c, transport.c, transport.h

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 0b21f07..57ab7e4 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -37,6 +37,7 @@ static const char *depth;
 static const char *upload_pack;
 static struct strbuf default_rla = STRBUF_INIT;
 static struct transport *gtransport;
+static struct transport *gsecondary;
 static const char *submodule_prefix = "";
 static const char *recurse_submodules_default;
 
@@ -97,6 +98,8 @@ static void unlock_pack(void)
 {
 	if (gtransport)
 		transport_unlock_pack(gtransport);
+	if (gsecondary)
+		transport_unlock_pack(gsecondary);
 }
 
 static void unlock_pack_on_signal(int signo)
@@ -747,9 +750,19 @@ struct transport *prepare_transport(struct remote *remote)
 
 static void backfill_tags(struct transport *transport, struct ref *ref_map)
 {
+	if (transport->cannot_reuse) {
+		gsecondary = prepare_transport(transport->remote);
+		transport = gsecondary;
+	}
+
 	transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
 	transport_set_option(transport, TRANS_OPT_DEPTH, "0");
 	fetch_refs(transport, ref_map);
+
+	if (gsecondary) {
+		transport_disconnect(gsecondary);
+		gsecondary = NULL;
+	}
 }
 
 static int do_fetch(struct transport *transport,
diff --git a/transport.c b/transport.c
index e15db98..de25588 100644
--- a/transport.c
+++ b/transport.c
@@ -875,6 +875,8 @@ void transport_take_over(struct transport *transport,
 	transport->push_refs = git_transport_push;
 	transport->disconnect = disconnect_git;
 	transport->smart_options = &(data->options);
+
+	transport->cannot_reuse = 1;
 }
 
 static int is_local(const char *url)
diff --git a/transport.h b/transport.h
index ea70ea7..96e0ede 100644
--- a/transport.h
+++ b/transport.h
@@ -27,6 +27,12 @@ struct transport {
 	 */
 	unsigned got_remote_refs : 1;
 
+	/*
+	 * Transports that call take-over destroys the data specific to
+	 * the transport type while doing so, and cannot be reused.
+	 */
+	unsigned cannot_reuse : 1;
+
 	/**
 	 * Returns 0 if successful, positive if the option is not
 	 * recognized or is inapplicable, and negative if the option
-- 
1.8.4-rc1-210-gf6d87e2

← back to recent threads