Merge branch 'hn/url-push-tracking' into jch

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
jch
Junio C Hamano 2026-07-24 14:40:40 -07:00
commit 740ffec908
6 changed files with 203 additions and 6 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,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)) {

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

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