From cb7ab1360088ac438f2ab529837923e5b0a7ed03 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:30:29 +0530 Subject: [PATCH] Fix: Auto-stash uncommitted changes on logs-only rewind --- cmd/entire/cli/git_operations.go | 21 +++++++++++++++++++++ cmd/entire/cli/rewind.go | 29 ++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/cmd/entire/cli/git_operations.go b/cmd/entire/cli/git_operations.go index cec82c1c1..2ff3eb919 100644 --- a/cmd/entire/cli/git_operations.go +++ b/cmd/entire/cli/git_operations.go @@ -229,6 +229,27 @@ func HasUncommittedChanges(ctx context.Context) (bool, error) { return len(strings.TrimSpace(string(output))) > 0, nil } +// StashIfDirty checks for uncommitted changes and stashes them if present. +// Returns true if a stash was created (changes were stashed), false if the +// working directory was clean. The caller should inform the user that they +// can recover stashed changes with `git stash pop`. +func StashIfDirty(ctx context.Context) (bool, error) { + dirty, err := HasUncommittedChanges(ctx) + if err != nil { + return false, fmt.Errorf("failed to check for uncommitted changes: %w", err) + } + if !dirty { + return false, nil + } + + cmd := exec.CommandContext(ctx, "git", "stash", "--include-untracked", "-m", "entire rewind: auto-stashed before checkout") + if output, err := cmd.CombinedOutput(); err != nil { + return false, fmt.Errorf("failed to stash changes: %s: %w", strings.TrimSpace(string(output)), err) + } + + return true, nil +} + // findNewUntrackedFiles finds files that are newly untracked (not in pre-existing list) func findNewUntrackedFiles(current, preExisting []string) []string { preExistingSet := make(map[string]bool) diff --git a/cmd/entire/cli/rewind.go b/cmd/entire/cli/rewind.go index fe05241b8..f1be41e50 100644 --- a/cmd/entire/cli/rewind.go +++ b/cmd/entire/cli/rewind.go @@ -548,6 +548,20 @@ func handleLogsOnlyRewindNonInteractive(ctx context.Context, start *strategy.Man slog.String("session_id", point.SessionID), ) + // Since we are forcing a full checkout in non-interactive mode if the user asks for it + // via CLI flags, we should auto-stash any dirty changes to prevent checkout failures. + stashed, err := StashIfDirty(ctx) + if err != nil { + logging.Error(logCtx, "logs-only rewind failed to stash", + slog.String("checkpoint_id", point.ID), + slog.String("error", err.Error()), + ) + return fmt.Errorf("failed to stash uncommitted changes: %w", err) + } + if stashed { + fmt.Println("Stashed uncommitted changes before rewind. Run 'git stash pop' to recover.") + } + sessions, err := start.RestoreLogsOnly(ctx, point, true) // force=true for explicit rewind if err != nil { logging.Error(logCtx, "logs-only rewind failed", @@ -882,7 +896,7 @@ func handleLogsOnlyCheckout(ctx context.Context, start *strategy.ManualCommitStr huh.NewGroup( huh.NewConfirm(). Title("Create detached HEAD?"). - Description("This will checkout the commit directly. You'll be in 'detached HEAD' state.\nAny uncommitted changes will be lost!"). + Description("This will checkout the commit directly. You'll be in 'detached HEAD' state.\nAny uncommitted changes will be stashed (recoverable)."). Value(&confirm), ), ) @@ -897,6 +911,19 @@ func handleLogsOnlyCheckout(ctx context.Context, start *strategy.ManualCommitStr return nil } + // Stash uncommitted changes before checkout to avoid git failure + stashed, err := StashIfDirty(ctx) + if err != nil { + logging.Error(logCtx, "logs-only checkout failed to stash", + slog.String("checkpoint_id", point.ID), + slog.String("error", err.Error()), + ) + return fmt.Errorf("failed to stash uncommitted changes: %w", err) + } + if stashed { + fmt.Println("Stashed uncommitted changes before checkout. Run 'git stash pop' to recover.") + } + // Perform git checkout if err := CheckoutBranch(ctx, point.ID); err != nil { logging.Error(logCtx, "logs-only checkout failed during git checkout",