diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 6cee8bb8e89134..a57ca5426fb4ca 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -445,3 +445,24 @@ Fixes since v2.55 'gitweb' has been corrected to work even when the index line carries a trailing file mode. (merge fda513d6fe tl/gitweb-shorten-hashes-with-modes later to maint). + + * 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}'). + + * Traversals with '--exclude-first-parent-only' have been corrected + to properly stop after the first parent even when it has already + been marked as 'SEEN'. + (merge 47382f7398 jc/exclude-first-parent-seen later to maint). + + * A segfault when 'git clone --revision' talks to a server that does not + support protocol v2 (falling back to protocol v0) has been corrected. + (merge 1034ad383f af/clone-revision-v0-segfault-fix later to maint). + + * rewrites_release() in 'remote.c' has been updated to free 'struct + rewrite' instances, their '.instead_of' arrays, and their contents. + (merge dcef3bf041 jc/remote-insteadof-leakfix later to maint). + + * The remote-matching logic for submodules has been corrected to resolve + 'url.*.insteadOf' aliases before comparing the inventoried URL from + '.gitmodules' with the URLs of configured remotes. diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc index a4db9fa5c87eab..5a85fde8de9286 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 6ea6c7cead1647..3fbfbd3d5fc091 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/builtin/clone.c b/builtin/clone.c index c649a9f49e6e22..5b25cca5102956 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -557,7 +557,7 @@ static void update_remote_refs(const struct ref *refs, write_followtags(refs, msg); } - if (remote_head_points_at && !option_bare) { + if (remote_head_points_at && remote_head_points_at->peer_ref && !option_bare) { struct strbuf head_ref = STRBUF_INIT; strbuf_addstr(&head_ref, branch_top); strbuf_addstr(&head_ref, "HEAD"); diff --git a/remote.c b/remote.c index b55fc6e18a487f..fe620684635620 100644 --- a/remote.c +++ b/remote.c @@ -309,8 +309,15 @@ static struct rewrite *make_rewrite(struct rewrites *r, static void rewrites_release(struct rewrites *r) { - for (int i = 0; i < r->rewrite_nr; i++) - free((char *)r->rewrite[i]->base); + for (int i = 0; i < r->rewrite_nr; i++) { + struct rewrite *rewrite = r->rewrite[i]; + + free((char *)rewrite->base); + for (int j = 0; j < rewrite->instead_of_nr; j++) + free((char *)rewrite->instead_of[j].s); + free(rewrite->instead_of); + free(rewrite); + } free(r->rewrite); memset(r, 0, sizeof(*r)); } @@ -954,6 +961,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) { @@ -1821,17 +1839,25 @@ const char *repo_default_remote(struct repository *repo) const char *repo_remote_from_url(struct repository *repo, const char *url) { + char *rewritten_url; + const char *remote_name = NULL; + read_config(repo, 0); + if ((rewritten_url = alias_url(url, &repo->remote_state->rewrites))) + url = rewritten_url; for (int i = 0; i < repo->remote_state->remotes_nr; i++) { struct remote *remote = repo->remote_state->remotes[i]; if (!remote) continue; - if (remote_has_url(remote, url)) - return remote->name; + if (remote_has_url(remote, url)) { + remote_name = remote->name; + break; + } } - return NULL; + free(rewritten_url); + return remote_name; } int branch_has_merge_config(struct branch *branch) @@ -1887,12 +1913,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 +1984,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 +1998,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 +2012,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 72a54d84ad511d..cca02033b9d73b 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/revision.c b/revision.c index 8eb7cb9bbccd1e..526bcf3fb5aeb9 100644 --- a/revision.c +++ b/revision.c @@ -1153,12 +1153,18 @@ static int process_parents(struct rev_info *revs, struct commit *commit, if (p) p->object.flags |= UNINTERESTING | CHILD_VISITED; - if (repo_parse_commit_gently(revs->repo, p, 1) < 0) + if (repo_parse_commit_gently(revs->repo, p, 1) < 0) { + if (revs->exclude_first_parent_only) + break; continue; + } if (p->parents) mark_parents_uninteresting(revs, p); - if (p->object.flags & SEEN) + if (p->object.flags & SEEN) { + if (revs->exclude_first_parent_only) + break; continue; + } p->object.flags |= (SEEN | NOT_USER_GIVEN); if (queue) prio_queue_put(queue, p); diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 81cc251d3bc3a1..5fbfcb084830f2 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/t/t5621-clone-revision.sh b/t/t5621-clone-revision.sh index db3b8cff558f0b..54789423f8cccc 100755 --- a/t/t5621-clone-revision.sh +++ b/t/t5621-clone-revision.sh @@ -90,6 +90,14 @@ test_expect_success 'clone with --revision and --bare' ' test_must_fail git -C dst config remote.origin.fetch ' +test_expect_success 'clone with --revision and protocol v0' ' + test_when_finished "rm -rf dst" && + git -c protocol.version=0 clone --no-local --revision=refs/heads/main . dst && + git rev-parse refs/heads/main >expect && + git -C dst rev-parse HEAD >actual && + test_cmp expect actual +' + test_expect_success 'clone with --revision being a short raw commit hash' ' test_when_finished "rm -rf dst" && oid=$(git rev-parse --short refs/heads/feature) && diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh index 4cecb6224cb464..2284bbba12c726 100755 --- a/t/t6012-rev-list-simplify.sh +++ b/t/t6012-rev-list-simplify.sh @@ -285,4 +285,22 @@ test_expect_success 'log --graph --simplify-merges --show-pulls' ' test_cmp expect actual ' +test_expect_success 'exclude-first-parent-only with parent already seen' ' + git checkout --orphan test-seen && + git rm -rf . && + test_commit r1 && + git checkout -b branch-f && + test_commit f && + git checkout test-seen && + git merge --no-ff --no-edit -m r2 branch-f && + git tag r2 && + + git rev-list --exclude-first-parent-only f ^r2 >actual && + git rev-parse f >expect && + test_cmp expect actual && + + git rev-list --exclude-first-parent-only f r1 ^r2 >actual2 && + test_cmp expect actual2 +' + test_done diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index 9554720152f6f0..10adeabf0fcc85 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -256,6 +256,25 @@ test_expect_success 'submodule update --remote should fetch upstream changes' ' ) ' +test_expect_success 'submodule update --remote resolves URL rewrites' ' + test_config_global "url.$(pwd)/.insteadOf" local: && + mkdir alias-super alias-submodule && + ( + cd alias-submodule && + git init && + git commit --allow-empty --message "Initial commit" + ) && + ( + cd alias-super && + git init && + git submodule add local:alias-submodule submodule && + git submodule update --force && + git -C submodule remote rename origin upstream && + git -C submodule remote add fork user@host && + git submodule update --remote + ) +' + test_expect_success 'submodule update --remote should fetch upstream changes with .' ' ( cd super && diff --git a/transport.c b/transport.c index f0a6a455479800..42b0a2d0bd6616 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)