From e763659b60e838f0ffbd772926213630479840ce Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Fri, 13 Mar 2026 17:03:10 +0100 Subject: [PATCH] status filter --- domain/app.go | 42 ++++++++++++++++++++++++++++++---- domain/config.go | 6 +++-- domain/model/issue.go | 53 +++++++++++++++++++++++++++++++++++++++++++ domain/ui.go | 11 +++++---- 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/domain/app.go b/domain/app.go index 304d28b..fe611ed 100644 --- a/domain/app.go +++ b/domain/app.go @@ -1,7 +1,9 @@ package domain import ( + "flag" "os" + "strings" "github.com/joho/godotenv" @@ -11,6 +13,14 @@ import ( func Run() { ui := NewUI() // Instantiate UI + // Parse flags + includeStatusFlag := flag.String("status", "", "Filter issues by status (comma-separated names or IDs)") + excludeStatusFlag := flag.String("exclude-status", "", "Exclude issues by status (comma-separated names or IDs)") + flag.Usage = func() { + ui.PrintUsage(os.Args[0]) + } + flag.Parse() + // Load .env file err := godotenv.Load() if err != nil && !os.IsNotExist(err) { @@ -27,9 +37,22 @@ func Run() { apiKey := "" // Try to get values from config file first + var includeStatuses []string + var excludeStatuses []string + if config != nil { baseURL = config.Host apiKey = config.Token + includeStatuses = config.IncludeStatuses + excludeStatuses = config.ExcludeStatuses + } + + // Flag overrides config file + if *includeStatusFlag != "" { + includeStatuses = strings.Split(*includeStatusFlag, ",") + } + if *excludeStatusFlag != "" { + excludeStatuses = strings.Split(*excludeStatusFlag, ",") } // Environment variables override config file @@ -43,8 +66,14 @@ func Run() { var projectID string // projectID must be provided as a command-line argument, or list projects - if len(os.Args) > 1 { - projectID = os.Args[1] + if flag.NArg() > 0 { + projectID = flag.Arg(0) + // Check for misplaced flags + for i := 1; i < flag.NArg(); i++ { + if strings.HasPrefix(flag.Arg(i), "-") { + ui.PrintError("Error: flags must come BEFORE the project ID. Found misplaced flag: %s\n", flag.Arg(i)) + } + } } else { projects, err := model.FetchAllProjects(baseURL, apiKey) if err != nil { @@ -70,12 +99,15 @@ func Run() { ui.PrintError("Error fetching issues: %v\n", err) } - ui.PrintTotalIssuesFetched(len(issues)) + // Apply filtering + filteredIssues := model.FilterIssues(issues, includeStatuses, excludeStatuses) - roots := model.BuildTree(issues) // Corrected call + ui.PrintTotalIssuesFetched(len(filteredIssues)) + + roots := model.BuildTree(filteredIssues) // Corrected call ui.PrintIssueTreeHeader(len(roots)) model.PrintTree(roots, "", false) // Corrected call // Print summary - ui.PrintSummary(len(issues), len(roots)) + ui.PrintSummary(len(filteredIssues), len(roots)) } diff --git a/domain/config.go b/domain/config.go index b0b2a0f..ef8e7a3 100644 --- a/domain/config.go +++ b/domain/config.go @@ -8,8 +8,10 @@ import ( // Config struct for ~/.redmine-tree.json type Config struct { - Host string `json:"host"` - Token string `json:"token"` + Host string `json:"host"` + Token string `json:"token"` + IncludeStatuses []string `json:"include_statuses,omitempty"` + ExcludeStatuses []string `json:"exclude_statuses,omitempty"` } // Load configuration from ~/.redmine-tree.json diff --git a/domain/model/issue.go b/domain/model/issue.go index 6412618..8671e79 100644 --- a/domain/model/issue.go +++ b/domain/model/issue.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" ) // --- Redmine API structs --- @@ -28,6 +29,58 @@ type IssuesResponse struct { Limit int `json:"limit"` } +// FilterIssues filters issues based on included and excluded status names or IDs. +func FilterIssues(issues []Issue, includeStatuses, excludeStatuses []string) []Issue { + if len(includeStatuses) == 0 && len(excludeStatuses) == 0 { + return issues + } + + var filtered []Issue + for _, issue := range issues { + statusID := fmt.Sprintf("%d", issue.Status.ID) + statusName := strings.ToLower(issue.Status.Title) + + keep := true + + // If includeStatuses is specified, issue MUST match at least one + if len(includeStatuses) > 0 { + match := false + for _, s := range includeStatuses { + s = strings.TrimSpace(strings.ToLower(s)) + if s == "" { + continue + } + if s == statusID || s == statusName { + match = true + break + } + } + if !match { + keep = false + } + } + + // If excludeStatuses is specified, issue MUST NOT match any + if keep && len(excludeStatuses) > 0 { + for _, s := range excludeStatuses { + s = strings.TrimSpace(strings.ToLower(s)) + if s == "" { + continue + } + if s == statusID || s == statusName { + keep = false + break + } + } + } + + if keep { + filtered = append(filtered, issue) + } + } + return filtered +} + // Fetch all issues with pagination func FetchAllIssues(baseURL, apiKey, projectID string) ([]Issue, error) { var all []Issue diff --git a/domain/ui.go b/domain/ui.go index 541d3a2..48d47f5 100644 --- a/domain/ui.go +++ b/domain/ui.go @@ -48,10 +48,13 @@ func (ui *UI) PrintProjects(projects []model.Project) { // PrintUsage prints the application usage message to stderr and exits. func (ui *UI) PrintUsage(appName string) { - ui.Printf("Usage: %s \n", appName) - ui.Printf(" REDMINE_URL and REDMINE_TOKEN must be set in .env, as environment variables, or in ~/.redmine-tree.json.\n") - ui.Printf("Example: REDMINE_URL=https://redmine.example.com REDMINE_TOKEN=abc123 %s my-project\n", appName) - ui.Printf("Or: %s my-project (if REDMINE_URL, REDMINE_TOKEN are in .env or ~/.redmine-tree.json)\n", appName) + ui.Printf("Usage: %s [options] \n", appName) + ui.Printf("Options:\n") + ui.Printf(" -status Include only issues with these statuses (comma-separated names or IDs)\n") + ui.Printf(" -exclude-status Exclude issues with these statuses (comma-separated names or IDs)\n\n") + ui.Printf("Environment:\n") + ui.Printf(" REDMINE_URL and REDMINE_TOKEN must be set in .env, as environment variables, or in ~/.redmine-tree.json.\n") + ui.Printf("Example: REDMINE_URL=https://redmine.example.com REDMINE_TOKEN=abc123 %s -status 'In Progress' my-project\n", appName) os.Exit(1) }