From 07d1a78dfc8f0d3c07d69d7a9fdd184ef061bbb6 Mon Sep 17 00:00:00 2001 From: Joakim Date: Thu, 19 Feb 2026 15:22:51 +0100 Subject: [PATCH] feat: add uncomplete command to restore completed tasks to pending Dedicated command that sets status back to pending and clears End time. Unlike undo, works on any completed task regardless of when it was completed. Co-Authored-By: Claude Opus 4.6 --- opal-task/cmd/root.go | 4 +- opal-task/cmd/uncomplete.go | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 opal-task/cmd/uncomplete.go diff --git a/opal-task/cmd/root.go b/opal-task/cmd/root.go index 1818122..00bf4cd 100644 --- a/opal-task/cmd/root.go +++ b/opal-task/cmd/root.go @@ -34,7 +34,7 @@ var commandNames = []string{ "add", "done", "modify", "delete", "start", "stop", "count", "projects", "tags", "info", "edit", "server", "sync", "reports", "setup", - "version", "annotate", "denotate", "undo", "log", "completion", + "version", "annotate", "denotate", "undo", "uncomplete", "log", "completion", } // Report names (dynamically populated) @@ -241,6 +241,7 @@ func init() { annotateCmd.GroupID = "task" denotateCmd.GroupID = "task" undoCmd.GroupID = "task" + uncompleteCmd.GroupID = "task" logCmd.GroupID = "task" rootCmd.AddCommand(addCmd) @@ -254,6 +255,7 @@ func init() { rootCmd.AddCommand(annotateCmd) rootCmd.AddCommand(denotateCmd) rootCmd.AddCommand(undoCmd) + rootCmd.AddCommand(uncompleteCmd) rootCmd.AddCommand(logCmd) // Other commands diff --git a/opal-task/cmd/uncomplete.go b/opal-task/cmd/uncomplete.go new file mode 100644 index 0000000..4fc1ae6 --- /dev/null +++ b/opal-task/cmd/uncomplete.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + "os" + + "git.jnss.me/joakim/opal/internal/engine" + "github.com/spf13/cobra" +) + +var uncompleteCmd = &cobra.Command{ + Use: "uncomplete [filter...]", + Short: "Restore a completed task to pending", + Long: `Restore a completed task back to pending status. + +Unlike undo, this is a targeted action that works on any completed task +regardless of when it was completed. + +Examples: + opal 2 uncomplete + opal uncomplete +errand`, + Run: func(cmd *cobra.Command, args []string) { + parsed := getParsedArgs(cmd) + if err := uncompleteTasks(parsed.Filters); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + }, +} + +func uncompleteTasks(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no task specified") + } + + filter, err := engine.ParseFilter(args) + if err != nil { + return fmt.Errorf("failed to parse filter: %w", err) + } + + ws, err := engine.LoadWorkingSet() + if err != nil { + return fmt.Errorf("failed to load working set: %w", err) + } + + var tasks []*engine.Task + + if len(filter.IDs) > 0 { + for _, id := range filter.IDs { + task, err := ws.GetTaskByDisplayID(id) + if err != nil { + return err + } + tasks = append(tasks, task) + } + } else { + tasks, err = engine.GetTasks(filter) + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + } + + if len(tasks) == 0 { + return fmt.Errorf("no tasks matched filter") + } + + uncompleted := 0 + for _, task := range tasks { + if task.Status != engine.StatusCompleted { + fmt.Fprintf(os.Stderr, "Warning: task %s is not completed, skipping\n", task.UUID) + continue + } + task.Status = engine.StatusPending + task.End = nil + if err := task.Save(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to uncomplete task %s: %v\n", task.UUID, err) + } else { + uncompleted++ + } + } + + if uncompleted == 1 { + fmt.Printf("Restored task %s to pending\n", engine.FormatTaskSummary(tasks[0], ws)) + } else { + fmt.Printf("Restored %d task(s) to pending.\n", uncompleted) + } + return nil +}