checkout: finish switch when tracking config cannot be written
cover checkout: finish switch when tracking config cannot be written
When the tracking config cannot be written (for example, because .git/config.lock exists), git switch -c and checkout -b stop halfway. The worktree, the index and the new branch ref are already updated, but HEAD stays on the old branch, and the changes are left staged.
* 1/2 makes setup_tracking() return an error instead of calling exit(). No behavior change yet.
* 2/2 makes switch and checkout finish the switch and exit with status 0, as push -u, clone and fetch already do. git branch still fails.
These patches were first sent as part of v2 of a series that also fixed the hint
(https://lore.kernel.org/git/[email protected]/).
Junio asked whether they can be treated separately from the hint fix
(https://lore.kernel.org/git/[email protected]/), and I replied that they can
(https://lore.kernel.org/git/CAP8tz2KOXHLjc5V1o1tZZWSsgOjVrp-ekH7H5KszBGK+hRqwrg@mail.gmail.com/).
The direction was agreed in the bug report thread
(https://lore.kernel.org/git/[email protected]/).
Making the switch fully atomic, so that a failure undoes the worktree, the index, the ref and the config, is left for a separate topic.
tsukasa inoue (2):
branch: return an error from setup_tracking()
checkout: finish switching when the tracking config cannot be written
branch.c | 35 +++++++++++++++++++++-------------- branch.h | 21 +++++++++++++-------- builtin/branch.c | 17 ++++++++++++----- builtin/checkout.c | 6 ++++++ t/t2027-checkout-track.sh | 20 ++++++++++++++++++++ t/t2060-switch.sh | 20 ++++++++++++++++++++ t/t3200-branch.sh | 13 +++++++++++++ 7 files changed, 105 insertions(+), 27 deletions(-)
base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2275%2FtsukasaI%2Ffix%2Fcheckout-tracking-config-failure-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2275/tsukasaI/fix/checkout-tracking-config-failure-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2275
Signature · 1 line
gitgitgadget
PATCH 1/2 branch: return an error from setup_tracking()
From: tsukasa inoue <[email protected]>
setup_tracking() calls exit(1) when it cannot write the tracking configuration. This exit was introduced in 27852b2c53 (branch: report errors in tracking branch setup, 2016-02-22). Because of it, callers cannot continue after a tracking failure, even though the ref has already been created.
Change setup_tracking() to return an error instead of exiting, and make create_branch() and dwim_and_setup_tracking() return that value too.
This does not change behavior. Callers still exit with status 1 on failure, as before. Changing checkout
--- branch.c | 35 +++++++++++++++++++++-------------- branch.h | 21 +++++++++++++-------- builtin/branch.c | 17 ++++++++++++----- builtin/checkout.c | 17 +++++++++-------- 4 files changed, 55 insertions(+), 35 deletions(-)
branch.c +21 -14
@@ -249,12 +249,13 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref)* will not be expanded to "refs/remotes/origin/main", so it is not safe * for 'orig_ref' to be raw user input. */ -static void setup_tracking (const char *new_ref,const char *orig_ref, -enum branch_track track,int quiet) +static int setup_tracking (const char *new_ref,const char *orig_ref, +enum branch_track track,int quiet) {struct tracking tracking;struct string_list tracking_srcs =STRING_LIST_INIT_DUP ;int config_flags = quiet ?0 :BRANCH_CONFIG_VERBOSE ; +int ret =0 ;struct find_tracked_branch_cb ftb_cb = { .tracking = &tracking, .ambiguous_remotes =STRING_LIST_INIT_DUP ,
@@ -343,11 +344,12 @@ static void setup_tracking(const char *new_ref, const char *orig_ref,string_list_append (tracking.srcs, orig_ref);if (install_branch_config_multiple_remotes (config_flags, new_ref, tracking.remote, tracking.srcs) <0 ) -exit (1 ); + ret =-1 ; cleanup:string_list_clear (&tracking_srcs,0 );string_list_clear (&ftb_cb.ambiguous_remotes,0 ); +return ret; }int read_branch_desc (struct strbuf *buf,const char *branch_name)
@@ -627,10 +629,10 @@ static void dwim_branch_start(struct repository *r, const char *start_name,FREE_AND_NULL (real_ref); } -void create_branch (struct repository *r, -const char *name,const char *start_name, -int force,int clobber_head_ok,int reflog, -int quiet,enum branch_track track,int dry_run) +int create_branch (struct repository *r, +const char *name,const char *start_name, +int force,int clobber_head_ok,int reflog, +int quiet,enum branch_track track,int dry_run) {struct object_id oid;char *real_ref;
@@ -639,6 +641,7 @@ void create_branch(struct repository *r,struct ref_transaction *transaction;struct strbuf err =STRBUF_INIT ;int flags =0 ; +int ret =0 ;char *msg;if (track ==BRANCH_TRACK_OVERRIDE )
@@ -676,21 +679,25 @@ void create_branch(struct repository *r,free (msg);if (real_ref && track) -setup_tracking (ref.buf +11 , real_ref, track, quiet); + ret =setup_tracking (ref.buf +11 , real_ref, track, quiet); cleanup:strbuf_release (&ref);free (real_ref); +return ret; } -void dwim_and_setup_tracking (struct repository *r,const char *new_ref, -const char *orig_ref,enum branch_track track, -int quiet) +int dwim_and_setup_tracking (struct repository *r,const char *new_ref, +const char *orig_ref,enum branch_track track, +int quiet) {char *real_orig_ref =NULL ; +int ret; +dwim_branch_start (r, orig_ref, track, &real_orig_ref,NULL ); -setup_tracking (new_ref, real_orig_ref, track, quiet); + ret =setup_tracking (new_ref, real_orig_ref, track, quiet);free (real_orig_ref); +return ret; }/**
@@ -840,8 +847,8 @@ void create_branches_recursively(struct repository *r, const char *name,* tedious to determine whether or not tracking was set up in the * superproject. */ -if (track) -setup_tracking (name, tracking_name, track, quiet); +if (track &&setup_tracking (name, tracking_name, track, quiet)) +exit (1 );for (i =0 ; i < submodule_entry_list.entry_nr; i++) { if (submodule_create_branch(
branch.h +13 -8
@@ -34,10 +34,13 @@ enum branch_track {* causes new_ref to track whatever orig_ref tracks. * * - quiet suppresses tracking information. +* +* Returns 0 on success, or -1 if the tracking configuration could not be +* written. */ -void dwim_and_setup_tracking (struct repository *r,const char *new_ref, -const char *orig_ref,enum branch_track track, -int quiet); +int dwim_and_setup_tracking (struct repository *r,const char *new_ref, +const char *orig_ref,enum branch_track track, +int quiet);/* * Creates a new branch, where:
@@ -63,12 +66,14 @@ void dwim_and_setup_tracking(struct repository *r, const char *new_ref,* * - dry_run causes the branch to be validated but not created. * +* Returns 0 on success, or -1 if the tracking configuration could not be +* written. */ -void create_branch (struct repository *r, -const char *name,const char *start_name, -int force,int clobber_head_ok, -int reflog,int quiet,enum branch_track track, -int dry_run); +int create_branch (struct repository *r, +const char *name,const char *start_name, +int force,int clobber_head_ok, +int reflog,int quiet,enum branch_track track, +int dry_run);/* * Creates a new branch in a repository and its submodules (and its
builtin/branch.c +12 -5
@@ -1253,9 +1253,13 @@ int cmd_branch(int argc,die (_ ("branch '%s' does not exist" ), branch->name); } -dwim_and_setup_tracking (the_repository, branch->name, - new_upstream,BRANCH_TRACK_OVERRIDE , - quiet); +if (dwim_and_setup_tracking (the_repository, branch->name, + new_upstream,BRANCH_TRACK_OVERRIDE , + quiet)) { +strbuf_release (&buf); + ret =1 ; + goto out; + }strbuf_release (&buf); }else if (unset_upstream) {struct branch *branch;
@@ -1305,8 +1309,11 @@ int cmd_branch(int argc, ret =0 ; goto out; } -create_branch (the_repository, branch_name, start_name, force,0 , - reflog, quiet, track,0 ); +if (create_branch (the_repository, branch_name, start_name, + force,0 , reflog, quiet, track,0 )) { + ret =1 ; + goto out; + } }else usage_with_options (builtin_branch_usage, options);
builtin/checkout.c +9 -8
@@ -979,14 +979,15 @@ static void update_refs_for_switch(const struct checkout_opts *opts,free (refname); }else -create_branch (the_repository, - opts->new_branch, new_branch_info->name, - opts->new_branch_force ?1 :0 , - opts->new_branch_force ?1 :0 , - opts->new_branch_log, - opts->quiet, - opts->track, -0 ); +if (create_branch (the_repository, + opts->new_branch, new_branch_info->name, + opts->new_branch_force ?1 :0 , + opts->new_branch_force ?1 :0 , + opts->new_branch_log, + opts->quiet, + opts->track, +0 )) +exit (1 );free (new_branch_info->name);free (new_branch_info->refname); new_branch_info->name =xstrdup (opts->new_branch);
Signature · 1 line
gitgitgadget
PATCH 2/2 checkout: finish switching when the tracking config cannot be written
In reply to tsukasa-ino via GitGitGadgetFrom: tsukasa inoue <[email protected]>
When git switch -c <new> <remote>/<branch> (or checkout -b) cannot write the tracking config, for example because .git/config.lock exists, the worktree, the index and the new branch ref have already been updated. Git then exits with status 1 while HEAD still points to the original branch. The changes of the switch remain staged, and the post-checkout hook does not run.
create_branch() now returns an error instead of exiting. Make checkout ignore that error so that the switch completes and the command exits with status 0. The error and the hint are still printed.
Match push -u, clone and fetch, which also print the error and continue. The upstream can be set later with the command shown in the hint.
Keep git branch <new> <start> failing when the tracking config cannot be written, so its behavior does not change; t3200 covers this. Making the switch fully atomic, undoing the worktree, the index, the ref and the config when the tracking config fails, is a separate topic.
--- builtin/checkout.c | 23 ++++++++++++++--------- t/t2027-checkout-track.sh | 20 ++++++++++++++++++++ t/t2060-switch.sh | 20 ++++++++++++++++++++ t/t3200-branch.sh | 13 +++++++++++++ 4 files changed, 67 insertions(+), 9 deletions(-)
builtin/checkout.c +14 -9
@@ -979,15 +979,20 @@ static void update_refs_for_switch(const struct checkout_opts *opts,free (refname); }else -if (create_branch (the_repository, - opts->new_branch, new_branch_info->name, - opts->new_branch_force ?1 :0 , - opts->new_branch_force ?1 :0 , - opts->new_branch_log, - opts->quiet, - opts->track, -0 )) -exit (1 ); +/* +* Not fatal: the error and hint have already been +* shown, and like "push -u" and "clone" we finish +* the switch instead of leaving HEAD on the old +* branch with the worktree already updated. +*/ +create_branch (the_repository, + opts->new_branch, new_branch_info->name, + opts->new_branch_force ?1 :0 , + opts->new_branch_force ?1 :0 , + opts->new_branch_log, + opts->quiet, + opts->track, +0 );free (new_branch_info->name);free (new_branch_info->refname); new_branch_info->name =xstrdup (opts->new_branch);
t/t2027-checkout-track.sh +20 -0
@@ -47,6 +47,26 @@ test_expect_success 'checkout --track -b overrides autoSetupMerge=inherit' ' test_cmp_config refs/heads/main branch.b4.merge ' +test_expect_success 'checkout -b completes when tracking config cannot be written' ' + test_when_finished "git update-ref -d refs/remotes/lockremote/lockbranch" && + test_when_finished "git branch -D lock-new" && + test_when_finished "git checkout main" && + test_config remote.lockremote.url . && + test_config remote.lockremote.fetch "+refs/heads/*:refs/remotes/lockremote/*" && + git update-ref refs/remotes/lockremote/lockbranch one && + test_when_finished "rm -f .git/config.lock" && + >.git/config.lock && + git -c core.configLockTimeout=0 \ + checkout -b lock-new lockremote/lockbranch 2>err && + test_grep "unable to write upstream branch configuration" err && + echo refs/heads/lock-new >expect && + git symbolic-ref HEAD >actual && + test_cmp expect actual && + git diff --cached --exit-code && + rm -f .git/config.lock && + test_must_fail git config branch.lock-new.remote +' + test_expect_success 'ambiguous tracking info' ' # Set up a few remote repositories git init --bare --initial-branch=trunk src1 &&
t/t2060-switch.sh +20 -0
@@ -146,6 +146,26 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' ' test_cmp_config "" --default "" branch.main2.merge ' +test_expect_success 'switch -c completes when tracking config cannot be written' ' + test_when_finished "git update-ref -d refs/remotes/lockremote/lockbranch" && + test_when_finished "git branch -D lock-new" && + test_when_finished "git switch main" && + test_config remote.lockremote.url . && + test_config remote.lockremote.fetch "+refs/heads/*:refs/remotes/lockremote/*" && + git update-ref refs/remotes/lockremote/lockbranch first-branch && + test_when_finished "rm -f .git/config.lock" && + >.git/config.lock && + git -c core.configLockTimeout=0 \ + switch -c lock-new lockremote/lockbranch 2>err && + test_grep "unable to write upstream branch configuration" err && + echo refs/heads/lock-new >expect && + git symbolic-ref HEAD >actual && + test_cmp expect actual && + git diff --cached --exit-code && + rm -f .git/config.lock && + test_must_fail git config branch.lock-new.remote +' + test_expect_success 'switch back when temporarily detached and checked out elsewhere ' ' test_when_finished " git worktree remove wt1 ||:
t/t3200-branch.sh +13 -0
@@ -1080,6 +1080,19 @@ test_expect_success '--set-upstream-to fails on locked config' ' test_grep "could not lock config file .git/config" err ' +test_expect_success 'branch with start point fails when tracking config cannot be written' ' + test_when_finished "git branch -D lock-new || :" && + test_when_finished "git update-ref -d refs/remotes/lockremote/lockbranch" && + test_config remote.lockremote.url . && + test_config remote.lockremote.fetch "+refs/heads/*:refs/remotes/lockremote/*" && + git update-ref refs/remotes/lockremote/lockbranch HEAD && + test_when_finished "rm -f .git/config.lock" && + >.git/config.lock && + test_must_fail git -c core.configLockTimeout=0 \ + branch lock-new lockremote/lockbranch 2>err && + test_grep "unable to write upstream branch configuration" err +' + test_expect_success 'use --set-upstream-to modify HEAD' ' test_config branch.main.remote foo && test_config branch.main.merge foo &&
Signature · 1 line
gitgitgadget
Quoted 9 lines · tsukasa-ino via GitGitGadget
When the tracking config cannot be written (for example, because .git/config.lock exists), git switch -c and checkout -b stop halfway. The worktree, the index and the new branch ref are already updated, but HEAD stays on the old branch, and the changes are left staged.
* 1/2 makes
setup_tracking()return an error instead of callingexit(). No behavior change yet.
* 2/2 makes switch and checkout finish the switch and exit with status 0, as push -u, clone and fetch already do. git branch still fails.
Thanks, queued.