git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 16:49 UTC

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

From
Harald Nordgren via GitGitGadget <gitgitgadget@gmail.com>
Date
Jun 24, 2026, 21:54 UTC
Message-ID
<pull.2281.v15.git.git.1782338098.gitgitgadget@gmail.com>
In-Reply-To
<pull.2281.v14.git.git.1781786652.gitgitgadget@gmail.com>
Extend checkout --track with a fetch mode to refresh start-point.
Changes in v15:
 * Reword commit message to lead with motivation.
 * Drop RESOLVE_REF_NO_RECURSE so <ns>/HEAD lookup matches what git checkout
   does for the same ref. Drop redundant check_refname_format on a ref we
   just read. Replace memset with brace initializer. Use refs_ref_exists and
   pass NULL for the OID out-params.
 * Split the "set HEAD" error onto its own line via die_message and advise,
   matching the suggested format.
 * Remove 6 redundant tests, replace test $a = $b with test_cmp_rev, and
   rename test titles to avoid "namespace".
Changes in v14:
 * Handle .h files in a better way.
Changes in v13:
 * 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              | 138 +++++++++++++++++++-
 t/t7201-co.sh                   | 222 ++++++++++++++++++++++++++++++++
 6 files changed, 443 insertions(+), 51 deletions(-)
base-commit: ab776a62a78576513ee121424adb19597fbb7613
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2281%2FHaraldNordgren%2Fcheckout-fetch-start-point-v15
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2281/HaraldNordgren/checkout-fetch-start-point-v15
Pull-Request: https://github.com/git/git/pull/2281
Range-diff vs v14:
 1:  f79689c23d = 1:  8139490c36 branch: expose helpers for finding the remote owning a tracking ref
 2:  8518f090b1 ! 2:  b7e387f123 checkout: extend --track with a "fetch" mode to refresh start-point
     @@ Metadata
       ## Commit message ##
          checkout: extend --track with a "fetch" mode to refresh start-point
      
     -    Add a "fetch" mode to the "--track" option of "git checkout" / "git
     -    switch" that refreshes <start-point> before checking it out:
     +    Forking from an existing remote branch without refreshing first often
     +    has consequences: you start work that has already been done, or you
     +    build on an old version of the code which causes big conflicts later
     +    when you pull. The workaround is two commands ("git fetch <remote>
     +    <branch> && git checkout -b <topic> <remote>/<branch>"), and when
     +    the fetch is skipped the checkout silently starts from a stale tip.
      
     -        git checkout -b new_branch --track=fetch origin/some-branch
     +    Users may already expect "<remote>/<branch>" to refer to the latest
     +    tip on the remote. While this blurs the line between fetch and
     +    checkout, git already does this in places where it pays off: "git
     +    clone" fetches and checks out, and "git pull" fetches and merges.
      
     -    is shorthand for
     +    Add a "fetch" mode to "--track" that refreshes <start-point> before
     +    checking it out:
      
     -        git fetch origin some-branch
     -        git checkout -b new_branch --track origin/some-branch
     +        git checkout -b new_branch --track=fetch origin/some-branch
      
     -    Identify the remote whose configured fetch refspec maps to
     -    <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.
     +    Only the requested branch is fetched 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.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
     @@ builtin/checkout.c: struct branch_info {
      +static void fetch_remote_for_start_point(const char *arg, int quiet)
      +{
      +	struct strbuf dst = STRBUF_INIT;
     -+	struct tracking tracking;
     ++	struct tracking tracking = { 0 };
      +	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;
      +
     @@ builtin/checkout.c: struct branch_info {
      +		const char *head_target =
      +			refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
      +						head_path,
     -+						RESOLVE_REF_READING |
     -+						RESOLVE_REF_NO_RECURSE,
     -+						&oid, NULL);
     ++						RESOLVE_REF_READING,
     ++						NULL, NULL);
      +		if (head_target &&
      +		    starts_with(head_target, dst.buf) &&
     -+		    head_target[dst.len] == '/' &&
     -+		    !check_refname_format(head_target, 0)) {
     ++		    head_target[dst.len] == '/') {
      +			strbuf_reset(&dst);
      +			strbuf_addstr(&dst, head_target);
      +			bare_ns = 0;
     @@ builtin/checkout.c: struct branch_info {
      +		free(head_path);
      +	}
      +
     -+	memset(&tracking, 0, sizeof(tracking));
      +	tracking.spec.dst = dst.buf;
      +	tracking.srcs = &tracking_srcs;
      +	find_tracking_remote_for_ref(&tracking, &ambiguous_remotes);
     @@ builtin/checkout.c: struct branch_info {
      +
      +	if (!tracking.matches) {
      +		if (bare_ns && named_remote &&
     -+		    remote_is_configured(named_remote, 1))
     -+			die(_("cannot fetch start-point '%s': "
     -+			      "'refs/remotes/%s/HEAD' is not set; run "
     -+			      "'git remote set-head %s --auto' to set it"),
     -+			    arg, arg, arg);
     ++		    remote_is_configured(named_remote, 1)) {
     ++			int status = die_message(_("cannot fetch start-point '%s' "
     ++						   "because 'refs/remotes/%s/HEAD' "
     ++						   "does not exist."), arg, arg);
     ++			advise(_("To create it run\n"
     ++				 "\n"
     ++				 "    git remote set-head %s --auto\n"), arg);
     ++			exit(status);
     ++		}
      +		die(_("cannot fetch start-point '%s': no configured remote's "
      +		      "fetch refspec matches it"), arg);
      +	}
     @@ builtin/checkout.c: struct branch_info {
      +		     tracking_srcs.items[0].string, NULL);
      +	cmd.git_cmd = 1;
      +	if (run_command(&cmd)) {
     -+		if (!refs_read_ref(get_main_ref_store(the_repository),
     -+				   dst.buf, &oid))
     ++		if (refs_ref_exists(get_main_ref_store(the_repository), dst.buf))
      +			warning(_("failed to fetch start-point '%s'; "
      +				  "using existing '%s'"), arg, dst.buf);
      +		else
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	git -C fetch_upstream checkout -b fetch_other &&
      +	test_commit -C fetch_upstream u_other_pre &&
      +	git fetch fetch_upstream &&
     -+	other_before=$(git rev-parse refs/remotes/fetch_upstream/fetch_other) &&
     ++	git update-ref refs/heads/snapshot_other refs/remotes/fetch_upstream/fetch_other &&
      +	git -C fetch_upstream checkout fetch_target &&
      +	test_commit -C fetch_upstream u_target_post &&
      +	git -C fetch_upstream checkout fetch_other &&
      +	test_commit -C fetch_upstream u_other_post &&
      +	git checkout --track=fetch -b local_target fetch_upstream/fetch_target &&
      +	test_cmp_rev refs/remotes/fetch_upstream/fetch_target HEAD &&
     -+	test "$(git rev-parse refs/remotes/fetch_upstream/fetch_other)" = "$other_before"
     ++	test_cmp_rev refs/remotes/fetch_upstream/fetch_other snapshot_other
      +'
      +
      +test_expect_success 'checkout --track=fetch with bare remote name fetches only <remote>/HEAD target' '
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	git -C fetch_upstream checkout -b fetch_unrelated &&
      +	test_commit -C fetch_upstream u_unrelated_pre &&
      +	git fetch fetch_upstream fetch_unrelated &&
     -+	unrelated_before=$(git rev-parse refs/remotes/fetch_upstream/fetch_unrelated) &&
     ++	git update-ref refs/heads/snapshot_unrelated \
     ++		refs/remotes/fetch_upstream/fetch_unrelated &&
      +	git -C fetch_upstream checkout main &&
      +	test_commit -C fetch_upstream u_main_post &&
      +	git -C fetch_upstream checkout fetch_unrelated &&
      +	test_commit -C fetch_upstream u_unrelated_post &&
      +	git checkout --track=fetch -b local_from_remote fetch_upstream &&
      +	test_cmp_rev refs/remotes/fetch_upstream/main HEAD &&
     -+	test "$(git rev-parse refs/remotes/fetch_upstream/fetch_unrelated)" = "$unrelated_before"
     ++	test_cmp_rev refs/remotes/fetch_upstream/fetch_unrelated snapshot_unrelated
      +'
      +
      +test_expect_success 'checkout --track=fetch aborts and does not create branch when no existing ref' '
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_cmp_rev refs/remotes/custom-ns/fetch_refspec HEAD
      +'
      +
     -+test_expect_success 'checkout --track=fetch on namespace bare name follows <ns>/HEAD' '
     ++test_expect_success 'checkout --track=fetch on bare remote-tracking prefix follows <prefix>/HEAD' '
      +	git checkout main &&
      +	git remote add fetch_ns ./fetch_upstream &&
      +	test_when_finished "git remote remove fetch_ns" &&
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_cmp_config refs/heads/main branch.local_ns.merge
      +'
      +
     -+test_expect_success '--track=fetch on bare hierarchical remote name follows <ns>/HEAD' '
     -+	git checkout main &&
     -+	git remote add nested/bare ./fetch_upstream &&
     -+	test_when_finished "git remote remove nested/bare" &&
     -+	test_when_finished "git update-ref -d refs/remotes/nested/bare/HEAD" &&
     -+	git fetch nested/bare &&
     -+	git symbolic-ref refs/remotes/nested/bare/HEAD \
     -+		refs/remotes/nested/bare/main &&
     -+	git -C fetch_upstream checkout main &&
     -+	test_commit -C fetch_upstream u_nested_bare_post &&
     -+	git checkout --track=fetch -b local_nested_bare nested/bare &&
     -+	test_cmp_rev refs/remotes/nested/bare/main HEAD
     -+'
     -+
     -+test_expect_success 'checkout --track=fetch handles hierarchical remote name' '
     -+	git checkout main &&
     -+	git remote add nested/remote ./fetch_upstream &&
     -+	test_when_finished "git remote remove nested/remote" &&
     -+	git -C fetch_upstream checkout -b fetch_hier &&
     -+	test_commit -C fetch_upstream u_hier &&
     -+	test_must_fail git rev-parse --verify refs/remotes/nested/remote/fetch_hier &&
     -+	git checkout --track=fetch -b local_hier nested/remote/fetch_hier &&
     -+	test_cmp_rev refs/remotes/nested/remote/fetch_hier HEAD
     -+'
     -+
      +test_expect_success 'checkout --track=fetch dies on bare remote name with no <ns>/HEAD' '
      +	git checkout main &&
      +	git remote add fetch_nohead ./fetch_upstream &&
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_must_fail git rev-parse --verify refs/heads/local_unknown
      +'
      +
     -+test_expect_success 'checkout --track=fetch rejects <ns>/HEAD pointing outside namespace' '
     ++test_expect_success 'checkout --track=fetch rejects <ns>/HEAD pointing outside the tracking prefix' '
      +	git checkout main &&
      +	git remote add fetch_crossns ./fetch_upstream &&
      +	test_when_finished "git remote remove fetch_crossns" &&
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_must_fail git rev-parse --verify refs/heads/local_invalid
      +'
      +
     -+test_expect_success 'checkout --track=fetch,inherit rejects invalid refname components' '
     -+	git checkout main &&
     -+	test_must_fail git checkout --track=fetch,inherit -b local_invalid \
     -+		"foo..bar" 2>err &&
     -+	test_grep "valid" err &&
     -+	test_must_fail git rev-parse --verify refs/heads/local_invalid
     -+'
     -+
      +test_expect_success 'checkout --track=inherit,direct is rejected' '
      +	test_must_fail git checkout --track=inherit,direct -b bad fetch_upstream/fetch_new 2>err &&
      +	test_grep "cannot combine" err
      +'
      +
     -+test_expect_success 'checkout --track=direct,inherit is rejected' '
     -+	test_must_fail git checkout --track=direct,inherit -b bad fetch_upstream/fetch_new 2>err &&
     -+	test_grep "cannot combine" err
     -+'
     -+
      +test_expect_success 'checkout --track=fetch then --track=direct drops fetch (last-one-wins)' '
      +	git checkout main &&
      +	git -C fetch_upstream checkout -b fetch_lastwin &&
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_lastwin
      +'
      +
     -+test_expect_success 'checkout --track=fetch then --no-track drops fetch' '
     -+	git checkout main &&
     -+	git -C fetch_upstream checkout -b fetch_notrack &&
     -+	test_commit -C fetch_upstream u_notrack &&
     -+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_notrack &&
     -+	test_must_fail git checkout --track=fetch --no-track \
     -+		-b local_notrack fetch_upstream/fetch_notrack &&
     -+	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_notrack
     -+'
     -+
      +test_expect_success 'checkout --track=fetch,inherit fetches remote-tracking start-point' '
      +	git checkout main &&
      +	git -C fetch_upstream checkout -b fetch_inherit &&
     @@ t/t7201-co.sh: test_expect_success 'tracking info copied with autoSetupMerge=inh
      +	test_cmp_rev refs/remotes/fetch_upstream/fetch_inherit HEAD
      +'
      +
     -+test_expect_success 'checkout --track=fetch,inherit errors when start-point does not map to a remote' '
     -+	git checkout main &&
     -+	test_must_fail git checkout --track=fetch,inherit -b bad main 2>err &&
     -+	test_grep "no configured remote" err &&
     -+	test_must_fail git rev-parse --verify refs/heads/bad
     -+'
     -+
      +test_expect_success 'checkout --track=fetch on local start-point errors' '
      +	git checkout main &&
      +	test_must_fail git checkout --track=fetch -b bad main 2>err &&
-- 
gitgitgadget
Previous: Harald NordgrenNext: Harald Nordgren via GitGitGadget
Message 66 of 73 in “checkout: add --fetch to fetch remote before resolving start-point”
  1. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 24, 2026
  2. Ramsay JonesApr 24, 2026
  3. D. Ben KnobleApr 24, 2026
  4. Kristoffer HaugsbakkApr 24, 2026
  5. Marc BranchaudApr 24, 2026
  6. Junio C HamanoApr 24, 2026
  7. Junio C HamanoApr 25, 2026
  8. Comments on Phillip's reviewHarald Nordgren, Apr 25, 2026
  9. Comments on Phillip's reviewHarald Nordgren, Apr 25, 2026
  10. Wrong subject lineHarald Nordgren, Apr 25, 2026
  11. Wrong subject lineHarald Nordgren, Apr 25, 2026
  12. Wrong subject lineHarald Nordgren, Apr 25, 2026
  13. Multiple remotesHarald Nordgren, Apr 25, 2026
  14. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 25, 2026
  15. Ben KnobleApr 25, 2026
  16. ghHarald Nordgren, Apr 25, 2026
  17. Kristoffer HaugsbakkApr 26, 2026
  18. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 26, 2026
  19. Harald NordgrenApr 26, 2026
  20. Ramsay JonesApr 26, 2026
  21. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 26, 2026
  22. Junio C HamanoApr 28, 2026
  23. Harald NordgrenApr 28, 2026
  24. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, Apr 28, 2026
  25. Junio C HamanoMay 3, 2026
  26. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 3, 2026
  27. checkout: add --autostash option for branch switchingHarald Nordgren, May 3, 2026
  28. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren, May 7, 2026
  29. Phillip WoodMay 8, 2026
  30. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren, May 8, 2026
  31. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 8, 2026
  32. Phillip WoodMay 11, 2026
  33. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 11, 2026
  34. Junio C HamanoMay 12, 2026
  35. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 12, 2026
  36. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 18, 2026
  37. Harald NordgrenMay 18, 2026
  38. Junio C HamanoMay 19, 2026
  39. Harald NordgrenMay 19, 2026
  40. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 19, 2026
  41. Junio C HamanoMay 19, 2026
  42. Harald NordgrenMay 19, 2026
  43. Harald NordgrenMay 19, 2026
  44. Junio C HamanoMay 19, 2026
  45. Phillip WoodMay 21, 2026
  46. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 21, 2026
  47. Harald NordgrenMay 21, 2026
  48. Junio C HamanoMay 21, 2026
  49. Phillip WoodMay 21, 2026
  50. Junio C HamanoMay 21, 2026
  51. 0/2 checkout: --track=fetchHarald Nordgren via GitGitGadget, May 23, 2026
  52. 1/2 branch: expose helpers for finding the remote owning a tracking refHarald Nordgren via GitGitGadget, May 23, 2026
  53. 2/2 checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 23, 2026
  54. Harald NordgrenJun 13, 2026
  55. Junio C HamanoJun 17, 2026
  56. Harald NordgrenJun 17, 2026
  57. Junio C HamanoJun 18, 2026
  58. Harald NordgrenJun 18, 2026
  59. Harald NordgrenJun 18, 2026
  60. 0/2 checkout: --track=fetchHarald Nordgren via GitGitGadget, Jun 18, 2026
  61. 1/2 branch: expose helpers for finding the remote owning a tracking refHarald Nordgren via GitGitGadget, Jun 18, 2026
  62. 2/2 checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, Jun 18, 2026
  63. D. Ben KnobleJun 18, 2026
  64. Phillip WoodJun 23, 2026
  65. Harald NordgrenJun 23, 2026
  66. 0/2 checkout: --track=fetchHarald Nordgren via GitGitGadget, Jun 24, 2026
  67. 1/2 branch: expose helpers for finding the remote owning a tracking refHarald Nordgren via GitGitGadget, Jun 24, 2026
  68. 2/2 checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, Jun 24, 2026
  69. Junio C HamanoJun 24, 2026
  70. Ben KnobleJun 25, 2026
  71. Harald NordgrenJun 25, 2026
  72. Kristofer KarlssonJun 25, 2026
  73. Junio C HamanoJul 23, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.