feat: Add Unix-friendly issue index with multiple output formats
- Add issue-index command with TSV, CSV, JSON, and fields output formats - Support sorting by number, title, priority, state, created, updated - Add filtering by state (open/closed) and priority level - Include proper data cleaning for Unix pipeline processing - Add make targets: issues-get, issues-csv, issues-json, issues-high - Optimize for awk, cut, grep, and other Unix text processing tools 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -89,3 +89,4 @@ debug_*.py
|
||||
# Claude Code local settings (user-specific permissions)
|
||||
.claude/settings.local.json
|
||||
|
||||
.aider*
|
||||
|
||||
44
Makefile
44
Makefile
@@ -37,9 +37,13 @@ help:
|
||||
@echo " add-diary-entry - Add new entry to ProjectDiary.md (requires Claude Code)"
|
||||
@echo ""
|
||||
@echo "Issue Management:"
|
||||
@echo " list-issues - Show all gitea issues with status and priority"
|
||||
@echo " list-open-issues - Show only open issues (active backlog)"
|
||||
@echo " show-issue NUM=X - Show detailed view of specific issue"
|
||||
@echo " list-issues - Show all gitea issues with status and priority"
|
||||
@echo " list-open-issues - Show only open issues (active backlog)"
|
||||
@echo " show-issue NUM=X - Show detailed view of specific issue"
|
||||
@echo " issues-get - Export compact issue index to ISSUES.index"
|
||||
@echo " issues-csv - Export issues as CSV for spreadsheet processing"
|
||||
@echo " issues-json - Export issues as JSON for programmatic processing"
|
||||
@echo " issues-high - Export only high/critical priority issues"
|
||||
@echo ""
|
||||
@echo "Test-Driven Development:"
|
||||
@echo " test-from-issue NUM=X - Generate test skeleton from issue (requires Claude Code)"
|
||||
@@ -247,6 +251,40 @@ show-issue: $(VENV)/bin/activate
|
||||
list-open-issues: $(VENV)/bin/activate
|
||||
@PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py list-open-issues
|
||||
|
||||
# Export compact issue index to ISSUES.index file (TSV format)
|
||||
issues-get: $(VENV)/bin/activate
|
||||
@echo "📋 Fetching issue index from gitea..."
|
||||
@PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format tsv --sort number > ISSUES.index
|
||||
@echo "✅ Issue index exported to ISSUES.index (TSV format)"
|
||||
@echo "📄 File contents:"
|
||||
@cat ISSUES.index
|
||||
|
||||
# Export issues as CSV for spreadsheet processing
|
||||
issues-csv: $(VENV)/bin/activate
|
||||
@echo "📊 Exporting issues as CSV..."
|
||||
@PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format csv --sort priority --include-state > ISSUES.csv
|
||||
@echo "✅ Issues exported to ISSUES.csv"
|
||||
@wc -l ISSUES.csv | awk '{print "📄 Total entries:", $$1-1, "(excluding header)"}'
|
||||
|
||||
# Export issues as JSON for programmatic processing
|
||||
issues-json: $(VENV)/bin/activate
|
||||
@echo "🔧 Exporting issues as JSON..."
|
||||
@PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format json --sort priority > ISSUES.json
|
||||
@echo "✅ Issues exported to ISSUES.json"
|
||||
@echo "📄 Sample entry:"
|
||||
@head -20 ISSUES.json
|
||||
|
||||
# Export only high and critical priority issues
|
||||
issues-high: $(VENV)/bin/activate
|
||||
@echo "🚨 Exporting high priority issues..."
|
||||
@echo "High priority issues:" > ISSUES.high.txt
|
||||
@PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format tsv --filter-priority high --sort number >> ISSUES.high.txt
|
||||
@echo "" >> ISSUES.high.txt
|
||||
@echo "Critical priority issues:" >> ISSUES.high.txt
|
||||
@PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format tsv --filter-priority critical --sort number >> ISSUES.high.txt
|
||||
@echo "✅ High priority issues exported to ISSUES.high.txt"
|
||||
@cat ISSUES.high.txt
|
||||
|
||||
# Generate test skeleton from gitea issue (requires Claude Code)
|
||||
test-from-issue:
|
||||
@if [ -z "$(NUM)" ]; then \
|
||||
|
||||
137
tddai_cli.py
137
tddai_cli.py
@@ -679,6 +679,123 @@ def assign_issue_to_milestone(issue_number: int, milestone_id: int):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False):
|
||||
"""Output compact index of all issues with ID, title, and priority for efficient parsing.
|
||||
|
||||
Args:
|
||||
format_type: Output format (tsv, csv, json, fields)
|
||||
sort_by: Sort by field (number, title, priority, state, created, updated)
|
||||
filter_state: Filter by state (open, closed, all) - None means all
|
||||
filter_priority: Filter by priority (low, medium, high, critical, none) - None means all
|
||||
include_state: Include state column in output
|
||||
"""
|
||||
try:
|
||||
fetcher = IssueFetcher()
|
||||
project_mgr = ProjectManager()
|
||||
from tddai.config import get_config
|
||||
import json
|
||||
|
||||
config = get_config()
|
||||
|
||||
issues = fetcher.fetch_issues()
|
||||
if not issues:
|
||||
return
|
||||
|
||||
# Collect full issue data with additional fields
|
||||
issue_data = []
|
||||
for issue in issues:
|
||||
# Get priority and state from labels
|
||||
priority = "none"
|
||||
state = "none"
|
||||
try:
|
||||
# Use ProjectManager's API call method
|
||||
issue_url = f"{config.issues_api_url}/{issue.number}"
|
||||
detailed_issue = project_mgr._make_api_call('GET', issue_url)
|
||||
labels = detailed_issue.get('labels', [])
|
||||
priority_labels = [l['name'] for l in labels if l['name'].startswith('priority:')]
|
||||
state_labels = [l['name'] for l in labels if l['name'].startswith('status:')]
|
||||
|
||||
if priority_labels:
|
||||
priority = priority_labels[0].replace('priority:', '')
|
||||
if state_labels:
|
||||
state = state_labels[0].replace('status:', '')
|
||||
except:
|
||||
pass # Keep defaults if API call fails
|
||||
|
||||
issue_info = {
|
||||
'number': issue.number,
|
||||
'title': issue.title.replace('\t', ' ').replace('\n', ' '), # Clean for TSV
|
||||
'priority': priority,
|
||||
'state': issue.state, # open/closed from basic data
|
||||
'status': state, # detailed status from labels
|
||||
'created': issue.created_at.strftime('%Y-%m-%d'),
|
||||
'updated': issue.updated_at.strftime('%Y-%m-%d')
|
||||
}
|
||||
issue_data.append(issue_info)
|
||||
|
||||
# Apply filters
|
||||
if filter_state:
|
||||
if filter_state == "open":
|
||||
issue_data = [i for i in issue_data if i['state'] == 'open']
|
||||
elif filter_state == "closed":
|
||||
issue_data = [i for i in issue_data if i['state'] == 'closed']
|
||||
|
||||
if filter_priority:
|
||||
issue_data = [i for i in issue_data if i['priority'] == filter_priority]
|
||||
|
||||
# Sort issues
|
||||
sort_key_map = {
|
||||
'number': lambda x: x['number'],
|
||||
'title': lambda x: x['title'].lower(),
|
||||
'priority': lambda x: {'critical': 4, 'high': 3, 'medium': 2, 'low': 1, 'none': 0}[x['priority']],
|
||||
'state': lambda x: x['state'],
|
||||
'created': lambda x: x['created'],
|
||||
'updated': lambda x: x['updated']
|
||||
}
|
||||
|
||||
if sort_by in sort_key_map:
|
||||
issue_data.sort(key=sort_key_map[sort_by], reverse=(sort_by in ['number', 'priority', 'created', 'updated']))
|
||||
|
||||
# Output in requested format
|
||||
if format_type == "json":
|
||||
print(json.dumps(issue_data, indent=2))
|
||||
elif format_type == "csv":
|
||||
# CSV header
|
||||
if include_state:
|
||||
print("number,title,priority,state,created,updated")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace('"', '""') # Escape quotes
|
||||
print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["state"]},{issue["created"]},{issue["updated"]}')
|
||||
else:
|
||||
print("number,title,priority,created,updated")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace('"', '""')
|
||||
print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["created"]},{issue["updated"]}')
|
||||
elif format_type == "fields":
|
||||
# Space-separated fields for easy awk processing
|
||||
if include_state:
|
||||
print("NUMBER TITLE PRIORITY STATE CREATED UPDATED")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace(' ', '_')
|
||||
print(f'{issue["number"]} {title} {issue["priority"]} {issue["state"]} {issue["created"]} {issue["updated"]}')
|
||||
else:
|
||||
print("NUMBER TITLE PRIORITY CREATED UPDATED")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace(' ', '_')
|
||||
print(f'{issue["number"]} {title} {issue["priority"]} {issue["created"]} {issue["updated"]}')
|
||||
else: # Default TSV
|
||||
if include_state:
|
||||
for issue in issue_data:
|
||||
print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["state"]}\t{issue["created"]}\t{issue["updated"]}')
|
||||
else:
|
||||
for issue in issue_data:
|
||||
print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["created"]}\t{issue["updated"]}')
|
||||
|
||||
except TddaiError as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def project_overview():
|
||||
"""Show project management overview."""
|
||||
try:
|
||||
@@ -718,6 +835,18 @@ def main():
|
||||
subparsers.add_parser('list-issues', help='List all issues')
|
||||
subparsers.add_parser('list-open-issues', help='List open issues')
|
||||
|
||||
index_parser = subparsers.add_parser('issue-index', help='Output compact issue index for Unix processing')
|
||||
index_parser.add_argument('--format', choices=['tsv', 'csv', 'json', 'fields'], default='tsv',
|
||||
help='Output format (default: tsv)')
|
||||
index_parser.add_argument('--sort', choices=['number', 'title', 'priority', 'state', 'created', 'updated'],
|
||||
default='number', help='Sort by field (default: number)')
|
||||
index_parser.add_argument('--filter-state', choices=['open', 'closed'],
|
||||
help='Filter by issue state')
|
||||
index_parser.add_argument('--filter-priority', choices=['low', 'medium', 'high', 'critical', 'none'],
|
||||
help='Filter by priority level')
|
||||
index_parser.add_argument('--include-state', action='store_true',
|
||||
help='Include state column in output')
|
||||
|
||||
show_parser = subparsers.add_parser('show-issue', help='Show issue details')
|
||||
show_parser.add_argument('issue_number', type=int, help='Issue number')
|
||||
|
||||
@@ -783,6 +912,14 @@ def main():
|
||||
list_issues()
|
||||
elif args.command == 'list-open-issues':
|
||||
list_open_issues()
|
||||
elif args.command == 'issue-index':
|
||||
issue_index(
|
||||
format_type=args.format,
|
||||
sort_by=args.sort,
|
||||
filter_state=args.filter_state,
|
||||
filter_priority=args.filter_priority,
|
||||
include_state=args.include_state
|
||||
)
|
||||
elif args.command == 'show-issue':
|
||||
show_issue(args.issue_number)
|
||||
elif args.command == 'analyze-coverage':
|
||||
|
||||
Reference in New Issue
Block a user