Merge branch 'main' of github.com:steveyegge/beads
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("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
|
||||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
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("Priority: P%d\n", issue.Priority)
|
||||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||||
if issue.Assignee != "" {
|
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("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
|
||||||
fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
|
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("Priority: P%d\n", issue.Priority)
|
||||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||||
if issue.Assignee != "" {
|
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
|
return issues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,9 +212,93 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
}
|
}
|
||||||
issue.Labels = labels
|
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
|
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
|
// GetIssueByExternalRef retrieves an issue by external reference
|
||||||
func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef string) (*types.Issue, error) {
|
func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef string) (*types.Issue, error) {
|
||||||
var issue types.Issue
|
var issue types.Issue
|
||||||
@@ -282,6 +366,15 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
}
|
}
|
||||||
issue.Labels = labels
|
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
|
return &issue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Issue struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
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"
|
ExternalRef *string `json:"external_ref,omitempty"` // e.g., "gh-9", "jira-ABC"
|
||||||
CompactionLevel int `json:"compaction_level,omitempty"`
|
CompactionLevel int `json:"compaction_level,omitempty"`
|
||||||
CompactedAt *time.Time `json:"compacted_at,omitempty"`
|
CompactedAt *time.Time `json:"compacted_at,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user