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..merge`:: Defines, together with `branch..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)