checkout: extend --track with a "fetch" mode to refresh start-point

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. 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. Add a "fetch" mode to "--track" that refreshes <start-point> before checking it out: git checkout -b new_branch --track=fetch origin/some-branch 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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Jun 24, 2026 at 21:54 UTC 8b13e2d194d5e1c2280d33de6b64c778a9cfa2ff
4 files changed +375 -7
Documentation/git-checkout.adoc
+16 -1
@@ -158,11 +158,26 @@ of it").
158 resets _<branch>_ to the start point instead of failing.
159
160 `-t`::
161 -`--track[=(direct|inherit)]`::
161 +`--track[=(direct|inherit|fetch)[,...]]`::
162 When creating a new branch, set up "upstream" configuration. See
163 `--track` in linkgit:git-branch[1] for details. As a convenience,
164 --track without -b implies branch creation.
165 +
166 +The argument is a comma-separated list. `direct` (the default) and
167 +`inherit` select the tracking mode and are mutually exclusive. Adding
168 +`fetch` requests that the remote be fetched before _<start-point>_ is
169 +resolved, so the new branch starts from a fresh tip: when
170 +_<start-point>_ is in _<remote>/<branch>_ form, only that branch is
171 +updated; when _<start-point>_ is a bare _<remote>_ (e.g. `origin`), the
172 +branch named by _<remote>/HEAD_ is updated, and the checkout fails
173 +with a hint to configure that symref if it is not set. The checkout
174 +also fails if no configured remote's fetch refspec maps to
175 +_<start-point>_, or if more than one does (in which case the `fetch`
176 +cannot be unambiguously routed). If the fetch itself fails and the
177 +corresponding remote-tracking ref already exists, a warning is printed
178 +and the checkout proceeds from the existing tip; otherwise the checkout
179 +is aborted.
180 ++
181 If no `-b` option is given, the name of the new branch will be
182 derived from the remote-tracking branch, by looking at the local part of
183 the refspec configured for the corresponding remote, and then stripping
Documentation/git-switch.adoc
+3 -2
@@ -154,10 +154,11 @@ should result in deletion of the path).
154 attached to a terminal, regardless of `--quiet`.
155
156 `-t`::
157 -`--track[ (direct|inherit)]`::
157 +`--track[=(direct|inherit|fetch)[,...]]`::
158 When creating a new branch, set up "upstream" configuration.
159 `-c` is implied. See `--track` in linkgit:git-branch[1] for
160 - details.
160 + details, and `--track` in linkgit:git-checkout[1] for the
161 + `fetch` mode.
162 +
163 If no `-c` option is given, the name of the new branch will be derived
164 from the remote-tracking branch, by looking at the local part of the
builtin/checkout.c
+134 -4
@@ -26,10 +26,12 @@
26 #include "preload-index.h"
27 #include "read-cache.h"
28 #include "refs.h"
29 +#include "refspec.h"
30 #include "remote.h"
31 #include "repo-settings.h"
32 #include "resolve-undo.h"
33 #include "revision.h"
34 +#include "run-command.h"
35 #include "setup.h"
36 #include "submodule.h"
37 #include "symlinks.h"
@@ -61,6 +63,7 @@ struct checkout_opts {
63 int count_checkout_paths;
64 int overlay_mode;
65 int dwim_new_local_branch;
66 + int fetch;
67 int discard_changes;
68 int accept_ref;
69 int accept_pathspec;
@@ -112,6 +115,128 @@ struct branch_info {
115 char *checkout;
116 };
117
118 +static void fetch_remote_for_start_point(const char *arg, int quiet)
119 +{
120 + struct strbuf dst = STRBUF_INIT;
121 + struct tracking tracking = { 0 };
122 + struct string_list tracking_srcs = STRING_LIST_INIT_DUP;
123 + struct string_list ambiguous_remotes = STRING_LIST_INIT_DUP;
124 + struct child_process cmd = CHILD_PROCESS_INIT;
125 + struct remote *named_remote;
126 + int bare_ns;
127 +
128 + strbuf_addf(&dst, "refs/remotes/%s", arg);
129 + if (check_refname_format(dst.buf, 0))
130 + die(_("cannot fetch start-point '%s': not a valid "
131 + "remote-tracking name"), arg);
132 +
133 + named_remote = remote_get(arg);
134 + bare_ns = !strchr(arg, '/') ||
135 + (named_remote && remote_is_configured(named_remote, 1));
136 + if (bare_ns) {
137 + char *head_path = xstrfmt("refs/remotes/%s/HEAD", arg);
138 + const char *head_target =
139 + refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
140 + head_path,
141 + RESOLVE_REF_READING,
142 + NULL, NULL);
143 + if (head_target &&
144 + starts_with(head_target, dst.buf) &&
145 + head_target[dst.len] == '/') {
146 + strbuf_reset(&dst);
147 + strbuf_addstr(&dst, head_target);
148 + bare_ns = 0;
149 + }
150 + free(head_path);
151 + }
152 +
153 + tracking.spec.dst = dst.buf;
154 + tracking.srcs = &tracking_srcs;
155 + find_tracking_remote_for_ref(&tracking, &ambiguous_remotes);
156 +
157 + if (tracking.matches > 1) {
158 + int status = die_message(_("cannot fetch start-point '%s': "
159 + "fetch refspecs of multiple remotes "
160 + "map to '%s'"), arg, dst.buf);
161 + advise_ambiguous_fetch_refspec(dst.buf, &ambiguous_remotes);
162 + exit(status);
163 + }
164 +
165 + if (!tracking.matches) {
166 + if (bare_ns && named_remote &&
167 + remote_is_configured(named_remote, 1)) {
168 + int status = die_message(_("cannot fetch start-point '%s' "
169 + "because 'refs/remotes/%s/HEAD' "
170 + "does not exist."), arg, arg);
171 + advise(_("To create it run\n"
172 + "\n"
173 + " git remote set-head %s --auto\n"), arg);
174 + exit(status);
175 + }
176 + die(_("cannot fetch start-point '%s': no configured remote's "
177 + "fetch refspec matches it"), arg);
178 + }
179 +
180 + strvec_push(&cmd.args, "fetch");
181 + if (quiet)
182 + strvec_push(&cmd.args, "--quiet");
183 + strvec_pushl(&cmd.args, tracking.remote,
184 + tracking_srcs.items[0].string, NULL);
185 + cmd.git_cmd = 1;
186 + if (run_command(&cmd)) {
187 + if (refs_ref_exists(get_main_ref_store(the_repository), dst.buf))
188 + warning(_("failed to fetch start-point '%s'; "
189 + "using existing '%s'"), arg, dst.buf);
190 + else
191 + die(_("failed to fetch start-point '%s'"), arg);
192 + }
193 +
194 + string_list_clear(&tracking_srcs, 0);
195 + string_list_clear(&ambiguous_remotes, 0);
196 + strbuf_release(&dst);
197 +}
198 +
199 +static int parse_opt_checkout_track(const struct option *opt,
200 + const char *arg, int unset)
201 +{
202 + struct checkout_opts *opts = opt->value;
203 + struct string_list tokens = STRING_LIST_INIT_DUP;
204 + struct string_list_item *item;
205 + int saw_direct = 0;
206 + int ret = 0;
207 +
208 + opts->fetch = 0;
209 + if (unset) {
210 + opts->track = BRANCH_TRACK_NEVER;
211 + return 0;
212 + }
213 + opts->track = BRANCH_TRACK_EXPLICIT;
214 + if (!arg)
215 + return 0;
216 +
217 + string_list_split(&tokens, arg, ",", -1);
218 + for_each_string_list_item(item, &tokens) {
219 + if (!strcmp(item->string, "fetch"))
220 + opts->fetch = 1;
221 + else if (!strcmp(item->string, "direct"))
222 + saw_direct = 1;
223 + else if (!strcmp(item->string, "inherit"))
224 + opts->track = BRANCH_TRACK_INHERIT;
225 + else {
226 + ret = error(_("option `%s' expects \"%s\", \"%s\", "
227 + "or \"%s\""),
228 + "--track", "direct", "inherit", "fetch");
229 + goto out;
230 + }
231 + }
232 + if (saw_direct && opts->track == BRANCH_TRACK_INHERIT)
233 + ret = error(_("option `%s' cannot combine \"%s\" and \"%s\""),
234 + "--track", "direct", "inherit");
235 +out:
236 + string_list_clear(&tokens, 0);
237 + return ret;
238 +}
239 +
240 static void branch_info_release(struct branch_info *info)
241 {
242 free(info->name);
@@ -1734,10 +1859,10 @@ static struct option *add_common_switch_branch_options(
1859 {
1860 struct option options[] = {
1861 OPT_BOOL('d', "detach", &opts->force_detach, N_("detach HEAD at named commit")),
1737 - OPT_CALLBACK_F('t', "track", &opts->track, "(direct|inherit)",
1862 + OPT_CALLBACK_F('t', "track", opts, "(direct|inherit|fetch)[,...]",
1863 N_("set branch tracking configuration"),
1864 PARSE_OPT_OPTARG,
1740 - parse_opt_tracking_mode),
1865 + parse_opt_checkout_track),
1866 OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"),
1867 PARSE_OPT_NOCOMPLETE),
1868 OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unborn branch")),
@@ -1942,8 +2067,13 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
2067 opts->dwim_new_local_branch &&
2068 opts->track == BRANCH_TRACK_UNSPECIFIED &&
2069 !opts->new_branch;
1945 - int n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
1946 - &new_branch_info, opts, &rev);
2070 + int n;
2071 +
2072 + if (opts->fetch)
2073 + fetch_remote_for_start_point(argv[0], opts->quiet);
2074 +
2075 + n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
2076 + &new_branch_info, opts, &rev);
2077 argv += n;
2078 argc -= n;
2079 } else if (!opts->accept_ref && opts->from_treeish) {
t/t7201-co.sh
+222
@@ -801,4 +801,226 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
801 test_cmp_config "" --default "" branch.main2.merge
802 '
803
804 +test_expect_success 'setup upstream for --track=fetch tests' '
805 + git checkout main &&
806 + git init fetch_upstream &&
807 + test_commit -C fetch_upstream u_main &&
808 + git remote add fetch_upstream fetch_upstream &&
809 + git fetch fetch_upstream &&
810 + git -C fetch_upstream checkout -b fetch_new &&
811 + test_commit -C fetch_upstream u_new
812 +'
813 +
814 +test_expect_success 'checkout --track=fetch -b picks up branch created upstream after clone' '
815 + git checkout main &&
816 + test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_new &&
817 + git checkout --track=fetch -b local_new fetch_upstream/fetch_new &&
818 + test_cmp_rev refs/remotes/fetch_upstream/fetch_new HEAD &&
819 + test_cmp_config fetch_upstream branch.local_new.remote &&
820 + test_cmp_config refs/heads/fetch_new branch.local_new.merge
821 +'
822 +
823 +test_expect_success 'checkout --track=fetch <remote>/<branch> leaves other tracking branches untouched' '
824 + git checkout main &&
825 + git -C fetch_upstream checkout -b fetch_target &&
826 + test_commit -C fetch_upstream u_target_pre &&
827 + git -C fetch_upstream checkout -b fetch_other &&
828 + test_commit -C fetch_upstream u_other_pre &&
829 + git fetch fetch_upstream &&
830 + git update-ref refs/heads/snapshot_other refs/remotes/fetch_upstream/fetch_other &&
831 + git -C fetch_upstream checkout fetch_target &&
832 + test_commit -C fetch_upstream u_target_post &&
833 + git -C fetch_upstream checkout fetch_other &&
834 + test_commit -C fetch_upstream u_other_post &&
835 + git checkout --track=fetch -b local_target fetch_upstream/fetch_target &&
836 + test_cmp_rev refs/remotes/fetch_upstream/fetch_target HEAD &&
837 + test_cmp_rev refs/remotes/fetch_upstream/fetch_other snapshot_other
838 +'
839 +
840 +test_expect_success 'checkout --track=fetch with bare remote name fetches only <remote>/HEAD target' '
841 + git checkout main &&
842 + git -C fetch_upstream checkout main &&
843 + git remote set-head fetch_upstream main &&
844 + git -C fetch_upstream checkout -b fetch_unrelated &&
845 + test_commit -C fetch_upstream u_unrelated_pre &&
846 + git fetch fetch_upstream fetch_unrelated &&
847 + git update-ref refs/heads/snapshot_unrelated \
848 + refs/remotes/fetch_upstream/fetch_unrelated &&
849 + git -C fetch_upstream checkout main &&
850 + test_commit -C fetch_upstream u_main_post &&
851 + git -C fetch_upstream checkout fetch_unrelated &&
852 + test_commit -C fetch_upstream u_unrelated_post &&
853 + git checkout --track=fetch -b local_from_remote fetch_upstream &&
854 + test_cmp_rev refs/remotes/fetch_upstream/main HEAD &&
855 + test_cmp_rev refs/remotes/fetch_upstream/fetch_unrelated snapshot_unrelated
856 +'
857 +
858 +test_expect_success 'checkout --track=fetch aborts and does not create branch when no existing ref' '
859 + git checkout main &&
860 + test_might_fail git branch -D bogus &&
861 + test_must_fail git checkout --track=fetch -b bogus fetch_upstream/does_not_exist &&
862 + test_must_fail git rev-parse --verify refs/heads/bogus
863 +'
864 +
865 +test_expect_success 'checkout --track=fetch warns and proceeds when fetch fails but ref exists' '
866 + git checkout main &&
867 + git -C fetch_upstream checkout -b fetch_offline &&
868 + test_commit -C fetch_upstream u_offline &&
869 + git fetch fetch_upstream fetch_offline &&
870 + saved_url=$(git config remote.fetch_upstream.url) &&
871 + test_when_finished "git config remote.fetch_upstream.url \"$saved_url\"" &&
872 + git config remote.fetch_upstream.url ./does-not-exist &&
873 + git checkout --track=fetch -b local_offline fetch_upstream/fetch_offline 2>err &&
874 + test_grep "failed to fetch" err &&
875 + test_cmp_rev refs/remotes/fetch_upstream/fetch_offline HEAD
876 +'
877 +
878 +test_expect_success 'checkout --track=fetch resolves through configured fetch refspec' '
879 + git checkout main &&
880 + git remote add fetch_custom ./fetch_upstream &&
881 + test_when_finished "git remote remove fetch_custom" &&
882 + git config --replace-all remote.fetch_custom.fetch \
883 + "+refs/heads/*:refs/remotes/custom-ns/*" &&
884 + git -C fetch_upstream checkout -b fetch_refspec &&
885 + test_commit -C fetch_upstream u_refspec &&
886 + test_must_fail git rev-parse --verify refs/remotes/custom-ns/fetch_refspec &&
887 + git checkout --track=fetch -b local_refspec custom-ns/fetch_refspec &&
888 + test_cmp_rev refs/remotes/custom-ns/fetch_refspec HEAD
889 +'
890 +
891 +test_expect_success 'checkout --track=fetch on bare remote-tracking prefix follows <prefix>/HEAD' '
892 + git checkout main &&
893 + git remote add fetch_ns ./fetch_upstream &&
894 + test_when_finished "git remote remove fetch_ns" &&
895 + test_when_finished "git update-ref -d refs/remotes/ns_alias/HEAD" &&
896 + git config --replace-all remote.fetch_ns.fetch \
897 + "+refs/heads/*:refs/remotes/ns_alias/*" &&
898 + git fetch fetch_ns &&
899 + git symbolic-ref refs/remotes/ns_alias/HEAD refs/remotes/ns_alias/main &&
900 + git -C fetch_upstream checkout main &&
901 + test_commit -C fetch_upstream u_ns_post &&
902 + git checkout --track=fetch -b local_ns ns_alias &&
903 + test_cmp_rev refs/remotes/ns_alias/main HEAD &&
904 + test_cmp_config fetch_ns branch.local_ns.remote &&
905 + test_cmp_config refs/heads/main branch.local_ns.merge
906 +'
907 +
908 +test_expect_success 'checkout --track=fetch dies on bare remote name with no <ns>/HEAD' '
909 + git checkout main &&
910 + git remote add fetch_nohead ./fetch_upstream &&
911 + test_when_finished "git remote remove fetch_nohead" &&
912 + test_might_fail git symbolic-ref -d refs/remotes/fetch_nohead/HEAD &&
913 + test_must_fail git checkout --track=fetch -b local_nohead fetch_nohead 2>err &&
914 + test_grep "refs/remotes/fetch_nohead/HEAD" err &&
915 + test_grep "git remote set-head fetch_nohead --auto" err &&
916 + test_must_fail git rev-parse --verify refs/heads/local_nohead
917 +'
918 +
919 +test_expect_success 'checkout --track=fetch on bare unknown name does not suggest set-head' '
920 + git checkout main &&
921 + test_must_fail git rev-parse --verify refs/remotes/no_such_ns/HEAD &&
922 + test_must_fail git config --get remote.no_such_ns.url &&
923 + test_must_fail git checkout --track=fetch -b local_unknown no_such_ns 2>err &&
924 + test_grep "no configured remote" err &&
925 + test_grep ! "set-head" err &&
926 + test_must_fail git rev-parse --verify refs/heads/local_unknown
927 +'
928 +
929 +test_expect_success 'checkout --track=fetch rejects <ns>/HEAD pointing outside the tracking prefix' '
930 + git checkout main &&
931 + git remote add fetch_crossns ./fetch_upstream &&
932 + test_when_finished "git remote remove fetch_crossns" &&
933 + test_when_finished "git update-ref -d refs/remotes/fetch_crossns/HEAD" &&
934 + git fetch fetch_crossns &&
935 + git symbolic-ref refs/remotes/fetch_crossns/HEAD \
936 + refs/remotes/fetch_upstream/u_main &&
937 + test_must_fail git checkout --track=fetch -b local_crossns fetch_crossns 2>err &&
938 + test_grep "refs/remotes/fetch_crossns/HEAD" err &&
939 + test_must_fail git rev-parse --verify refs/heads/local_crossns
940 +'
941 +
942 +test_expect_success 'checkout --track=fetch dies on ambiguous fetch refspec match' '
943 + git checkout main &&
944 + git remote add fetch_ambig_a ./fetch_upstream &&
945 + git remote add fetch_ambig_b ./fetch_upstream &&
946 + test_when_finished "git remote remove fetch_ambig_a" &&
947 + test_when_finished "git remote remove fetch_ambig_b" &&
948 + git config --replace-all remote.fetch_ambig_a.fetch \
949 + "+refs/heads/*:refs/remotes/ambig_ns/*" &&
950 + git config --replace-all remote.fetch_ambig_b.fetch \
951 + "+refs/heads/*:refs/remotes/ambig_ns/*" &&
952 + git -C fetch_upstream checkout -b fetch_ambig &&
953 + test_commit -C fetch_upstream u_ambig &&
954 + test_must_fail git checkout --track=fetch -b local_ambig ambig_ns/fetch_ambig 2>err &&
955 + test_grep "fetch_ambig_a" err &&
956 + test_grep "fetch_ambig_b" err &&
957 + test_grep "tracking namespaces" err &&
958 + test_must_fail git rev-parse --verify refs/heads/local_ambig
959 +'
960 +
961 +test_expect_success 'checkout --track=fetch rejects invalid refname components' '
962 + git checkout main &&
963 + test_must_fail git checkout --track=fetch -b local_invalid "foo..bar" 2>err &&
964 + test_grep "valid" err &&
965 + test_must_fail git rev-parse --verify refs/heads/local_invalid
966 +'
967 +
968 +test_expect_success 'checkout --track=inherit,direct is rejected' '
969 + test_must_fail git checkout --track=inherit,direct -b bad fetch_upstream/fetch_new 2>err &&
970 + test_grep "cannot combine" err
971 +'
972 +
973 +test_expect_success 'checkout --track=fetch then --track=direct drops fetch (last-one-wins)' '
974 + git checkout main &&
975 + git -C fetch_upstream checkout -b fetch_lastwin &&
976 + test_commit -C fetch_upstream u_lastwin &&
977 + test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_lastwin &&
978 + test_must_fail git checkout --track=fetch --track=direct \
979 + -b local_lastwin fetch_upstream/fetch_lastwin &&
980 + test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_lastwin
981 +'
982 +
983 +test_expect_success 'checkout --track=fetch,inherit fetches remote-tracking start-point' '
984 + git checkout main &&
985 + git -C fetch_upstream checkout -b fetch_inherit &&
986 + test_commit -C fetch_upstream u_inherit &&
987 + test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_inherit &&
988 + git checkout --track=fetch,inherit -b local_inherit \
989 + fetch_upstream/fetch_inherit &&
990 + test_cmp_rev refs/remotes/fetch_upstream/fetch_inherit HEAD
991 +'
992 +
993 +test_expect_success 'checkout --track=fetch on local start-point errors' '
994 + git checkout main &&
995 + test_must_fail git checkout --track=fetch -b bad main 2>err &&
996 + test_grep "no configured remote" err &&
997 + test_must_fail git rev-parse --verify refs/heads/bad
998 +'
999 +
1000 +test_expect_success 'checkout --track=bogus reports an error' '
1001 + git checkout main &&
1002 + test_must_fail git checkout --track=bogus -b bogus_branch fetch_upstream/fetch_new 2>err &&
1003 + test_grep "expects" err
1004 +'
1005 +
1006 +test_expect_success 'checkout -q --track=fetch silences the fetch output' '
1007 + git checkout main &&
1008 + git -C fetch_upstream checkout -b fetch_quiet &&
1009 + test_commit -C fetch_upstream u_quiet &&
1010 + test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_quiet &&
1011 + git checkout -q --track=fetch -b local_quiet \
1012 + fetch_upstream/fetch_quiet 2>err &&
1013 + test_grep ! "-> fetch_upstream/fetch_quiet" err &&
1014 + test_cmp_rev refs/remotes/fetch_upstream/fetch_quiet HEAD
1015 +'
1016 +
1017 +test_expect_success 'switch --track=fetch -c picks up branch created upstream after clone' '
1018 + git checkout main &&
1019 + git -C fetch_upstream checkout -b fetch_switch &&
1020 + test_commit -C fetch_upstream u_switch &&
1021 + test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_switch &&
1022 + git switch --track=fetch -c local_switch fetch_upstream/fetch_switch &&
1023 + test_cmp_rev refs/remotes/fetch_upstream/fetch_switch HEAD
1024 +'
1025 +
1026 test_done