summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-08-03 09:31:29 -0700
committerJunio C Hamano <gitster@pobox.com>2026-08-03 09:31:29 -0700
commit24e4e2ef8b25279d117b0bc29d5389cc9df5dbea (patch)
tree1df83fd638f05a24ff1b09f6c46ba943512fe0d7
parenta97fcc37c2bc6340a8d7ce78dedf227aac4e9aa7 (diff)
parent93775e35b7ed7983a8aaa33a566f45a8a1152718 (diff)
Merge branch 'hn/url-push-tracking'
When the push remote is specified as a URL, the fetch refspec of a uniquely matching configured remote is now used to find and update the remote-tracking branch (e.g., '@{push}'). * hn/url-push-tracking: remote: find tracking branches for URL push destinations remote: pass repository to push tracking helper
-rw-r--r--Documentation/config/branch.adoc1
-rw-r--r--Documentation/revisions.adoc3
-rw-r--r--remote.c54
-rw-r--r--remote.h2
-rwxr-xr-xt/t5505-remote.sh144
-rw-r--r--transport.c5
6 files changed, 203 insertions, 6 deletions
diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc
index a4db9fa5c8..5a85fde8de 100644
--- a/Documentation/config/branch.adoc
+++ b/Documentation/config/branch.adoc
@@ -55,6 +55,7 @@ This option defaults to `never`.
repository), you would want to set `remote.pushDefault` to
specify the remote to push to for all branches, and use this
option to override it for a specific branch.
+ The value may be the name of a configured remote or a repository URL.
`branch.<name>.merge`::
Defines, together with `branch.<name>.remote`, the upstream branch
diff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc
index 6ea6c7cead..3fbfbd3d5f 100644
--- a/Documentation/revisions.adoc
+++ b/Documentation/revisions.adoc
@@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8.
`git push` were run while `branchname` was checked out (or the current
`HEAD` if no branchname is specified). Like for '@\{upstream\}', we report
the remote-tracking branch that corresponds to that branch at the remote.
+ If the push destination is a URL and exactly one configured remote uses
+ that URL for pushing, '@\{push}' reports that remote's remote-tracking
+ branch.
+
Here's an example to make it more clear:
+
diff --git a/remote.c b/remote.c
index b55fc6e18a..a13d9a0903 100644
--- a/remote.c
+++ b/remote.c
@@ -954,6 +954,17 @@ struct strvec *push_url_of_remote(struct remote *remote)
return remote->pushurl.nr ? &remote->pushurl : &remote->url;
}
+static bool remote_has_push_url(struct remote *remote, const char *url)
+{
+ const struct strvec *push_urls = push_url_of_remote(remote);
+
+ for (size_t i = 0; i < push_urls->nr; i++) {
+ if (!strcmp(push_urls->v[i], url))
+ return true;
+ }
+ return false;
+}
+
void ref_push_report_free(struct ref_push_report *report)
{
while (report) {
@@ -1887,12 +1898,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)
return branch->merge[0]->dst;
}
-static char *tracking_for_push_dest(struct remote *remote,
+struct remote *repo_remote_for_push_tracking(struct repository *repo,
+ struct remote *remote)
+{
+ const struct strvec *push_urls;
+ struct remote *first_match = NULL;
+ struct remote_state *remote_state = repo->remote_state;
+ const char *check_url;
+
+ if (remote->origin != REMOTE_UNCONFIGURED)
+ return remote;
+
+ push_urls = push_url_of_remote(remote);
+ if (push_urls->nr != 1)
+ return remote;
+ check_url = push_urls->v[0];
+
+ for (int i = 0; i < remote_state->remotes_nr; i++) {
+ struct remote *candidate = remote_state->remotes[i];
+
+ if (!candidate || candidate == remote ||
+ !remote_is_configured(candidate, 0) ||
+ !remote_has_push_url(candidate, check_url))
+ continue;
+ if (first_match)
+ return remote;
+ first_match = candidate;
+ }
+
+ return first_match ? first_match : remote;
+}
+
+static char *tracking_for_push_dest(struct repository *repo,
+ struct remote *remote,
const char *refname,
struct strbuf *err)
{
char *ret;
+ remote = repo_remote_for_push_tracking(repo, remote);
ret = apply_refspecs(&remote->fetch, refname);
if (!ret)
return error_buf(err,
@@ -1925,13 +1969,13 @@ static char *branch_get_push_1(struct repository *repo,
_("push refspecs for '%s' do not include '%s'"),
remote->name, branch->name);
- ret = tracking_for_push_dest(remote, dst, err);
+ ret = tracking_for_push_dest(repo, remote, dst, err);
free(dst);
return ret;
}
if (remote->mirror)
- return tracking_for_push_dest(remote, branch->refname, err);
+ return tracking_for_push_dest(repo, remote, branch->refname, err);
switch (repo_config_values(repo)->push_default) {
case PUSH_DEFAULT_NOTHING:
@@ -1939,7 +1983,7 @@ static char *branch_get_push_1(struct repository *repo,
case PUSH_DEFAULT_MATCHING:
case PUSH_DEFAULT_CURRENT:
- return tracking_for_push_dest(remote, branch->refname, err);
+ return tracking_for_push_dest(repo, remote, branch->refname, err);
case PUSH_DEFAULT_UPSTREAM:
return xstrdup_or_null(branch_get_upstream(branch, err));
@@ -1953,7 +1997,7 @@ static char *branch_get_push_1(struct repository *repo,
up = branch_get_upstream(branch, err);
if (!up)
return NULL;
- cur = tracking_for_push_dest(remote, branch->refname, err);
+ cur = tracking_for_push_dest(repo, remote, branch->refname, err);
if (!cur)
return NULL;
if (strcmp(cur, up)) {
diff --git a/remote.h b/remote.h
index 72a54d84ad..cca02033b9 100644
--- a/remote.h
+++ b/remote.h
@@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push);
const char *repo_default_remote(struct repository *repo);
const char *repo_remote_from_url(struct repository *repo, const char *url);
+struct remote *repo_remote_for_push_tracking(struct repository *repo,
+ struct remote *remote);
/* returns true if the given branch has merge configuration given. */
int branch_has_merge_config(struct branch *branch);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 81cc251d3b..5fbfcb0848 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -24,6 +24,28 @@ setup_repository () {
)
}
+setup_url_pushremote () {
+ rm -rf fork.git client &&
+ git clone --bare one fork.git &&
+ git clone one client &&
+ fork_url="file://$TRASH_DIRECTORY/fork.git" &&
+ (
+ cd client &&
+ git checkout -b topic --track origin/main &&
+ git commit --allow-empty -m topic-change &&
+ git config push.default current &&
+ git config status.compareBranches "@{upstream} @{push}" &&
+ git config branch.topic.pushRemote "$fork_url" &&
+ git push
+ )
+}
+
+check_status () {
+ git -C client status >actual &&
+ cat >expected &&
+ test_cmp expected actual
+}
+
tokens_match () {
echo "$1" | tr ' ' '\012' | sort | sed -e '/^$/d' >expect &&
echo "$2" | tr ' ' '\012' | sort | sed -e '/^$/d' >actual &&
@@ -1018,6 +1040,128 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g
)
'
+test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '
+ setup_url_pushremote &&
+
+ check_status <<-EOF
+ On branch topic
+ Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+'
+
+test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' '
+ setup_url_pushremote &&
+
+ (
+ cd client &&
+ git remote rename origin upstream &&
+ git remote add -f origin "$fork_url"
+ ) &&
+
+ check_status <<-EOF
+ On branch topic
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+'
+
+test_expect_success 'configured pushurl makes URL-valued pushRemote trackable' '
+ setup_url_pushremote &&
+
+ (
+ cd client &&
+ git remote rename origin upstream &&
+ git remote add -f origin ../fork.git &&
+ git remote set-url --push origin "$fork_url"
+ ) &&
+
+ check_status <<-EOF
+ On branch topic
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+'
+
+test_expect_success 'pushInsteadOf URL pushRemote is trackable' '
+ setup_url_pushremote &&
+ (
+ cd client &&
+ git remote rename origin upstream &&
+ git remote add -f origin "$fork_url" &&
+ git config "url.$fork_url.pushInsteadOf" fork: &&
+ git config branch.topic.pushRemote fork:
+ ) &&
+
+ check_status <<-EOF
+ On branch topic
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+'
+
+test_expect_success 'up-to-date URL push refreshes stale tracking branch' '
+ setup_url_pushremote &&
+ (
+ cd client &&
+ git remote rename origin upstream &&
+ git remote add -f origin "$fork_url" &&
+ git commit --allow-empty -m another-topic-change &&
+ git -C ../fork.git fetch ../client topic:topic
+ ) &&
+
+ check_status <<-EOF &&
+ On branch topic
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.
+
+ Your branch is ahead of ${SQ}origin/topic${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+
+ git -C client push >actual 2>&1 &&
+ test_grep "Everything up-to-date" actual &&
+
+ check_status <<-EOF
+ On branch topic
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.
+
+ Your branch is up to date with ${SQ}origin/topic${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+'
+
+test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '
+ setup_url_pushremote &&
+ (
+ cd client &&
+ git remote rename origin upstream &&
+ git remote add -f origin "$fork_url" &&
+ git remote add duplicate "$fork_url"
+ ) &&
+
+ check_status <<-EOF
+ On branch topic
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+'
+
test_expect_success 'rename handles remote without fetch refspec' '
git clone --bare one no-refspec.git &&
# confirm assumption that bare clone does not create refspec
diff --git a/transport.c b/transport.c
index f0a6a45547..42b0a2d0bd 100644
--- a/transport.c
+++ b/transport.c
@@ -1597,8 +1597,11 @@ int transport_push(struct repository *r,
if (!(flags & (TRANSPORT_PUSH_DRY_RUN |
TRANSPORT_RECURSE_SUBMODULES_ONLY))) {
struct ref *ref;
+ struct remote *tracking_remote = repo_remote_for_push_tracking(
+ r, transport->remote);
+
for (ref = remote_refs; ref; ref = ref->next)
- transport_update_tracking_ref(transport->remote, ref, verbose);
+ transport_update_tracking_ref(tracking_remote, ref, verbose);
}
if (porcelain && !push_ret)