Merge pull request #75 from LazyBachelor/LPM-140
LPM-140 The Sprinting Update Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer. Changes: Introduces sprint entities in storage and exposes sprint operations via IssueService. Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes. Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
// Variables for completion options and functions.
|
||||
var (
|
||||
typeOptions = []string{"bug", "feature", "task", "chore"}
|
||||
statusOptions = []string{"open", "closed", "in_progress", "blocked", "ready_to_sprint"}
|
||||
statusOptions = []string{"open", "closed", "in_progress", "blocked"}
|
||||
priorityRange = []string{"0", "1", "2", "3", "4"}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ var CreateCmd = &cobra.Command{
|
||||
Example: createCmdExample,
|
||||
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Aliases: []string{"add"},
|
||||
Aliases: []string{"add", "new"},
|
||||
RunE: runCreateCmd,
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func runCreateInteractive() error {
|
||||
func init() {
|
||||
//CreateCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively")
|
||||
CreateCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description")
|
||||
CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress, ready_to_sprint)")
|
||||
CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)")
|
||||
CreateCmd.Flags().StringVarP(&createFlags.issueType, "type", "t", "task", "Issue type(bug, feature, task)")
|
||||
CreateCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)")
|
||||
CreateCmd.Flags().StringVarP(&createFlags.assignee, "assignee", "a", "", "Issue assignee")
|
||||
|
||||
@@ -74,7 +74,7 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error {
|
||||
func init() {
|
||||
ListCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title")
|
||||
ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description")
|
||||
ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress, ready_to_sprint)")
|
||||
ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)")
|
||||
ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)")
|
||||
ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)")
|
||||
ListCmd.Flags().StringVarP(&listFlags.assignee, "assignee", "a", "", "Filter issues by assignee")
|
||||
|
||||
322
internal/commands/issues/sprint.go
Normal file
322
internal/commands/issues/sprint.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package issues
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// sprintFlags holds the flag values for sprint commands
|
||||
type sprintFlags struct {
|
||||
sprintNum int
|
||||
issueID string
|
||||
}
|
||||
|
||||
var sprintCmdFlags sprintFlags
|
||||
|
||||
// SprintCmd represents the sprint management command
|
||||
var SprintCmd = &cobra.Command{
|
||||
Use: "sprint",
|
||||
Short: "Manage sprints",
|
||||
Long: `Manage sprints - create, list, add/remove issues, and view sprint contents.`,
|
||||
}
|
||||
|
||||
// SprintListCmd lists all sprints
|
||||
var SprintListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all sprints",
|
||||
Long: `List all sprints in the project.`,
|
||||
Aliases: []string{"ls"},
|
||||
RunE: runSprintListCmd,
|
||||
}
|
||||
|
||||
// SprintCreateCmd creates a new sprint
|
||||
var SprintCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new sprint",
|
||||
Long: `Create a new sprint for organizing issues.`,
|
||||
Aliases: []string{"new"},
|
||||
RunE: runSprintCreateCmd,
|
||||
}
|
||||
|
||||
// SprintIssuesCmd lists issues in a sprint
|
||||
var SprintIssuesCmd = &cobra.Command{
|
||||
Use: "issues [sprint-num]",
|
||||
Short: "List issues in a sprint",
|
||||
Long: `List all issues assigned to a specific sprint. Use 'backlog' or omit to view the backlog.`,
|
||||
Aliases: []string{"show", "view"},
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runSprintIssuesCmd,
|
||||
}
|
||||
|
||||
// SprintAddCmd adds an issue to a sprint
|
||||
var SprintAddCmd = &cobra.Command{
|
||||
Use: "add [issue-id] [sprint-num]",
|
||||
Short: "Add an issue to a sprint",
|
||||
Long: `Add an issue to a sprint. If sprint-num is omitted, adds to backlog.`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: runSprintAddCmd,
|
||||
|
||||
ValidArgsFunction: completeIssues,
|
||||
}
|
||||
|
||||
// SprintRemoveCmd removes an issue from a sprint
|
||||
var SprintRemoveCmd = &cobra.Command{
|
||||
Use: "remove [issue-id] [sprint-num]",
|
||||
Short: "Remove an issue from a sprint",
|
||||
Long: `Remove an issue from a sprint. If sprint-num is omitted, removes from backlog.`,
|
||||
Aliases: []string{"rm"},
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: runSprintRemoveCmd,
|
||||
|
||||
ValidArgsFunction: completeIssues,
|
||||
}
|
||||
|
||||
// SprintBacklogCmd shows the backlog sprint
|
||||
var SprintBacklogCmd = &cobra.Command{
|
||||
Use: "backlog",
|
||||
Short: "Show backlog issues",
|
||||
Long: `Show all issues in the backlog sprint.`,
|
||||
RunE: runSprintBacklogCmd,
|
||||
}
|
||||
|
||||
// SprintDeleteCmd deletes a sprint
|
||||
var SprintDeleteCmd = &cobra.Command{
|
||||
Use: "delete [sprint-num]",
|
||||
Short: "Delete a sprint",
|
||||
Long: `Delete a sprint by its number. Issues in the sprint will not be deleted.`,
|
||||
Aliases: []string{"del"},
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runSprintDeleteCmd,
|
||||
}
|
||||
|
||||
func runSprintListCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
|
||||
sprints, err := app.Issues.GetSprints(cmd.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get sprints: %w", err)
|
||||
}
|
||||
|
||||
if len(sprints) == 0 {
|
||||
cmd.Println("No sprints found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
backlogNum, _ := app.Issues.GetBacklogSprint(cmd.Context())
|
||||
|
||||
cmd.Println("Sprints:")
|
||||
for _, sprintNum := range sprints {
|
||||
label := ""
|
||||
if sprintNum == backlogNum {
|
||||
label = " (backlog)"
|
||||
}
|
||||
cmd.Printf(" Sprint %d%s\n", sprintNum, label)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSprintCreateCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
|
||||
sprintNum, err := app.Issues.AddSprint(cmd.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sprint: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Created sprint %d\n", sprintNum)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSprintIssuesCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
ctx := cmd.Context()
|
||||
|
||||
var sprintNum int
|
||||
var isBacklog bool
|
||||
var err error
|
||||
|
||||
if len(args) == 0 || args[0] == "backlog" {
|
||||
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get backlog sprint: %w", err)
|
||||
}
|
||||
isBacklog = true
|
||||
} else {
|
||||
sprintNum, err = strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprint number: %s", args[0])
|
||||
}
|
||||
isBacklog = false
|
||||
}
|
||||
|
||||
var issues []*models.Issue
|
||||
if isBacklog {
|
||||
issues, err = app.Issues.GetIssuesNotInAnySprint(ctx)
|
||||
} else {
|
||||
issues, err = app.Issues.GetIssuesBySprint(ctx, sprintNum)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get issues in sprint %d: %w", sprintNum, err)
|
||||
}
|
||||
|
||||
if isBacklog {
|
||||
cmd.Printf("Backlog (%d issues):\n", len(issues))
|
||||
} else {
|
||||
cmd.Printf("Sprint %d (%d issues):\n", sprintNum, len(issues))
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
cmd.Println(" No issues in this sprint.")
|
||||
return nil
|
||||
}
|
||||
|
||||
issuesList := models.IssuesPtrToIssues(issues)
|
||||
models.PrintIssues(issuesList)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSprintAddCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
ctx := cmd.Context()
|
||||
|
||||
issueID := args[0]
|
||||
|
||||
var sprintNum int
|
||||
var err error
|
||||
|
||||
if len(args) == 1 {
|
||||
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get backlog sprint: %w", err)
|
||||
}
|
||||
} else {
|
||||
sprintNum, err = strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprint number: %s", args[1])
|
||||
}
|
||||
}
|
||||
|
||||
err = app.Issues.AddIssueToSprint(ctx, issueID, sprintNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add issue to sprint: %w", err)
|
||||
}
|
||||
|
||||
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
|
||||
|
||||
if sprintNum == backlogNum {
|
||||
cmd.Printf("Added issue %s to backlog\n", issueID)
|
||||
} else {
|
||||
cmd.Printf("Added issue %s to sprint %d\n", issueID, sprintNum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSprintRemoveCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
ctx := cmd.Context()
|
||||
|
||||
issueID := args[0]
|
||||
|
||||
var sprintNum int
|
||||
var err error
|
||||
|
||||
if len(args) == 1 {
|
||||
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get backlog sprint: %w", err)
|
||||
}
|
||||
} else {
|
||||
sprintNum, err = strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprint number: %s", args[1])
|
||||
}
|
||||
}
|
||||
|
||||
err = app.Issues.RemoveIssueFromSprint(ctx, issueID, sprintNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove issue from sprint: %w", err)
|
||||
}
|
||||
|
||||
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
|
||||
|
||||
if sprintNum == backlogNum {
|
||||
cmd.Printf("Removed issue %s from backlog\n", issueID)
|
||||
} else {
|
||||
cmd.Printf("Removed issue %s from sprint %d\n", issueID, sprintNum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSprintBacklogCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
ctx := cmd.Context()
|
||||
|
||||
issues, err := app.Issues.GetIssuesNotInAnySprint(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get backlog issues: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Backlog (%d issues):\n", len(issues))
|
||||
|
||||
if len(issues) == 0 {
|
||||
cmd.Println(" No issues in backlog.")
|
||||
return nil
|
||||
}
|
||||
|
||||
issuesList := models.IssuesPtrToIssues(issues)
|
||||
models.PrintIssues(issuesList)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSprintDeleteCmd(cmd *cobra.Command, args []string) error {
|
||||
app := AppFromContext(cmd.Context())
|
||||
ctx := cmd.Context()
|
||||
|
||||
var sprintNum int
|
||||
var err error
|
||||
|
||||
if len(args) == 0 {
|
||||
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get backlog sprint: %w", err)
|
||||
}
|
||||
} else {
|
||||
sprintNum, err = strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid sprint number: %s", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
|
||||
if sprintNum == backlogNum {
|
||||
return fmt.Errorf("cannot delete the backlog sprint")
|
||||
}
|
||||
|
||||
err = app.Issues.RemoveSprint(ctx, sprintNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete sprint: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Deleted sprint %d\n", sprintNum)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
SprintCmd.AddCommand(SprintListCmd)
|
||||
SprintCmd.AddCommand(SprintBacklogCmd)
|
||||
SprintCmd.AddCommand(SprintCreateCmd)
|
||||
SprintCmd.AddCommand(SprintIssuesCmd)
|
||||
SprintCmd.AddCommand(SprintAddCmd)
|
||||
SprintCmd.AddCommand(SprintRemoveCmd)
|
||||
SprintCmd.AddCommand(SprintDeleteCmd)
|
||||
|
||||
RootCmd.AddCommand(SprintCmd)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error {
|
||||
func init() {
|
||||
UpdateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title")
|
||||
UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description")
|
||||
UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress, ready_to_sprint)")
|
||||
UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)")
|
||||
UpdateCmd.Flags().StringVarP(&updateFlags.issueType, "type", "t", "", "New issue type(bug, feature, task)")
|
||||
UpdateCmd.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-4)")
|
||||
UpdateCmd.Flags().StringVarP(&updateFlags.assignee, "assignee", "a", "", "New issue assignee")
|
||||
|
||||
@@ -53,6 +53,15 @@ type IssueService interface {
|
||||
AddIssueComment(ctx context.Context, issueID, author, text string) (*Comment, error)
|
||||
GetIssueComments(ctx context.Context, issueID string) ([]*Comment, error)
|
||||
GetCommentCounts(ctx context.Context, issueIDs []string) (map[string]int, error)
|
||||
|
||||
AddSprint(ctx context.Context) (int, error)
|
||||
RemoveSprint(ctx context.Context, sprintNum int) error
|
||||
GetSprints(ctx context.Context) ([]int, error)
|
||||
GetBacklogSprint(ctx context.Context) (int, error)
|
||||
GetIssuesBySprint(ctx context.Context, sprintNum int) ([]*Issue, error)
|
||||
GetIssuesNotInAnySprint(ctx context.Context) ([]*Issue, error)
|
||||
AddIssueToSprint(ctx context.Context, issueID string, sprintNum int) error
|
||||
RemoveIssueFromSprint(ctx context.Context, issueID string, sprintNum int) error
|
||||
}
|
||||
|
||||
type StatsService interface {
|
||||
|
||||
@@ -35,12 +35,11 @@ type (
|
||||
|
||||
// Status constants
|
||||
const (
|
||||
StatusOpen = beads.StatusOpen
|
||||
StatusInProgress = beads.StatusInProgress
|
||||
StatusBlocked = beads.StatusBlocked
|
||||
StatusDeferred = beads.StatusDeferred
|
||||
StatusClosed = beads.StatusClosed
|
||||
StatusReadyToSprint Status = "ready_to_sprint"
|
||||
StatusOpen = beads.StatusOpen
|
||||
StatusInProgress = beads.StatusInProgress
|
||||
StatusBlocked = beads.StatusBlocked
|
||||
StatusDeferred = beads.StatusDeferred
|
||||
StatusClosed = beads.StatusClosed
|
||||
)
|
||||
|
||||
// IssueType constants
|
||||
|
||||
@@ -2,7 +2,10 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
|
||||
@@ -21,13 +24,71 @@ func NewBeadsIssueStorage(ctx context.Context, storage beads.Storage, prefix str
|
||||
}
|
||||
}
|
||||
|
||||
storage.SetConfig(ctx, "status.custom", "ready_to_sprint")
|
||||
storage.UnderlyingDB().Exec(`
|
||||
CREATE TABLE IF NOT EXISTS sprints (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
issues TEXT,
|
||||
sprint_num INTEGER UNIQUE,
|
||||
is_backlog BOOLEAN DEFAULT 0
|
||||
);
|
||||
`)
|
||||
|
||||
backlogNum, err := getBacklogSprintNum(storage)
|
||||
if err != nil {
|
||||
_, err = storage.UnderlyingDB().Exec(
|
||||
"INSERT INTO sprints (name, issues, sprint_num, is_backlog) VALUES (?, ?, 0, 1)",
|
||||
"backlog", "[]",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create backlog sprint: %w", err)
|
||||
}
|
||||
storage.SetConfig(ctx, "backlog_sprint", "0")
|
||||
} else {
|
||||
storage.SetConfig(ctx, "backlog_sprint", fmt.Sprintf("%d", backlogNum))
|
||||
}
|
||||
|
||||
return &BeadsService{
|
||||
Storage: storage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) CreateIssue(ctx context.Context, issue *models.Issue, actor string) error {
|
||||
if err := s.Storage.CreateIssue(ctx, issue, actor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backlogNum, err := s.GetBacklogSprint(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.AddIssueToSprint(ctx, issue.ID, backlogNum); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) CreateIssues(ctx context.Context, issues []*models.Issue, actor string) error {
|
||||
if err := s.Storage.CreateIssues(ctx, issues, actor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backlogNum, err := s.GetBacklogSprint(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if err := s.AddIssueToSprint(ctx, issue.ID, backlogNum); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) {
|
||||
issuesPtr, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
@@ -45,10 +106,241 @@ func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) {
|
||||
|
||||
func (s *BeadsService) DeleteIssues() error {
|
||||
|
||||
var deleteIssues = "DELETE FROM issues;"
|
||||
var deleteIssues = `DELETE FROM issues;
|
||||
DELETE FROM sprints;`
|
||||
|
||||
if _, err := s.UnderlyingDB().Exec(deleteIssues); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) AddSprint(ctx context.Context) (int, error) {
|
||||
var addSprint = "INSERT INTO sprints (sprint_num, issues) VALUES ((SELECT IFNULL(MAX(sprint_num), 0) + 1 FROM sprints), '[]');"
|
||||
|
||||
r, err := s.UnderlyingDB().Exec(addSprint)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to add sprint: %w", err)
|
||||
}
|
||||
|
||||
id, err := r.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get last insert id: %w", err)
|
||||
}
|
||||
|
||||
var sprintNum int
|
||||
err = s.UnderlyingDB().QueryRow("SELECT sprint_num FROM sprints WHERE id = ?", id).Scan(&sprintNum)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get sprint_num: %w", err)
|
||||
}
|
||||
|
||||
return sprintNum, nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) RemoveSprint(ctx context.Context, sprintNum int) error {
|
||||
result, err := s.UnderlyingDB().Exec("DELETE FROM sprints WHERE sprint_num = ?", sprintNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove sprint: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check rows affected: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("sprint %d not found", sprintNum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) GetSprints(ctx context.Context) ([]int, error) {
|
||||
rows, err := s.UnderlyingDB().Query("SELECT sprint_num FROM sprints WHERE is_backlog = 0 ORDER BY sprint_num")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sprints: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sprints []int
|
||||
for rows.Next() {
|
||||
var sprintNum int
|
||||
if err := rows.Scan(&sprintNum); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan sprint: %w", err)
|
||||
}
|
||||
sprints = append(sprints, sprintNum)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating sprints: %w", err)
|
||||
}
|
||||
|
||||
return sprints, nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) GetIssuesBySprint(ctx context.Context, sprintNum int) ([]*models.Issue, error) {
|
||||
var issuesJSON string
|
||||
err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []*models.Issue{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get sprint issues: %w", err)
|
||||
}
|
||||
|
||||
var issueIDs []string
|
||||
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal issues: %w", err)
|
||||
}
|
||||
|
||||
if len(issueIDs) == 0 {
|
||||
return []*models.Issue{}, nil
|
||||
}
|
||||
|
||||
var issues []*models.Issue
|
||||
for _, id := range issueIDs {
|
||||
issue, err := s.Storage.GetIssue(ctx, id)
|
||||
if err != nil {
|
||||
// Skip issues that don't exist or can't be retrieved
|
||||
continue
|
||||
}
|
||||
issues = append(issues, issue)
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) AddIssueToSprint(ctx context.Context, issueID string, sprintNum int) error {
|
||||
var issuesJSON string
|
||||
err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("sprint %d not found", sprintNum)
|
||||
}
|
||||
return fmt.Errorf("failed to get sprint: %w", err)
|
||||
}
|
||||
|
||||
var issueIDs []string
|
||||
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal issues: %w", err)
|
||||
}
|
||||
|
||||
if slices.Contains(issueIDs, issueID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
issueIDs = append(issueIDs, issueID)
|
||||
|
||||
updatedJSON, err := json.Marshal(issueIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal issues: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.UnderlyingDB().Exec("UPDATE sprints SET issues = ? WHERE sprint_num = ?", string(updatedJSON), sprintNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update sprint: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) RemoveIssueFromSprint(ctx context.Context, issueID string, sprintNum int) error {
|
||||
var issuesJSON string
|
||||
err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("sprint %d not found", sprintNum)
|
||||
}
|
||||
return fmt.Errorf("failed to get sprint: %w", err)
|
||||
}
|
||||
|
||||
var issueIDs []string
|
||||
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal issues: %w", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
var updatedIDs []string
|
||||
for _, id := range issueIDs {
|
||||
if id != issueID {
|
||||
updatedIDs = append(updatedIDs, id)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
updatedJSON, err := json.Marshal(updatedIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal issues: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.UnderlyingDB().Exec("UPDATE sprints SET issues = ? WHERE sprint_num = ?", string(updatedJSON), sprintNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update sprint: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BeadsService) GetBacklogSprint(ctx context.Context) (int, error) {
|
||||
sprintNum, err := getBacklogSprintNum(s.Storage)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, fmt.Errorf("backlog sprint not found")
|
||||
}
|
||||
return 0, fmt.Errorf("failed to get backlog sprint: %w", err)
|
||||
}
|
||||
return sprintNum, nil
|
||||
}
|
||||
|
||||
// GetIssuesNotInAnySprint returns issues that are only in the backlog
|
||||
func (s *BeadsService) GetIssuesNotInAnySprint(ctx context.Context) ([]*models.Issue, error) {
|
||||
allIssues, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get all issues: %w", err)
|
||||
}
|
||||
|
||||
rows, err := s.UnderlyingDB().Query("SELECT sprint_num, issues FROM sprints WHERE is_backlog = 0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sprint issues: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
issuesInSprints := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
var sprintNum int
|
||||
var issuesJSON string
|
||||
if err := rows.Scan(&sprintNum, &issuesJSON); err != nil {
|
||||
continue
|
||||
}
|
||||
var issueIDs []string
|
||||
if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, id := range issueIDs {
|
||||
issuesInSprints[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
var backlogIssues []*models.Issue
|
||||
for _, issue := range allIssues {
|
||||
if !issuesInSprints[issue.ID] {
|
||||
backlogIssues = append(backlogIssues, issue)
|
||||
}
|
||||
}
|
||||
|
||||
return backlogIssues, nil
|
||||
}
|
||||
|
||||
func getBacklogSprintNum(storage beads.Storage) (int, error) {
|
||||
var sprintNum int
|
||||
err := storage.UnderlyingDB().QueryRow("SELECT sprint_num FROM sprints WHERE is_backlog = 1 LIMIT 1").Scan(&sprintNum)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sprintNum, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user