Skip to content

Commit df7b638

Browse files
checkout: add --fetch to fetch remote before resolving start-point
A common workflow is: git fetch origin git checkout -b new_branch origin/some-branch The first command exists purely so the second sees an up-to-date view of the remote. If it is forgotten, origin/some-branch points at a stale commit and the new local branch is created from the wrong start point. Teach checkout (and switch) a --fetch flag that folds the two steps into one: git checkout --fetch -b new_branch origin/some-branch When --fetch is given and <start-point> is in <remote>/<branch> form, run "git fetch <remote> <branch>" before resolving the ref. This narrows the fetch to the requested branch so that other remote-tracking branches are left untouched -- many tools rely on the stability of remote-tracking refs between explicit fetches. 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. Also add a checkout.fetch config to enable this by default. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
1 parent 94f0577 commit df7b638

6 files changed

Lines changed: 144 additions & 2 deletions

File tree

Documentation/config/checkout.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ commands or functionality in the future.
2222
option in `git checkout` and `git switch`. See
2323
linkgit:git-switch[1] and linkgit:git-checkout[1].
2424

25+
`checkout.fetch`::
26+
Provides the default value for the `--fetch` or `--no-fetch`
27+
option in `git checkout` and `git switch`. See
28+
linkgit:git-switch[1] and linkgit:git-checkout[1].
29+
2530
`checkout.workers`::
2631
The number of parallel workers to use when updating the working tree.
2732
The default is one, i.e. sequential execution. If set to a value less

Documentation/git-checkout.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ linkgit:git-config[1].
201201
The default behavior can be set via the `checkout.guess` configuration
202202
variable.
203203
204+
`--fetch`::
205+
`--no-fetch`::
206+
If _<start-point>_ refers to a remote-tracking branch, fetch
207+
from that remote before resolving it. When _<start-point>_ is
208+
in _<remote>/<branch>_ form, only that branch is updated; when
209+
it is a bare remote name (e.g. `origin`), the whole remote is
210+
fetched. If the fetch fails, the checkout is aborted.
211+
+
212+
The default behavior can be set via the `checkout.fetch` configuration
213+
variable.
214+
204215
`-l`::
205216
Create the new branch's reflog; see linkgit:git-branch[1] for
206217
details.

Documentation/git-switch.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ ambiguous but exists on the 'origin' remote. See also
110110
The default behavior can be set via the `checkout.guess` configuration
111111
variable.
112112

113+
`--fetch`::
114+
`--no-fetch`::
115+
If _<start-point>_ refers to a remote-tracking branch, fetch
116+
from that remote before resolving it. When _<start-point>_ is
117+
in _<remote>/<branch>_ form, only that branch is updated; when
118+
it is a bare remote name (e.g. `origin`), the whole remote is
119+
fetched. If the fetch fails, the switch is aborted.
120+
+
121+
The default behavior can be set via the `checkout.fetch` configuration
122+
variable.
123+
113124
`-f`::
114125
`--force`::
115126
An alias for `--discard-changes`.

builtin/checkout.c

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
#include "repo-settings.h"
3131
#include "resolve-undo.h"
3232
#include "revision.h"
33+
#include "run-command.h"
3334
#include "setup.h"
35+
#include "strvec.h"
3436
#include "submodule.h"
3537
#include "symlinks.h"
3638
#include "trace2.h"
@@ -61,6 +63,7 @@ struct checkout_opts {
6163
int count_checkout_paths;
6264
int overlay_mode;
6365
int dwim_new_local_branch;
66+
int fetch;
6467
int discard_changes;
6568
int accept_ref;
6669
int accept_pathspec;
@@ -112,6 +115,36 @@ struct branch_info {
112115
char *checkout;
113116
};
114117

118+
static void fetch_remote_for_start_point(const char *arg)
119+
{
120+
const char *slash;
121+
char *remote_name;
122+
struct remote *remote;
123+
struct child_process cmd = CHILD_PROCESS_INIT;
124+
125+
if (!arg || !*arg)
126+
return;
127+
128+
slash = strchr(arg, '/');
129+
if (slash == arg)
130+
return;
131+
remote_name = slash ? xstrndup(arg, slash - arg) : xstrdup(arg);
132+
133+
remote = remote_get(remote_name);
134+
if (!remote || !remote_is_configured(remote, 1)) {
135+
free(remote_name);
136+
return;
137+
}
138+
139+
strvec_pushl(&cmd.args, "fetch", remote_name, NULL);
140+
if (slash && slash[1])
141+
strvec_push(&cmd.args, slash + 1);
142+
cmd.git_cmd = 1;
143+
free(remote_name);
144+
if (run_command(&cmd))
145+
die(_("failed to fetch start-point '%s'"), arg);
146+
}
147+
115148
static void branch_info_release(struct branch_info *info)
116149
{
117150
free(info->name);
@@ -1237,6 +1270,10 @@ static int git_checkout_config(const char *var, const char *value,
12371270
opts->dwim_new_local_branch = git_config_bool(var, value);
12381271
return 0;
12391272
}
1273+
if (!strcmp(var, "checkout.fetch")) {
1274+
opts->fetch = git_config_bool(var, value);
1275+
return 0;
1276+
}
12401277

12411278
if (starts_with(var, "submodule."))
12421279
return git_default_submodule_config(var, value, NULL);
@@ -1942,8 +1979,13 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
19421979
opts->dwim_new_local_branch &&
19431980
opts->track == BRANCH_TRACK_UNSPECIFIED &&
19441981
!opts->new_branch;
1945-
int n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
1946-
&new_branch_info, opts, &rev);
1982+
int n;
1983+
1984+
if (opts->fetch)
1985+
fetch_remote_for_start_point(argv[0]);
1986+
1987+
n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
1988+
&new_branch_info, opts, &rev);
19471989
argv += n;
19481990
argc -= n;
19491991
} else if (!opts->accept_ref && opts->from_treeish) {
@@ -2052,6 +2094,8 @@ int cmd_checkout(int argc,
20522094
OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode (default)")),
20532095
OPT_BOOL(0, "auto-advance", &opts.auto_advance,
20542096
N_("auto advance to the next file when selecting hunks interactively")),
2097+
OPT_BOOL(0, "fetch", &opts.fetch,
2098+
N_("fetch from the remote first if <start-point> is a remote-tracking branch")),
20552099
OPT_END()
20562100
};
20572101

@@ -2102,6 +2146,8 @@ int cmd_switch(int argc,
21022146
N_("second guess 'git switch <no-such-branch>'")),
21032147
OPT_BOOL(0, "discard-changes", &opts.discard_changes,
21042148
N_("throw away local modifications")),
2149+
OPT_BOOL(0, "fetch", &opts.fetch,
2150+
N_("fetch from the remote first if <start-point> is a remote-tracking branch")),
21052151
OPT_END()
21062152
};
21072153

t/t7201-co.sh

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,4 +801,72 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
801801
test_cmp_config "" --default "" branch.main2.merge
802802
'
803803

804+
test_expect_success 'setup upstream for --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 --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 --fetch -b local_new fetch_upstream/fetch_new &&
818+
test_cmp_rev refs/remotes/fetch_upstream/fetch_new HEAD
819+
'
820+
821+
test_expect_success 'checkout --fetch <remote>/<branch> leaves other tracking branches untouched' '
822+
git checkout main &&
823+
git -C fetch_upstream checkout -b fetch_target &&
824+
test_commit -C fetch_upstream u_target_pre &&
825+
git -C fetch_upstream checkout -b fetch_other &&
826+
test_commit -C fetch_upstream u_other_pre &&
827+
git fetch fetch_upstream &&
828+
other_before=$(git rev-parse refs/remotes/fetch_upstream/fetch_other) &&
829+
git -C fetch_upstream checkout fetch_target &&
830+
test_commit -C fetch_upstream u_target_post &&
831+
git -C fetch_upstream checkout fetch_other &&
832+
test_commit -C fetch_upstream u_other_post &&
833+
git checkout --fetch -b local_target fetch_upstream/fetch_target &&
834+
test_cmp_rev refs/remotes/fetch_upstream/fetch_target HEAD &&
835+
test "$(git rev-parse refs/remotes/fetch_upstream/fetch_other)" = "$other_before"
836+
'
837+
838+
test_expect_success 'checkout --fetch with bare remote name fetches the remote' '
839+
git checkout main &&
840+
git -C fetch_upstream checkout -b fetch_new2 &&
841+
test_commit -C fetch_upstream u_new2 &&
842+
test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_new2 &&
843+
git checkout --fetch -b local_from_remote fetch_upstream &&
844+
git rev-parse --verify refs/remotes/fetch_upstream/fetch_new2
845+
'
846+
847+
test_expect_success 'checkout --fetch aborts and does not create branch on fetch failure' '
848+
git checkout main &&
849+
test_might_fail git branch -D bogus &&
850+
test_must_fail git checkout --fetch -b bogus fetch_upstream/does_not_exist &&
851+
test_must_fail git rev-parse --verify refs/heads/bogus
852+
'
853+
854+
test_expect_success 'checkout.fetch=true enables fetching without --fetch' '
855+
git checkout main &&
856+
git -C fetch_upstream checkout -b fetch_cfg &&
857+
test_commit -C fetch_upstream u_cfg &&
858+
test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_cfg &&
859+
git -c checkout.fetch=true checkout -b local_cfg fetch_upstream/fetch_cfg &&
860+
test_cmp_rev refs/remotes/fetch_upstream/fetch_cfg HEAD
861+
'
862+
863+
test_expect_success 'switch --fetch -c picks up branch created upstream after clone' '
864+
git checkout main &&
865+
git -C fetch_upstream checkout -b fetch_switch &&
866+
test_commit -C fetch_upstream u_switch &&
867+
test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_switch &&
868+
git switch --fetch -c local_switch fetch_upstream/fetch_switch &&
869+
test_cmp_rev refs/remotes/fetch_upstream/fetch_switch HEAD
870+
'
871+
804872
test_done

t/t9902-completion.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,6 +2602,7 @@ test_expect_success 'double dash "git checkout"' '
26022602
--ignore-other-worktrees Z
26032603
--recurse-submodules Z
26042604
--auto-advance Z
2605+
--fetch Z
26052606
--progress Z
26062607
--guess Z
26072608
--no-guess Z

0 commit comments

Comments
 (0)