Add close reason display for closed issues
- Add CloseReason field to Issue struct - Add GetCloseReason and GetCloseReasonsForIssues queries - Batch-load close reasons in scanIssues for efficiency - Display close reason in bd show output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,9 @@ var showCmd = &cobra.Command{
|
||||
|
||||
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
|
||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
}
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
@@ -264,6 +267,9 @@ var showCmd = &cobra.Command{
|
||||
|
||||
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
|
||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
}
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
|
||||
@@ -736,6 +736,19 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
||||
}
|
||||
}
|
||||
|
||||
// Third pass: batch-load close reasons for closed issues
|
||||
closeReasonsMap, err := s.GetCloseReasonsForIssues(ctx, issueIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to batch get close reasons: %w", err)
|
||||
}
|
||||
|
||||
// Assign close reasons to issues
|
||||
for _, issue := range issues {
|
||||
if reason, ok := closeReasonsMap[issue.ID]; ok {
|
||||
issue.CloseReason = reason
|
||||
}
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -212,9 +212,93 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
}
|
||||
issue.Labels = labels
|
||||
|
||||
// Fetch close reason if issue is closed
|
||||
if issue.Status == types.StatusClosed {
|
||||
closeReason, err := s.GetCloseReason(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get close reason: %w", err)
|
||||
}
|
||||
issue.CloseReason = closeReason
|
||||
}
|
||||
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
// GetCloseReason retrieves the close reason from the most recent closed event for an issue
|
||||
func (s *SQLiteStorage) GetCloseReason(ctx context.Context, issueID string) (string, error) {
|
||||
var comment sql.NullString
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT comment FROM events
|
||||
WHERE issue_id = ? AND event_type = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, issueID, types.EventClosed).Scan(&comment)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get close reason: %w", err)
|
||||
}
|
||||
if comment.Valid {
|
||||
return comment.String, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GetCloseReasonsForIssues retrieves close reasons for multiple issues in a single query
|
||||
func (s *SQLiteStorage) GetCloseReasonsForIssues(ctx context.Context, issueIDs []string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
if len(issueIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Build placeholders for IN clause
|
||||
placeholders := make([]string, len(issueIDs))
|
||||
args := make([]interface{}, len(issueIDs)+1)
|
||||
args[0] = types.EventClosed
|
||||
for i, id := range issueIDs {
|
||||
placeholders[i] = "?"
|
||||
args[i+1] = id
|
||||
}
|
||||
|
||||
// Use a subquery to get the most recent closed event for each issue
|
||||
// #nosec G201 - safe SQL with controlled formatting
|
||||
query := fmt.Sprintf(`
|
||||
SELECT e.issue_id, e.comment
|
||||
FROM events e
|
||||
INNER JOIN (
|
||||
SELECT issue_id, MAX(created_at) as max_created_at
|
||||
FROM events
|
||||
WHERE event_type = ? AND issue_id IN (%s)
|
||||
GROUP BY issue_id
|
||||
) latest ON e.issue_id = latest.issue_id AND e.created_at = latest.max_created_at
|
||||
WHERE e.event_type = ?
|
||||
`, strings.Join(placeholders, ", "))
|
||||
|
||||
// Append event_type again for the outer WHERE clause
|
||||
args = append(args, types.EventClosed)
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get close reasons: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
for rows.Next() {
|
||||
var issueID string
|
||||
var comment sql.NullString
|
||||
if err := rows.Scan(&issueID, &comment); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan close reason: %w", err)
|
||||
}
|
||||
if comment.Valid && comment.String != "" {
|
||||
result[issueID] = comment.String
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetIssueByExternalRef retrieves an issue by external reference
|
||||
func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef string) (*types.Issue, error) {
|
||||
var issue types.Issue
|
||||
@@ -282,6 +366,15 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
}
|
||||
issue.Labels = labels
|
||||
|
||||
// Fetch close reason if issue is closed
|
||||
if issue.Status == types.StatusClosed {
|
||||
closeReason, err := s.GetCloseReason(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get close reason: %w", err)
|
||||
}
|
||||
issue.CloseReason = closeReason
|
||||
}
|
||||
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type Issue struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||
CloseReason string `json:"close_reason,omitempty"` // Reason provided when closing the issue
|
||||
ExternalRef *string `json:"external_ref,omitempty"` // e.g., "gh-9", "jira-ABC"
|
||||
CompactionLevel int `json:"compaction_level,omitempty"`
|
||||
CompactedAt *time.Time `json:"compacted_at,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user