diff --git a/.changes/unreleased/Fixed-20240729-210429.yaml b/.changes/unreleased/Fixed-20240729-210429.yaml new file mode 100644 index 00000000..177c75df --- /dev/null +++ b/.changes/unreleased/Fixed-20240729-210429.yaml @@ -0,0 +1,3 @@ +kind: Fixed +body: 'branch create: Don''t lose data if the branch cannot be created for any reason.' +time: 2024-07-29T21:04:29.049874-07:00 diff --git a/branch_create.go b/branch_create.go index cdcf4a15..cc3a05b3 100644 --- a/branch_create.go +++ b/branch_create.go @@ -145,12 +145,39 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, opts *glob if err := repo.DetachHead(ctx, baseName); err != nil { return fmt.Errorf("detach head: %w", err) } - // From this point on, if there's an error, - // restore the original branch. + + // From this point on, to prevent data loss, + // we'll revert to original branch while keeping the changes + // if we failed to successfully create the new branch. + // + // The condition for this is not whether an error is returned, + // and whether the new branch was successfully created. + var ( + branchCreated bool // whether the new branch was created + commitHash git.Hash // hash of the commit (if created) + ) defer func() { - if err != nil { - err = errors.Join(err, repo.Checkout(ctx, cmd.Target)) + if branchCreated { + return + } + + log.Warn("Unable to create branch. Rolling back.", + "branch", cmd.Target) + + // Move HEAD to the state just before the commit + // while leaving the index and working tree as-is. + resetErr := repo.Reset(ctx, commitHash.String()+"^", git.ResetOptions{ + Mode: git.ResetSoft, + Quiet: true, + }) + if resetErr != nil { + log.Warn("Could not reset to parent commit.", + "commit", commitHash, + "error", resetErr) } + + err = errors.Join(err, + repo.Checkout(ctx, cmd.Target)) }() if err := repo.Commit(ctx, git.CommitRequest{ @@ -161,6 +188,11 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, opts *glob return fmt.Errorf("commit: %w", err) } + commitHash, err = repo.Head(ctx) + if err != nil { + return fmt.Errorf("get commit hash: %w", err) + } + if cmd.Name == "" { // Branch name was not specified. // Generate one from the commit message. @@ -190,6 +222,7 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, opts *glob return fmt.Errorf("create branch: %w", err) } + branchCreated = true if err := repo.Checkout(ctx, cmd.Name); err != nil { return fmt.Errorf("checkout branch: %w", err) } diff --git a/testdata/script/issue307_branch_create_cannot_create.txt b/testdata/script/issue307_branch_create_cannot_create.txt new file mode 100644 index 00000000..5955fcdb --- /dev/null +++ b/testdata/script/issue307_branch_create_cannot_create.txt @@ -0,0 +1,34 @@ +# 'branch create' does not lose files if a branch cannot be created. +# https://github.com/abhinav/git-spice/issues/307 + +as 'Test ' +at '2024-03-30T14:59:32Z' + +cd repo +git init +git commit --allow-empty -m 'Initial commit' +gs repo init + +git checkout -b feature +git add feature1.txt +git commit -m 'Add feature1' + +git checkout main +git add feature2.txt + +# will be unable to create the branch because +# 'feature' is a file so a directory cannot be created there. +! gs branch create feature/2 -m 'Add feature2' + +# should not lose the file. +exists feature2.txt + +git status --porcelain +cmp stdout $WORK/golden/status.txt + +-- repo/feature1.txt -- +feature 1 +-- repo/feature2.txt -- +feature 2 +-- golden/status.txt -- +A feature2.txt