Skip to content

Commit 45037f6

Browse files
committed
apply: preserve local (unstaged) changes
This change allows you to run `cu apply` continuously, by doing a somewhat delicate git dance: 1. `git diff` to save the unstaged changes to a .patch file 2. `git reset --hard` to get back to a pristine state 3. `git merge --squash` (no `--autostash`) to pull in the env changes 4. `git commit -m "temporary commit"` 5. `git apply` the patch from 1 (but this stages them) 6. `git reset` to move them back to unstaged 7. `git reset --soft HEAD~1` to move the temp commit into staging The spookiest part is probably 2, since that'll nuke any non-agent changes the user had staged. Maybe there's a safer way? Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
1 parent 6d5e9ab commit 45037f6

File tree

1 file changed

+107
-1
lines changed

1 file changed

+107
-1
lines changed

repository/repository.go

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"path/filepath"
1212
"sort"
1313
"strings"
14+
"time"
1415

1516
"dagger.io/dagger"
1617
"github.com/dagger/container-use/environment"
@@ -420,5 +421,110 @@ func (r *Repository) Apply(ctx context.Context, id string, w io.Writer) error {
420421
return err
421422
}
422423

423-
return RunInteractiveGitCommand(ctx, r.userRepoPath, w, "merge", "--autostash", "--squash", "--", "container-use/"+envInfo.ID)
424+
// Create patch directory if it doesn't exist
425+
configPath := os.ExpandEnv("$HOME/.config/container-use")
426+
patchDir := filepath.Join(configPath, "patches")
427+
if err := os.MkdirAll(patchDir, 0755); err != nil {
428+
return fmt.Errorf("failed to create patch directory: %w", err)
429+
}
430+
431+
// Create a unique patch filename using timestamp and environment ID
432+
patchFile := filepath.Join(patchDir, fmt.Sprintf("user-changes-%s-%d.patch", envInfo.ID, time.Now().Unix()))
433+
434+
// Check if there are any unstaged changes
435+
diffCmd := exec.CommandContext(ctx, "git", "diff")
436+
diffCmd.Dir = r.userRepoPath
437+
diffOutput, err := diffCmd.Output()
438+
if err != nil {
439+
return fmt.Errorf("failed to check for unstaged changes: %w", err)
440+
}
441+
442+
hasUnstagedChanges := len(diffOutput) > 0
443+
444+
if hasUnstagedChanges {
445+
// Create a patch of only unstaged changes
446+
fmt.Fprintf(w, "Saving unstaged user changes to %s...\n", patchFile)
447+
448+
// Create the patch from unstaged changes only
449+
patchCmd := exec.CommandContext(ctx, "git", "diff")
450+
patchCmd.Dir = r.userRepoPath
451+
patchOutput, err := patchCmd.Output()
452+
if err != nil {
453+
return fmt.Errorf("failed to create patch: %w", err)
454+
}
455+
456+
// Write patch to file
457+
if err := os.WriteFile(patchFile, patchOutput, 0644); err != nil {
458+
return fmt.Errorf("failed to write patch file: %w", err)
459+
}
460+
461+
// Reset to clean state
462+
fmt.Fprintf(w, "Resetting to clean state...\n")
463+
if err := RunInteractiveGitCommand(ctx, r.userRepoPath, w, "reset", "--hard", "HEAD"); err != nil {
464+
return fmt.Errorf("failed to reset: %w", err)
465+
}
466+
}
467+
468+
// Apply the merge without autostash
469+
fmt.Fprintf(w, "Applying environment changes...\n")
470+
if err := RunInteractiveGitCommand(ctx, r.userRepoPath, w, "merge", "--squash", "--", "container-use/"+envInfo.ID); err != nil {
471+
// If merge fails, try to restore user changes
472+
if hasUnstagedChanges {
473+
fmt.Fprintf(w, "Merge failed, restoring user changes...\n")
474+
applyCmd := exec.CommandContext(ctx, "git", "apply", patchFile)
475+
applyCmd.Dir = r.userRepoPath
476+
applyCmd.Stdout = w
477+
applyCmd.Stderr = w
478+
applyCmd.Run() // Ignore error as patch might partially apply
479+
}
480+
return fmt.Errorf("failed to merge: %w", err)
481+
}
482+
483+
// Apply user changes back
484+
if hasUnstagedChanges {
485+
fmt.Fprintf(w, "Restoring user changes...\n")
486+
487+
// 1. Temporarily commit the agent's changes
488+
commitCmd := exec.CommandContext(ctx, "git", "commit", "-m", "temp: agent changes")
489+
commitCmd.Dir = r.userRepoPath
490+
if err := commitCmd.Run(); err != nil {
491+
fmt.Fprintf(w, "Warning: Failed to commit agent changes: %v\n", err)
492+
return nil
493+
}
494+
495+
// 2. Apply the user's patch
496+
applyCmd := exec.CommandContext(ctx, "git", "apply", patchFile)
497+
applyCmd.Dir = r.userRepoPath
498+
applyCmd.Stdout = w
499+
applyCmd.Stderr = w
500+
if err := applyCmd.Run(); err != nil {
501+
fmt.Fprintf(w, "Warning: Failed to apply some user changes. Patch saved at: %s\n", patchFile)
502+
fmt.Fprintf(w, "You can manually apply it with: git apply %s\n", patchFile)
503+
// Try to recover by doing soft reset
504+
resetCmd := exec.CommandContext(ctx, "git", "reset", "--soft", "HEAD~1")
505+
resetCmd.Dir = r.userRepoPath
506+
resetCmd.Run()
507+
return nil
508+
}
509+
510+
// 3. Reset to unstage everything
511+
resetCmd := exec.CommandContext(ctx, "git", "reset")
512+
resetCmd.Dir = r.userRepoPath
513+
if err := resetCmd.Run(); err != nil {
514+
fmt.Fprintf(w, "Warning: Failed to reset: %v\n", err)
515+
}
516+
517+
// 4. Soft reset to bring agent changes back to staging
518+
softResetCmd := exec.CommandContext(ctx, "git", "reset", "--soft", "HEAD~1")
519+
softResetCmd.Dir = r.userRepoPath
520+
if err := softResetCmd.Run(); err != nil {
521+
fmt.Fprintf(w, "Warning: Failed to restore agent changes to staging: %v\n", err)
522+
}
523+
524+
// Clean up patch file on successful application
525+
os.Remove(patchFile)
526+
fmt.Fprintf(w, "User changes successfully restored as unstaged changes.\n")
527+
}
528+
529+
return nil
424530
}

0 commit comments

Comments
 (0)