From 5d9adcb5dd30cb38c3d5fdf9259fe73cfb5001b1 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 23 Apr 2026 00:37:12 -0700 Subject: [PATCH] fix(finish): always clear merge state even when git abort reports no-op MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleAbort called git merge/rebase --abort first and bailed early on error, so a common real-world sequence — conflict, manual resolve+ commit (bypassing --continue), then --abort — left merge.json on disk after 'merge --abort' returned exit 128 ('MERGE_HEAD missing'). Every subsequent 'feature finish' was permanently blocked until the user deleted .git/gitflow/state/merge.json by hand. Treat the abort error as informational, continue cleanup, and still call mergestate.ClearMergeState so the repo is unblocked. A warning is printed to stderr when git had nothing to abort so operators can see it. The checkout-original-branch step only fails the whole operation when abort itself succeeded (the previously exercised happy path). Refs gittower/git-flow-next issue 110. Signed-off-by: SAY-5 --- cmd/finish.go | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/cmd/finish.go b/cmd/finish.go index c2822ab..89a1a3d 100644 --- a/cmd/finish.go +++ b/cmd/finish.go @@ -556,7 +556,7 @@ func handleContinue(cfg *config.Config, state *mergestate.MergeState, branchConf } func handleAbort(state *mergestate.MergeState) error { - // Abort the merge based on strategy + // Abort the merge based on strategy. var err error switch state.MergeStrategy { case strategyMerge: @@ -567,20 +567,36 @@ func handleAbort(state *mergestate.MergeState) error { err = git.MergeAbort() // Default to merge abort } - if err != nil { - return &errors.GitError{Operation: "abort merge", Err: err} - } - - // Checkout the original branch - if err := git.Checkout(state.FullBranchName); err != nil { - return &errors.GitError{Operation: fmt.Sprintf("checkout original branch '%s'", state.FullBranchName), Err: err} + // Git returns exit 128 from `merge --abort` when there's no MERGE_HEAD + // (user resolved the conflict manually with a commit, bypassing + // --continue). The stale merge.json state file then blocked every + // future `feature finish` call. Treat abort failures as warnings and + // continue cleaning up so ClearMergeState is always reached. See + // gittower/git-flow-next#110. + abortErr := err + + // Checkout the original branch. This may also no-op / fail when + // already on that branch; propagate any actual error only if we + // can't also clear state below. + if checkoutErr := git.Checkout(state.FullBranchName); checkoutErr != nil && abortErr == nil { + // If the abort itself succeeded but the checkout failed, that's + // a genuine problem the caller should see. + return &errors.GitError{Operation: fmt.Sprintf("checkout original branch '%s'", state.FullBranchName), Err: checkoutErr} } - // Clear the merge state + // Always clear the merge state so the repository is unblocked for + // the next feature finish, regardless of whether the underlying + // git abort believed there was anything to abort. if err := mergestate.ClearMergeState(); err != nil { return &errors.GitError{Operation: "clear merge state", Err: err} } + if abortErr != nil { + // Surface the git abort failure for visibility but don't return + // an error — state has been cleared and the user is unblocked. + fmt.Fprintf(os.Stderr, "git-flow: git abort reported no active merge/rebase; cleared stale merge state anyway (%v)\n", abortErr) + } + return nil }