git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v10] checkout: extend --track with a "fetch" mode to refresh start-point

From
Junio C Hamano <gitster@pobox.com>
Date
May 19, 2026, 06:16 UTC
Message-ID
<xmqq8q9f9b5w.fsf@gitster.g>
In-Reply-To
<pull.2281.v10.git.git.1779091483321.gitgitgadget@gmail.com>
"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 193 quoted lines
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> If you want to fork your topic branch from the very latest of the
> tip of a branch your remote has, you would do:
>
>     git fetch origin some-branch
>     git checkout -b new_branch --track origin/some-branch
>
> Extend the "--track" option of "git checkout" and allow users to
> write
>
>     git checkout -b new_branch --track=fetch origin/some-branch
>
> to (1) fetch 'some-branch' from the remote 'origin', updating the
> remote-tracking branch 'origin/some-branch', (2) arrange subsequent
> 'git pull' on 'new_branch' to interact with 'origin/some-branch' and
> (3) fork 'new_branch' from it.
>
> In the value of the '--track' option, 'fetch' can be combined with
> the existing 'direct' (default) and 'inherit' modes via a
> comma-separated list. Examples:
>
>     git checkout -b new_branch --track=fetch,inherit some_local_branch
>     git switch -c new_branch --track=fetch origin/some-branch
>
> When "fetch" is requested and <start-point> is in <remote>/<branch>
> form, run "git fetch <remote> <branch>" before resolving the ref, so
> that other remote-tracking branches are left untouched. If
> <start-point> is a bare remote name like "origin" (which resolves to
> that remote's default branch), "git fetch <remote>" is run instead,
> since the target branch is not known up front. Abort the checkout if
> the fetch fails.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>     checkout: --track=fetch
>     
>     Rebased to fix merge conflict with master.
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2281%2FHaraldNordgren%2Fcheckout-fetch-start-point-v10
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2281/HaraldNordgren/checkout-fetch-start-point-v10
> Pull-Request: https://github.com/git/git/pull/2281
>
> Range-diff vs v9:
>
>  1:  021375e4cc ! 1:  a773fb6bdf checkout: extend --track with a "fetch" mode to refresh start-point
>      @@ Documentation/git-checkout.adoc: of it").
>        the refspec configured for the corresponding remote, and then stripping
>       
>        ## Documentation/git-switch.adoc ##
>      -@@ Documentation/git-switch.adoc: should result in deletion of the path).
>      +@@ Documentation/git-switch.adoc: variable.
>        	attached to a terminal, regardless of `--quiet`.
>        
>        `-t`::
>      @@ builtin/checkout.c
>        #include "resolve-undo.h"
>        #include "revision.h"
>       +#include "run-command.h"
>      + #include "sequencer.h"
>        #include "setup.h"
>        #include "strvec.h"
>      - #include "submodule.h"
>       @@ builtin/checkout.c: struct checkout_opts {
>        	int count_checkout_paths;
>        	int overlay_mode;
>
>
>  Documentation/git-checkout.adoc |  13 ++-
>  Documentation/git-switch.adoc   |  13 ++-
>  builtin/checkout.c              | 168 +++++++++++++++++++++++++++++++-
>  t/t7201-co.sh                   | 144 +++++++++++++++++++++++++++
>  4 files changed, 332 insertions(+), 6 deletions(-)
>
> diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
> index a8b3b8c2e2..ec63434159 100644
> --- a/Documentation/git-checkout.adoc
> +++ b/Documentation/git-checkout.adoc
> @@ -158,11 +158,22 @@ of it").
>  	resets _<branch>_ to the start point instead of failing.
>  
>  `-t`::
> -`--track[=(direct|inherit)]`::
> +`--track[=(direct|inherit|fetch)[,...]]`::
>  	When creating a new branch, set up "upstream" configuration. See
>  	`--track` in linkgit:git-branch[1] for details. As a convenience,
>  	--track without -b implies branch creation.
>  +
> +The argument is a comma-separated list. `direct` (the default) and
> +`inherit` select the tracking mode and are mutually exclusive. Adding
> +`fetch` requests that the remote be fetched before _<start-point>_ is
> +resolved, so the new branch starts from a fresh tip: when
> +_<start-point>_ is in _<remote>/<branch>_ form, only that branch is
> +updated; when _<start-point>_ is a bare remote name (e.g. `origin`),
> +only the remote's default branch is updated. If the fetch fails and the
> +corresponding remote-tracking ref already exists, a warning is printed
> +and the checkout proceeds from the existing tip; otherwise the checkout
> +is aborted.
> ++
>  If no `-b` option is given, the name of the new branch will be
>  derived from the remote-tracking branch, by looking at the local part of
>  the refspec configured for the corresponding remote, and then stripping
> diff --git a/Documentation/git-switch.adoc b/Documentation/git-switch.adoc
> index d6c4f229a5..b5e79435cd 100644
> --- a/Documentation/git-switch.adoc
> +++ b/Documentation/git-switch.adoc
> @@ -155,11 +155,22 @@ variable.
>  	attached to a terminal, regardless of `--quiet`.
>  
>  `-t`::
> -`--track[ (direct|inherit)]`::
> +`--track[=(direct|inherit|fetch)[,...]]`::
>  	When creating a new branch, set up "upstream" configuration.
>  	`-c` is implied. See `--track` in linkgit:git-branch[1] for
>  	details.
>  +
> +The argument is a comma-separated list. `direct` (the default) and
> +`inherit` select the tracking mode and are mutually exclusive. Adding
> +`fetch` requests that the remote be fetched before _<start-point>_ is
> +resolved, so the new branch starts from a fresh tip: when
> +_<start-point>_ is in _<remote>/<branch>_ form, only that branch is
> +updated; when _<start-point>_ is a bare remote name (e.g. `origin`),
> +only the remote's default branch is updated. If the fetch fails and the
> +corresponding remote-tracking ref already exists, a warning is printed
> +and the switch proceeds from the existing tip; otherwise the switch is
> +aborted.
> ++
>  If no `-c` option is given, the name of the new branch will be derived
>  from the remote-tracking branch, by looking at the local part of the
>  refspec configured for the corresponding remote, and then stripping
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index 1345e8574a..fc58456546 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -25,10 +25,12 @@
>  #include "preload-index.h"
>  #include "read-cache.h"
>  #include "refs.h"
> +#include "refspec.h"
>  #include "remote.h"
>  #include "repo-settings.h"
>  #include "resolve-undo.h"
>  #include "revision.h"
> +#include "run-command.h"
>  #include "sequencer.h"
>  #include "setup.h"
>  #include "strvec.h"
> @@ -62,6 +64,7 @@ struct checkout_opts {
>  	int count_checkout_paths;
>  	int overlay_mode;
>  	int dwim_new_local_branch;
> +	int fetch;
>  	int discard_changes;
>  	int accept_ref;
>  	int accept_pathspec;
> @@ -115,6 +118,158 @@ struct branch_info {
>  	char *checkout;
>  };
>  
> +static int resolve_fetch_target(const char *arg, char **remote_out,
> +				char **src_ref_out, char **existing_ref_out)
> +{
> +	const char *slash;
> +	char *remote_name = NULL;
> +	struct remote *remote = NULL;
> +	struct refspec_item query = { 0 };
> +	struct strbuf dst = STRBUF_INIT;
> +	struct object_id oid;
> +	const char *rest = NULL;
> +	const char *head_target = NULL;
> +	const char *short_target;
> +
> +	*remote_out = NULL;
> +	*src_ref_out = NULL;
> +	*existing_ref_out = NULL;
> +
> +	if (!arg || !*arg || *arg == '/')
> +		return -1;
> +
> +	slash = arg + strlen(arg);
> +	while (1) {
> +		free(remote_name);
> +		remote_name = xstrndup(arg, slash - arg);
> +		remote = remote_get(remote_name);
> +		if (remote && remote_is_configured(remote, 1))
> +			break;
> +		while (slash > arg && *--slash != '/')
> +			;
> +		if (slash == arg) {
> +			free(remote_name);
> +			return -1;
> +		}
> +	}

OK. So the caller gives "foo/bar/baz" when "foo/bar" is the name of the remote that uses "refs/remotes/foo/bar" to store remote-tracking branches from there. You check "foo/bar/baz", which fails to be a remote, then "foo/bar", which is a configured remote and break.

> +
> +	if (*slash == '/' && slash[1])
> +		rest = slash + 1;

And "baz" becomes the "rest"; if the user gave us "foo/bar" and the abobve loop found it as the name of the remote, we would want to use "refs/remotes/foo/bar/HEAD", which is the "if (!rest)" below is about.

Show 16 quoted lines
> +	if (!rest) {
> +		strbuf_addf(&dst, "refs/remotes/%s/HEAD", remote_name);
> +		head_target = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
> +						      dst.buf,
> +						      RESOLVE_REF_READING |
> +						      RESOLVE_REF_NO_RECURSE,
> +						      &oid, NULL);
> +		if (head_target) {
> +			*existing_ref_out = xstrdup(dst.buf);
> +			if (skip_prefix(head_target, "refs/remotes/", &short_target) &&
> +			    skip_prefix(short_target, remote_name, &short_target) &&
> +			    *short_target == '/')
> +				rest = short_target + 1;
> +		}
> +		strbuf_reset(&dst);
> +	}

So we may have been given "foo/bar" and after resolving HEAD there, have "baz" in rest. Or "foo/bar/baz" was given and we may have figured out that that is "baz" in "foo/bar".

Show 17 quoted lines
> +	if (rest) {
> +		strbuf_addf(&dst, "refs/remotes/%s/%s", remote_name, rest);
> +		query.dst = dst.buf;
> +		if (!remote_find_tracking(remote, &query) && query.src) {
> +			*src_ref_out = xstrdup(query.src);
> +			free(query.src);
> +		} else {
> +			*src_ref_out = xstrdup(rest);
> +		}
> +		if (!*existing_ref_out) {
> +			strbuf_reset(&dst);
> +			strbuf_addf(&dst, "refs/remotes/%s", arg);
> +			if (!refs_read_ref(get_main_ref_store(the_repository),
> +					   dst.buf, &oid))
> +				*existing_ref_out = xstrdup(dst.buf);
> +		}
> +	}

What happens if "HEAD" was not there, though. If "refs/remotes/foo/bar/" did not exist, we would have already returned -1 after trying to find which part in arg is the remote name. But if "refs/remotes/foo/bar" is valid, the user gave us "foo/bar", and we cannot find HEAD, then the above "if (rest)" is skipped. We give "foo/bar" to *remote_out, and return 0 without touching *src_ref_out or *existing_ref_out at all.

Show 18 quoted lines
> +	strbuf_release(&dst);
> +	*remote_out = remote_name;
> +	return 0;
> +}
> +
> +static void fetch_remote_for_start_point(const char *arg)
> +{
> +	char *remote_name = NULL;
> +	char *src_ref = NULL;
> +	char *existing_ref = NULL;
> +	struct child_process cmd = CHILD_PROCESS_INIT;
> +
> +	if (resolve_fetch_target(arg, &remote_name, &src_ref, &existing_ref))
> +		return;
> +
> +	strvec_pushl(&cmd.args, "fetch", remote_name, NULL);
> +	if (src_ref)
> +		strvec_push(&cmd.args, src_ref);
What should happen with this configuration
    [remote "origin"]
        fetch = refs/heads/*:refs/upstream/*
and the user says either of these two:
    $ git checkout --track=fetch upstream
    $ git checkout --track=fetch upstream/master

We fail to find in "where does the remote name ends and branch name start?" loop that this request is about remote "origin" at all, no? We may see in the former case that there is refs/remotes/upstream/HEAD >that points at "master" in the same hierarchy, but the code thinks "upstream" is the remote name, which would mean you would "git fetch upstream", when the remote you need to fetch from is "origin".

Show 8 quoted lines
> +	cmd.git_cmd = 1;
> +	if (run_command(&cmd)) {
> +		if (existing_ref)
> +			warning(_("failed to fetch start-point '%s'; "
> +				  "using existing '%s'"),
> +				arg, existing_ref);
> +		else
> +			die(_("failed to fetch start-point '%s'"), arg);

If we failed to set *existing_ref_out, shouldn't we fail without even attempting to call run_command() here, as we will have to die() anyway even if "git fetch" succeeds. For that matter, it may be simpler and more correct for resolve_fetch_target() to fail (return -1) when it happens, by making the lat "if (rest) {...}" to have a corresponding "else { return -1 }" after it.

Show 6 quoted lines
> +	}
> +
> +	free(remote_name);
> +	free(src_ref);
> +	free(existing_ref);
> +}
I'll stop here.
Previous: Harald Nordgren via GitGitGadgetNext: Harald Nordgren
Message 41 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. Comments on Phillip's reviewHarald Nordgren, Apr 25, 2026
  5. Wrong subject lineHarald Nordgren, Apr 25, 2026
  6. Harald NordgrenJun 18, 2026
  7. D. Ben KnobleJun 18, 2026
  8. Kristoffer HaugsbakkApr 24, 2026
  9. Comments on Phillip's reviewHarald Nordgren, Apr 25, 2026
  10. Wrong subject lineHarald Nordgren, Apr 25, 2026
  11. Kristoffer HaugsbakkApr 26, 2026
  12. Harald NordgrenApr 26, 2026
  13. Marc BranchaudApr 24, 2026
  14. Wrong subject lineHarald Nordgren, Apr 25, 2026
  15. Junio C HamanoApr 24, 2026
  16. Junio C HamanoApr 25, 2026
  17. Multiple remotesHarald Nordgren, Apr 25, 2026
  18. Ben KnobleApr 25, 2026
  19. ghHarald Nordgren, Apr 25, 2026
  20. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 25, 2026
  21. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 26, 2026
  22. Ramsay JonesApr 26, 2026
  23. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren via GitGitGadget, Apr 26, 2026
  24. Junio C HamanoApr 28, 2026
  25. Harald NordgrenApr 28, 2026
  26. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, Apr 28, 2026
  27. Junio C HamanoMay 3, 2026
  28. checkout: add --autostash option for branch switchingHarald Nordgren, May 3, 2026
  29. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 3, 2026
  30. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren, May 7, 2026
  31. Phillip WoodMay 8, 2026
  32. checkout: add --fetch to fetch remote before resolving start-pointHarald Nordgren, May 8, 2026
  33. Harald NordgrenJun 18, 2026
  34. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 8, 2026
  35. Phillip WoodMay 11, 2026
  36. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 11, 2026
  37. Junio C HamanoMay 12, 2026
  38. Harald NordgrenMay 18, 2026
  39. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 12, 2026
  40. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 18, 2026
  41. Junio C HamanoMay 19, 2026
  42. Harald NordgrenMay 19, 2026
  43. Junio C HamanoMay 19, 2026
  44. Harald NordgrenMay 19, 2026
  45. Harald NordgrenMay 19, 2026
  46. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 19, 2026
  47. Junio C HamanoMay 19, 2026
  48. Phillip WoodMay 21, 2026
  49. Harald NordgrenMay 21, 2026
  50. Junio C HamanoMay 21, 2026
  51. Phillip WoodMay 21, 2026
  52. Junio C HamanoMay 21, 2026
  53. Harald NordgrenJun 13, 2026
  54. checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 21, 2026
  55. 0/2 checkout: --track=fetchHarald Nordgren via GitGitGadget, May 23, 2026
  56. 1/2 branch: expose helpers for finding the remote owning a tracking refHarald Nordgren via GitGitGadget, May 23, 2026
  57. 2/2 checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, May 23, 2026
  58. Junio C HamanoJun 17, 2026
  59. Harald NordgrenJun 17, 2026
  60. Junio C HamanoJun 18, 2026
  61. 0/2 checkout: --track=fetchHarald Nordgren via GitGitGadget, Jun 18, 2026
  62. 1/2 branch: expose helpers for finding the remote owning a tracking refHarald Nordgren via GitGitGadget, Jun 18, 2026
  63. 2/2 checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, Jun 18, 2026
  64. Phillip WoodJun 23, 2026
  65. Harald NordgrenJun 23, 2026
  66. Junio C HamanoJun 24, 2026
  67. Ben KnobleJun 25, 2026
  68. Harald NordgrenJun 25, 2026
  69. Kristofer KarlssonJun 25, 2026
  70. 0/2 checkout: --track=fetchHarald Nordgren via GitGitGadget, Jun 24, 2026
  71. 1/2 branch: expose helpers for finding the remote owning a tracking refHarald Nordgren via GitGitGadget, Jun 24, 2026
  72. 2/2 checkout: extend --track with a "fetch" mode to refresh start-pointHarald Nordgren via GitGitGadget, Jun 24, 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.