feat(veans): add list command with filters and JSON output
This commit is contained in:
parent
081373bb48
commit
5e80c17281
|
|
@ -0,0 +1,181 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"code.vikunja.io/veans/internal/client"
|
||||
"code.vikunja.io/veans/internal/config"
|
||||
"code.vikunja.io/veans/internal/output"
|
||||
"code.vikunja.io/veans/internal/status"
|
||||
)
|
||||
|
||||
type listFlags struct {
|
||||
ready bool
|
||||
mine bool
|
||||
branch string
|
||||
branchAuto bool
|
||||
filter string
|
||||
statuses []string
|
||||
}
|
||||
|
||||
func newListCmd() *cobra.Command {
|
||||
f := &listFlags{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List tasks in the configured project",
|
||||
Long: `List tasks in the project configured in .veans.yml.
|
||||
|
||||
Filters can be combined; they're AND-ed together:
|
||||
--ready ready to start: in Todo with done=false (incomplete-blocker
|
||||
detection is best-effort, see veans/README.md)
|
||||
--mine only tasks assigned to the veans bot
|
||||
--branch [name] only tasks tagged 'veans:branch:<name>' (defaults to the
|
||||
current git branch when used without a value)
|
||||
--filter <expr> raw Vikunja filter expression (see Vikunja docs); applied
|
||||
server-side
|
||||
--status <s> filter by status (todo|in-progress|in-review|completed|scrapped),
|
||||
may be repeated`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
rt, err := loadRuntime()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tasks, err := runList(cmd, rt, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if globals.JSON {
|
||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(tasks)
|
||||
}
|
||||
renderTasksHuman(cmd.OutOrStdout(), tasks, rt.cfg)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&f.ready, "ready", false, "only ready-to-start tasks (Todo bucket, not done)")
|
||||
cmd.Flags().BoolVar(&f.mine, "mine", false, "only tasks assigned to the veans bot")
|
||||
cmd.Flags().StringVar(&f.branch, "branch", "", "only tasks tagged 'veans:branch:<name>' (omit value for current branch)")
|
||||
cmd.Flags().Lookup("branch").NoOptDefVal = "__auto__"
|
||||
cmd.Flags().StringVar(&f.filter, "filter", "", "raw Vikunja filter expression, applied server-side")
|
||||
cmd.Flags().StringSliceVar(&f.statuses, "status", nil, "filter by status (repeatable)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(cmd *cobra.Command, rt *runtime, f *listFlags) ([]*client.Task, error) {
|
||||
opts := &client.TaskListOptions{
|
||||
Filter: f.filter,
|
||||
Expand: []string{"reactions"},
|
||||
}
|
||||
tasks, err := rt.client.ListProjectTasks(cmd.Context(), rt.cfg.ProjectID, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply client-side filters AND-style.
|
||||
var out []*client.Task
|
||||
for _, t := range tasks {
|
||||
if f.ready {
|
||||
if t.Done || t.BucketID != rt.cfg.Buckets.Todo {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if f.mine {
|
||||
if !taskAssignedTo(t, rt.cfg.Bot.UserID) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if f.branch != "" {
|
||||
want := f.branch
|
||||
if want == "__auto__" {
|
||||
want = currentGitBranch()
|
||||
if want == "" {
|
||||
return nil, output.New(output.CodeValidation,
|
||||
"--branch given without a value but no current git branch detected")
|
||||
}
|
||||
}
|
||||
label := branchLabel(want)
|
||||
if !taskHasLabel(t, label) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(f.statuses) > 0 {
|
||||
ok := false
|
||||
for _, raw := range f.statuses {
|
||||
s, err := status.Parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wantBucket, _ := status.BucketID(s, rt.cfg.Buckets)
|
||||
if t.BucketID == wantBucket {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func taskAssignedTo(t *client.Task, userID int64) bool {
|
||||
for _, a := range t.Assignees {
|
||||
if a != nil && a.ID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func taskHasLabel(t *client.Task, title string) bool {
|
||||
for _, l := range t.Labels {
|
||||
if l != nil && l.Title == title {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func renderTasksHuman(w fmtWriter, tasks []*client.Task, cfg *config.Config) {
|
||||
if len(tasks) == 0 {
|
||||
fmt.Fprintln(w, "(no tasks)")
|
||||
return
|
||||
}
|
||||
for _, t := range tasks {
|
||||
s := status.FromBucketID(t.BucketID, cfg.Buckets)
|
||||
stamp := string(s)
|
||||
if stamp == "" {
|
||||
stamp = "-"
|
||||
}
|
||||
fmt.Fprintf(w, "%-12s %-10s %s\n",
|
||||
cfg.FormatTaskID(t.Index),
|
||||
stamp,
|
||||
t.Title,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func branchLabel(branch string) string {
|
||||
return "veans:branch:" + branch
|
||||
}
|
||||
|
||||
// currentGitBranch returns the current git branch as reported by
|
||||
// `git rev-parse --abbrev-ref HEAD`, or "" if we're not in a git repo or
|
||||
// HEAD is detached. Failures are silent so callers can decide.
|
||||
func currentGitBranch() string {
|
||||
out, err := runGit("rev-parse", "--abbrev-ref", "HEAD")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
out = strings.TrimSpace(out)
|
||||
if out == "HEAD" {
|
||||
return ""
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ func Root(version string) *cobra.Command {
|
|||
|
||||
root.AddCommand(newVersionCmd(version))
|
||||
root.AddCommand(newInitCmd())
|
||||
root.AddCommand(newListCmd())
|
||||
|
||||
return root
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue