Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cmd/entire/cli/git_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 28 additions & 1 deletion cmd/entire/cli/rewind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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),
),
)
Expand All @@ -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",
Expand Down