threads / patch / 53171

v6, 2 partsremote.c: fix %(push) for triangular workflows

Subject: [PATCH v6 1/2] remote.c: fix %(push) for triangular workflows

## tl;dr

3 messages between Apr 6, 2020 and Apr 6, 2020. Diffs are folded; open one to read it.

replies: 2people: 1as markdown or json

Damien Robert· Apr 6, 2020, 17:56 UTC · lore

[RFC PATCH v4 0/2] %(push) and %(push:remoteref) bug fixes

This fix several bugs in for-each-ref for %(push) and %(push:remoteref), as explained in the commit messages.

Note that there are still several bugs:
- the memory leak mentioned by Jeff in
  https://public-inbox.org/git/20200328131553.GA643242@coredump.intra.peff.net/
- in my patch, to detect if the workflow is triangular, I use:
static int is_workflow_triangular(struct branch *branch)
{
	struct remote *fetch_remote = remote_get(remote_for_branch(branch, NULL));
	struct remote *push_remote = remote_get(pushremote_for_branch(branch, NULL));
	return (fetch_remote && push_remote && fetch_remote != push_remote);
}

But remote_get will always fallback to 'origin'. So this means that if we set up a pushRemote="foobar" and no 'remote', the workflow is detected as triangular.

Whereas in `git push`, this workflow will not be detected as triangular.

=> So I can check that by looking at *explicit, but I actually have a question about what constitutes a triangular workflow, hence the RFC.

Furthermore, the upstream (and simple in non triangular workflow) case of %(push) and (push:remoteref) are essentially via `branch_get_upstream`, which is also used for %(upstream):

	branch && branch->merge && branch->merge[0] &&
		    	    branch->merge[0]->dst)
but `git push` does different checks:
	if (!branch->merge_nr || !branch->merge || !branch->remote_name)
		die(_("The current branch %s has no upstream branch.\n"...
	if (branch->merge_nr != 1)
		die(_("The current branch %s has multiple upstream branches, "
		    "refusing to push."), branch->name);

in particular git push fails if merge_nr !=1 or if branch has no remote, whereas %(push) will still indicates a push branch (assuming I fix is_workflow_triangular).

So I'll need to add a `branch_get_push` with these checks instead.

So I first send this patch as an RFC, and I'll see how to proceed afterwards to handle these remaining corner cases. Luckily, having a pushRemote but no remote, or several merge in the branch config are probably not too common.

=> So one question I have first is about the case when we do have a branch.pushRemote but not a branch.remote. Should this still be considered a triangular workflow?

According to git-push, no:
	static int is_workflow_triangular(struct remote *remote)
	{
		struct remote *fetch_remote = remote_get(NULL);
		return (fetch_remote && fetch_remote != remote);
	}
but I would argue that we should.

This would change nothing for push.default=upstream, since currently we check that `branch` has a remote_name in `setup_push_upstream` so it fails anyway even if the workflow is not explicitly triangular, but this would make push.default=simple behave as current, exactly as when branch.remote is different from branch.pushRemote (and I would argue that no branch.remote is a particular case of this situation).

PS: the first patch has no tests because I add them in the second patch, it
is more convenient to add them at once and test both patches.
PPS: v4 and v5 are intermediate versions I made but did not send to the ML.
Damien Robert (2):
  remote.c: fix %(push) for triangular workflows
  remote.c: fix handling of %(push:remoteref)
 remote.c                | 139 ++++++++++++++++++++++++++++++----------
 t/t6300-for-each-ref.sh |  81 +++++++++++++++++++++--
 2 files changed, 180 insertions(+), 40 deletions(-)
-- 
Patched on top of v2.26.0-106-g9fadedd637 (git version 2.26.0)
Damien Robert· Apr 6, 2020, 17:56 UTC · re: Damien Robert · lore

The behaviour of `git push` when push.default is simple or upstream changes in a triangular workflow, but this was not taken into account by %(push). Update the code to detect triangular workflows and fix this.

Signed-off-by: Damien Robert <damien.olivier.robert+git@gmail.com>
---
 remote.c | 42 +++++++++++++++++++++++++++++++-----------
 1 file changed, 31 insertions(+), 11 deletions(-)
Show changes to remote.c +31 −11
diff --git a/remote.c b/remote.c
index c43196ec06..3750a2bcc1 100644
--- a/remote.c
+++ b/remote.c
@@ -1656,6 +1656,18 @@ static const char *tracking_for_push_dest(struct remote *remote,
 	return ret;
 }
 
+static int is_workflow_triangular(struct branch *branch)
+{
+	struct remote *fetch_remote = remote_get(remote_for_branch(branch, NULL));
+	struct remote *push_remote = remote_get(pushremote_for_branch(branch, NULL));
+	return (fetch_remote && push_remote && fetch_remote != push_remote);
+}
+
+/**
+ * Return the tracking branch, as in %(push), that corresponds to the ref we
+ * would push to given a bare `git push` while `branch` is checked out.
+ * See also branch_get_push_remoteref above.
+ */
 static const char *branch_get_push_1(struct branch *branch, struct strbuf *err)
 {
 	struct remote *remote;
@@ -1693,23 +1705,31 @@ static const char *branch_get_push_1(struct branch *branch, struct strbuf *err)
 		return tracking_for_push_dest(remote, branch->refname, err);
 
 	case PUSH_DEFAULT_UPSTREAM:
-		return branch_get_upstream(branch, err);
+		if (is_workflow_triangular(branch))
+			return error_buf(err, _("push has no destination (push.default is 'upstream' and we are in a triangular workflow)"));
+		else
+			return branch_get_upstream(branch, err);
 
 	case PUSH_DEFAULT_UNSPECIFIED:
 	case PUSH_DEFAULT_SIMPLE:
 		{
 			const char *up, *cur;
 
-			up = branch_get_upstream(branch, err);
-			if (!up)
-				return NULL;
-			cur = tracking_for_push_dest(remote, branch->refname, err);
-			if (!cur)
-				return NULL;
-			if (strcmp(cur, up))
-				return error_buf(err,
-						 _("cannot resolve 'simple' push to a single destination"));
-			return cur;
+			if (is_workflow_triangular(branch)) {
+				return tracking_for_push_dest(remote, branch->refname, err);
+			}
+			else {
+				up = branch_get_upstream(branch, err);
+				if (!up)
+					return NULL;
+				cur = tracking_for_push_dest(remote, branch->refname, err);
+				if (!cur)
+					return NULL;
+				if (strcmp(cur, up))
+					return error_buf(err,
+							 _("cannot resolve 'simple' push to a single destination"));
+				return cur;
+			}
 		}
 	}
 
-- 
Patched on top of v2.26.0-106-g9fadedd637 (git version 2.26.0)
Damien Robert· Apr 6, 2020, 17:56 UTC · re: Damien Robert · lore

[PATCH v6 2/2] remote.c: fix handling of %(push:remoteref)

Looking at the value of %(push:remoteref) only handles the case when an explicit push refspec is passed. But it does not handle the fallback cases of looking at the configuration value of `push.default`.

In particular, doing something like
    git config push.default current
    git for-each-ref --format='%(push)'
    git for-each-ref --format='%(push:remoteref)'

prints a useful tracking ref for the first for-each-ref, but an empty string for the second.

Since the intention of %(push:remoteref), from 9700fae5ee (for-each-ref: let upstream/push report the remote ref name) is to get exactly which branch `git push` will push to, even in the fallback cases, fix this.

To get the meaning of %(push:remoteref), `ref-filter.c` calls `remote_ref_for_branch`. We simply add a new static helper function, `branch_get_push_remoteref` that follows the logic of `branch_get_push_1`, and call it from `remote_ref_for_branch`.

We also update t/6300-for-each-ref.sh to handle all `push.default` strategies. This involves testing `push.default=simple` twice, once where there is a matching upstream branch and once when there is none.

Finally we also test for triangular workflows.
Signed-off-by: Damien Robert <damien.olivier.robert+git@gmail.com>
---
 remote.c                | 97 +++++++++++++++++++++++++++++++----------
 t/t6300-for-each-ref.sh | 81 +++++++++++++++++++++++++++++++---
 2 files changed, 149 insertions(+), 29 deletions(-)
Show changes to 2 files +149 −29

remote.c, t/t6300-for-each-ref.sh

diff --git a/remote.c b/remote.c
index 3750a2bcc1..2b7f8a3af5 100644
--- a/remote.c
+++ b/remote.c
@@ -516,28 +516,6 @@ const char *pushremote_for_branch(struct branch *branch, int *explicit)
 	return remote_for_branch(branch, explicit);
 }
 
-const char *remote_ref_for_branch(struct branch *branch, int for_push)
-{
-	if (branch) {
-		if (!for_push) {
-			if (branch->merge_nr) {
-				return branch->merge_name[0];
-			}
-		} else {
-			const char *dst, *remote_name =
-				pushremote_for_branch(branch, NULL);
-			struct remote *remote = remote_get(remote_name);
-
-			if (remote && remote->push.nr &&
-			    (dst = apply_refspecs(&remote->push,
-						  branch->refname))) {
-				return dst;
-			}
-		}
-	}
-	return NULL;
-}
-
 static struct remote *remote_get_1(const char *name,
 				   const char *(*get_default)(struct branch *, int *))
 {
@@ -1663,6 +1641,67 @@ static int is_workflow_triangular(struct branch *branch)
 	return (fetch_remote && push_remote && fetch_remote != push_remote);
 }
 
+/**
+ * Return the local name of the remote tracking branch, as in
+ * %(push:remoteref), that corresponds to the ref we would push to given a
+ * bare `git push` while `branch` is checked out.
+ * See also branch_get_push_1 below.
+ */
+static const char *branch_get_push_remoteref(struct branch *branch)
+{
+	struct remote *remote;
+
+	remote = remote_get(pushremote_for_branch(branch, NULL));
+	if (!remote)
+		return NULL;
+
+	if (remote->push.nr) {
+		return apply_refspecs(&remote->push, branch->refname);
+	}
+
+	if (remote->mirror)
+		return branch->refname;
+
+	switch (push_default) {
+	case PUSH_DEFAULT_NOTHING:
+		return NULL;
+
+	case PUSH_DEFAULT_MATCHING:
+	case PUSH_DEFAULT_CURRENT:
+		return branch->refname;
+
+	case PUSH_DEFAULT_UPSTREAM:
+		if (is_workflow_triangular(branch))
+		    return NULL;
+		else {
+			if (branch && branch->merge && branch->merge[0] &&
+		    	    branch->merge[0]->dst)
+				return branch->merge[0]->src;
+			else
+				return NULL;
+		}
+
+	case PUSH_DEFAULT_UNSPECIFIED:
+	case PUSH_DEFAULT_SIMPLE:
+		{
+			const char *up, *cur;
+
+			if (is_workflow_triangular(branch))
+				return branch->refname;
+			else {
+				up = branch_get_upstream(branch, NULL);
+				cur = tracking_for_push_dest(remote, branch->refname, NULL);
+				if (up && cur && !strcmp(cur, up))
+					return branch->refname;
+				else
+					return NULL;
+			}
+
+		}
+	}
+	BUG("unhandled push situation");
+}
+
 /**
  * Return the tracking branch, as in %(push), that corresponds to the ref we
  * would push to given a bare `git push` while `branch` is checked out.
@@ -1755,6 +1794,20 @@ static int ignore_symref_update(const char *refname)
 	return (flag & REF_ISSYMREF);
 }
 
+const char *remote_ref_for_branch(struct branch *branch, int for_push)
+{
+	if (branch) {
+		if (!for_push) {
+			if (branch->merge_nr) {
+				return branch->merge_name[0];
+			}
+		} else {
+			return branch_get_push_remoteref(branch);
+		}
+	}
+	return NULL;
+}
+
 /*
  * Create and return a list of (struct ref) consisting of copies of
  * each remote_ref that matches refspec.  refspec must be a pattern.
diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh
index b3c1092338..8e59ab2567 100755
--- a/t/t6300-for-each-ref.sh
+++ b/t/t6300-for-each-ref.sh
@@ -875,13 +875,80 @@ test_expect_success ':remotename and :remoteref' '
 			git for-each-ref --format="${pair%=*}" \
 				refs/heads/master >actual &&
 			test_cmp expect actual
-		done &&
-		git branch push-simple &&
-		git config branch.push-simple.pushRemote from &&
-		actual="$(git for-each-ref \
-			--format="%(push:remotename),%(push:remoteref)" \
-			refs/heads/push-simple)" &&
-		test from, = "$actual"
+		done
+	)
+'
+
+test_expect_success '%(push) and %(push:remoteref)' '
+	git init pushremote-tests &&
+	(
+		cd pushremote-tests &&
+		test_commit initial &&
+		git remote add from fifth.coffee:blub &&
+		git config branch.master.remote from &&
+		actual="$(git -c push.default=simple for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,, = "$actual" &&
+		git config branch.master.merge refs/heads/master &&
+		actual="$(git -c push.default=simple for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,refs/heads/master,refs/remotes/from/master = "$actual" &&
+		git config branch.master.merge refs/heads/other &&
+		actual="$(git -c push.default=simple for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,, = "$actual" &&
+		actual="$(git -c push.default=upstream for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,refs/heads/other,refs/remotes/from/other = "$actual" &&
+		actual="$(git -c push.default=current for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,refs/heads/master,refs/remotes/from/master = "$actual" &&
+		actual="$(git -c push.default=matching for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,refs/heads/master,refs/remotes/from/master = "$actual" &&
+		actual="$(git -c push.default=nothing for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test from,, = "$actual" &&
+		git remote add to southridge.audio:repo &&
+		git config branch.master.pushRemote to &&
+		git config --unset branch.master.merge &&
+		actual="$(git -c push.default=simple for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,refs/heads/master,refs/remotes/to/master = "$actual" &&
+		git config branch.master.merge refs/heads/master &&
+		actual="$(git -c push.default=simple for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,refs/heads/master,refs/remotes/to/master = "$actual" &&
+		git config branch.master.merge refs/heads/other &&
+		actual="$(git -c push.default=simple for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,refs/heads/master,refs/remotes/to/master = "$actual" &&
+		actual="$(git -c push.default=upstream for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,, = "$actual" &&
+		actual="$(git -c push.default=current for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,refs/heads/master,refs/remotes/to/master = "$actual" &&
+		actual="$(git -c push.default=matching for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,refs/heads/master,refs/remotes/to/master = "$actual" &&
+		actual="$(git -c push.default=nothing for-each-ref \
+			--format="%(push:remotename),%(push:remoteref),%(push)" \
+			refs/heads/master)" &&
+		test to,, = "$actual"
 	)
 '
 
-- 
Patched on top of v2.26.0-106-g9fadedd637 (git version 2.26.0)

← back to recent threads