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

77 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:18 -0500

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

2 

3from __future__ import annotations 

4 

5import argparse 

6from typing import TYPE_CHECKING 

7 

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

9 

10if TYPE_CHECKING: 

11 from little_loops.config import BRConfig 

12 

13 

14def 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