remote: find tracking branches for URL push destinations

Git accepts a repository URL as branch.<name>.pushRemote and can push
to it. This branch setting takes precedence over remote.pushDefault.

A branch can be configured with a URL-valued pushRemote before any push
occurs. If the remotes are later rearranged with "git remote rename" and
"git remote add", the newly added remote may use that URL. The URL value
is unaffected by the rename and continues to take precedence over
remote.pushDefault. The URL and the remote then point to the same
repository, but Git does not connect them for tracking. Pushing works,
but @{push} cannot identify the remote's tracking branch. As a result,
"git status" cannot show the push branch, and an up-to-date push can
leave its tracking information stale.

When exactly one configured remote would push to the same URL, use that
remote for push tracking. Continue to push to the URL so the configured
remote's push settings do not change existing behavior. Keep the current
behavior when no remote matches or multiple remotes match.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Harald Nordgren
2026-07-22 18:08:58 +00:00
committed by Junio C Hamano
parent 8391f01768
commit 93775e35b7
6 changed files with 198 additions and 2 deletions

View File

@@ -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

View File

@@ -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:
+

View File

@@ -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,13 +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 repository *repo UNUSED,
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,

View File

@@ -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);

View File

@@ -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

View File

@@ -1553,8 +1553,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)