Coverage for little_loops/cli/issues/list_cmd.py: 0%

1"""ll-issues list: List active issues with optional type/priority/status filters."""

2

3 from __future__ import annotations

4

5 import argparse

6 from typing import TYPE_CHECKING

7

8 from little_loops.cli.output import PRIORITY_COLOR, TYPE_COLOR, colorize, print_json

9

10 if TYPE_CHECKING:

11 from little_loops.config import BRConfig

12

13

14 def cmd_list(config: BRConfig, args: argparse.Namespace) -> int:

15 """List issues with optional filters.

16

17 Args:

18 config: Project configuration

19 args: Parsed arguments with optional .type, .priority, .status, .flat, and .json attributes

20

21 Returns:

22 Exit code (0 = success)

23 """

24 from datetime import date

25

26 from little_loops.cli.issues.search import (

27 _load_issues_with_status,

28 _parse_discovered_date,

29 _sort_issues,

30 )

31

32 status = getattr(args, "status", "active") or "active"

33 include_active = status in ("active", "all")

34 include_completed = status in ("completed", "all")

35 include_deferred = status in ("deferred", "all")

36

37 raw = _load_issues_with_status(config, include_active, include_completed, include_deferred)

38

39 type_filter = getattr(args, "type", None)

40 priority_filter = getattr(args, "priority", None)

41

42 filtered = [

43 (issue, stat)

44 for issue, stat in raw

45 if (not type_filter or issue.issue_id.split("-", 1)[0] == type_filter)

46 and (not priority_filter or issue.priority == priority_filter)

47 ]

48

49 # Sort

50 sort_field = getattr(args, "sort", "priority") or "priority"

51 need_content = sort_field in {"created", "completed"}

52 enriched: list[tuple] = []

53 for issue, stat in filtered:

54 disc_date: date | None = None

55 comp_date: date | None = None

56 if need_content:

57 try:

58 content = issue.path.read_text(encoding="utf-8")

59 except Exception:

60 content = ""

61 if sort_field == "created":

62 disc_date = _parse_discovered_date(content)

63 elif sort_field == "completed":

64 from little_loops.issue_history.parsing import _parse_completion_date

65

66 comp_date = _parse_completion_date(content, issue.path)

67 enriched.append((issue, stat, disc_date, comp_date))

68

69 if getattr(args, "desc", False):

70 descending = True

71 elif getattr(args, "asc", False):

72 descending = False

73 else:

74 descending = sort_field in {"created", "completed"}

75

76 enriched = _sort_issues(enriched, sort_field, descending)

77

78 limit = getattr(args, "limit", None)

79 if limit is not None and limit < 1:

80 import sys

81

82 print(f"Error: --limit must be a positive integer, got {limit}", file=sys.stderr)

83 return 1

84

85 if limit is not None:

86 enriched = enriched[:limit]

87

88 issues_with_status = [(item[0], item[1]) for item in enriched]

89

90 if not issues_with_status:

91 print("No active issues")

92 return 0

93

94 if getattr(args, "json", False):

95 print_json(

96 [

97 {

98 "id": issue.issue_id,

99 "priority": issue.priority,

100 "type": issue.issue_id.split("-", 1)[0],

101 "title": issue.title,

102 "path": str(issue.path),

103 "status": stat,

104 "discovered_date": str(disc_date) if disc_date else None,

105 }

106 for issue, stat, disc_date, _comp_date in enriched

107 ]

108 )

109 return 0

110

111 if getattr(args, "flat", False):

112 for issue, _stat in issues_with_status:

113 print(f"{issue.path.name} {issue.title}")

114 return 0

115

116 # Group by type prefix

117 buckets: dict[str, list] = {"BUG": [], "FEAT": [], "ENH": []}

118 for issue, stat in issues_with_status:

119 prefix = issue.issue_id.split("-", 1)[0]

120 if prefix in buckets:

121 buckets[prefix].append((issue, stat))

122

123 type_labels = {"BUG": "Bugs", "FEAT": "Features", "ENH": "Enhancements"}

124 lines: list[str] = []

125 for prefix, label in type_labels.items():

126 group = buckets[prefix]

127 header = colorize(f"{label} ({len(group)})", f"{TYPE_COLOR.get(prefix, '0')};1")

128 lines.append(header)

129 for issue, stat in group:

130 issue_type = issue.issue_id.split("-", 1)[0]

131 colored_id = colorize(issue.issue_id, TYPE_COLOR.get(issue_type, "0"))

132 colored_priority = colorize(issue.priority, PRIORITY_COLOR.get(issue.priority, "0"))

133 status_tag = f" [{stat}]" if stat != "active" else ""

134 lines.append(f" {colored_priority} {colored_id} {issue.title}{status_tag}")

135 lines.append("")

136 lines.append(f"Total: {len(issues_with_status)} active issues")

137 print("\n".join(lines))

138 return 0