Subject: [PATCH v13 0/2] checkout: --track=fetch

From: "Harald Nordgren via GitGitGadget" <[email-masked]>
Date: Sat, 23 May 2026 19:48:32 +0000

* Create a preparatory commit that exposes find_tracking_remote_for_ref()
   and advise_ambiguous_fetch_refspec() from branch.c, so checkout can reuse
   the same lookup git branch --track uses.
 * Use advise_ambiguous_fetch_refspec() for the "multiple remotes match"
   case, so the wording matches git branch --track.

Harald Nordgren (2):
  branch: expose helpers for finding the remote owning a tracking ref
  checkout: extend --track with a "fetch" mode to refresh start-point

 Documentation/git-checkout.adoc |  17 +-
 Documentation/git-switch.adoc   |   5 +-
 branch.c                        |  96 ++++++-----
 branch.h                        |  16 ++
 builtin/checkout.c              | 139 +++++++++++++++-
 t/t7201-co.sh                   | 276 ++++++++++++++++++++++++++++++++
 6 files changed, 498 insertions(+), 51 deletions(-)

base-commit: aec3f587505a472db67e9462d0702e7d463a449d
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2281%2FHaraldNordgren%2Fcheckout-fetch-start-point-v13
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2281/HaraldNordgren/checkout-fetch-start-point-v13
Pull-Request: https://github.com/git/git/pull/2281

Range-diff vs v12:

 -:  ---------- > 1:  2369afad24 branch: expose helpers for finding the remote owning a tracking ref
 1:  bcd034dbed ! 2:  60adf0e67d checkout: extend --track with a "fetch" mode to refresh start-point
     @@ Commit message
              git checkout -b new_branch --track origin/some-branch
      
          Identify the remote whose configured fetch refspec maps to
     -    <start-point>, then run "git fetch <remote> <src-ref>" for just that
     -    ref so other remote-tracking branches are left untouched. When
     -    <start-point> is a bare <remote> (e.g. "origin"), follow
     +    <start-point> using find_tracking_remote_for_ref() (the same lookup
     +    "--track" uses to pick which remote to record in
     +    branch.<name>.remote), then run "git fetch <remote> <src-ref>" for
     +    just that ref so other remote-tracking branches are left untouched.
     +    When <start-point> is a bare <remote> (e.g. "origin"), follow
          refs/remotes/<remote>/HEAD to learn which branch to refresh. If
          "git fetch" fails but the remote-tracking ref already exists locally,
          warn and proceed from the existing tip; otherwise abort.
     @@ builtin/checkout.c: struct branch_info {
       	char *checkout;
       };
       
     -+struct fetch_target_cb {
     -+	char *dst;
     -+	struct string_list matches;
     -+};
     -+
     -+static int match_fetch_target(struct remote *remote, void *priv)
     -+{
     -+	struct fetch_target_cb *cb = priv;
     -+	struct refspec_item q = { .dst = cb->dst };
     -+
     -+	if (!remote_find_tracking(remote, &q) && q.src)
     -+		string_list_append(&cb->matches, remote->name)->util = q.src;
     -+	return 0;
     -+}
     -+
      +static void fetch_remote_for_start_point(const char *arg, int quiet)
      +{
      +	struct strbuf dst = STRBUF_INIT;
     -+	struct fetch_target_cb cb = { .matches = STRING_LIST_INIT_NODUP };
     ++	struct tracking tracking;
     ++	struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
     ++	struct string_list ambiguous_remotes = STRING_LIST_INIT_DUP;
      +	struct child_process cmd = CHILD_PROCESS_INIT;
      +	struct object_id oid;
      +	struct remote *named_remote;
      +	int bare_ns;
     -+	size_t i;
      +
      +	strbuf_addf(&dst, "refs/remotes/%s", arg);
      +	if (check_refname_format(dst.buf, 0))
     @@ builtin/checkout.c: struct branch_info {
      +		free(head_path);
      +	}
      +
     -+	cb.dst = dst.buf;
     -+	for_each_remote(match_fetch_target, &cb);
     -+
     -+	if (cb.matches.nr > 1) {
     -+		struct strbuf msg = STRBUF_INIT;
     -+
     -+		strbuf_addf(&msg,
     -+			    _("cannot fetch start-point '%s': fetch refspecs "
     -+			      "of multiple remotes map to the same destination:"),
     -+			    arg);
     -+		for (i = 0; i < cb.matches.nr; i++)
     -+			strbuf_addf(&msg, "\n  %s", cb.matches.items[i].string);
     -+		strbuf_addstr(&msg,
     -+			      _("\nadjust 'remote.<name>.fetch' so only one "
     -+				"remote maps there, or omit '=fetch'"));
     -+		die("%s", msg.buf);
     ++	memset(&tracking, 0, sizeof(tracking));
     ++	tracking.spec.dst = dst.buf;
     ++	tracking.srcs = &tracking_srcs;
     ++	find_tracking_remote_for_ref(&tracking, &ambiguous_remotes);
     ++
     ++	if (tracking.matches > 1) {
     ++		int status = die_message(_("cannot fetch start-point '%s': "
     ++					   "fetch refspecs of multiple remotes "
     ++					   "map to '%s'"), arg, dst.buf);
     ++		advise_ambiguous_fetch_refspec(dst.buf, &ambiguous_remotes);
     ++		exit(status);
      +	}
      +
     -+	if (!cb.matches.nr) {
     ++	if (!tracking.matches) {
      +		if (bare_ns && named_remote &&
      +		    remote_is_configured(named_remote, 1))
      +			die(_("cannot fetch start-point '%s': "
     @@ builtin/checkout.c: struct branch_info {
      +	strvec_push(&cmd.args, "fetch");
      +	if (quiet)
      +		strvec_push(&cmd.args, "--quiet");
     -+	strvec_pushl(&cmd.args, cb.matches.items[0].string,
     -+		     (char *)cb.matches.items[0].util, NULL);
     ++	strvec_pushl(&cmd.args, tracking.remote,
     ++		     tracking_srcs.items[0].string, NULL);
      +	cmd.git_cmd = 1;
      +	if (run_command(&cmd)) {
      +		if (!refs_read_ref(get_main_ref_store(the_repository),
     @@ builtin/checkout.c: struct branch_info {
      +			die(_("failed to fetch start-point '%s'"), arg);
      +	}
      +
     -+	for (i = 0; i < cb.matches.nr; i++)
     -+		free(cb.matches.items[i].util);
     -+	string_list_clear(&cb.matches, 0);
     ++	string_list_clear(&tracking_srcs, 0);
     ++	string_list_clear(&ambiguous_remotes, 0);
      +	strbuf_release(&dst);
      +}
      +
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_must_fail git checkout --track=fetch -b local_ambig ambig_ns/fetch_ambig 2>err &&
      +	test_grep "fetch_ambig_a" err &&
      +	test_grep "fetch_ambig_b" err &&
     -+	test_grep "remote.<name>.fetch" err &&
     ++	test_grep "tracking namespaces" err &&
      +	test_must_fail git rev-parse --verify refs/heads/local_ambig
      +'
      +

-- 
gitgitgadget