|
| 1 | +package types |
| 2 | + |
| 3 | +import ( |
| 4 | + "time" |
| 5 | +) |
| 6 | + |
| 7 | +// FilterEvents removes non-essential events to reduce noise. |
| 8 | +// Currently filters out successful status_check events (keeps failures). |
| 9 | +func FilterEvents(events []Event) []Event { |
| 10 | + filtered := make([]Event, 0, len(events)) |
| 11 | + |
| 12 | + for i := range events { |
| 13 | + e := &events[i] |
| 14 | + // Include all non-status_check events |
| 15 | + if e.Kind != EventKindStatusCheck { |
| 16 | + filtered = append(filtered, *e) |
| 17 | + continue |
| 18 | + } |
| 19 | + |
| 20 | + // For status_check events, only include if outcome is failure |
| 21 | + if e.Outcome == "failure" { |
| 22 | + filtered = append(filtered, *e) |
| 23 | + } |
| 24 | + } |
| 25 | + |
| 26 | + return filtered |
| 27 | +} |
| 28 | + |
| 29 | +// UpgradeWriteAccess scans through events and upgrades write_access from 1 (likely) to 2 (definitely) |
| 30 | +// for actors who have performed actions that require write access. |
| 31 | +func UpgradeWriteAccess(events []Event) { |
| 32 | + // Track actors who have definitely demonstrated write access |
| 33 | + confirmed := make(map[string]bool) |
| 34 | + |
| 35 | + // First pass: identify actors who have performed write-access-requiring actions |
| 36 | + for i := range events { |
| 37 | + e := &events[i] |
| 38 | + switch e.Kind { |
| 39 | + case EventKindPRMerged, EventKindLabeled, EventKindUnlabeled, |
| 40 | + EventKindAssigned, EventKindUnassigned, EventKindMilestoned, EventKindDemilestoned: |
| 41 | + // These actions require write access to the repository |
| 42 | + if e.Actor != "" { |
| 43 | + confirmed[e.Actor] = true |
| 44 | + } |
| 45 | + default: |
| 46 | + // Other event types don't require write access |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + // Second pass: upgrade write_access from 1 to 2 for confirmed actors |
| 51 | + for i := range events { |
| 52 | + if events[i].WriteAccess == WriteAccessLikely { |
| 53 | + if confirmed[events[i].Actor] { |
| 54 | + events[i].WriteAccess = WriteAccessDefinitely |
| 55 | + } |
| 56 | + } |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// CalculateCheckSummary analyzes check/status events and categorizes them by outcome. |
| 61 | +func CalculateCheckSummary(events []Event, requiredChecks []string) *CheckSummary { |
| 62 | + summary := &CheckSummary{ |
| 63 | + Success: make(map[string]string), |
| 64 | + Failing: make(map[string]string), |
| 65 | + Pending: make(map[string]string), |
| 66 | + Cancelled: make(map[string]string), |
| 67 | + Skipped: make(map[string]string), |
| 68 | + Stale: make(map[string]string), |
| 69 | + Neutral: make(map[string]string), |
| 70 | + } |
| 71 | + |
| 72 | + // Track latest state for each check (deduplicates multiple runs of same check) |
| 73 | + type checkInfo struct { |
| 74 | + timestamp time.Time |
| 75 | + outcome string |
| 76 | + description string |
| 77 | + } |
| 78 | + latestChecks := make(map[string]checkInfo) |
| 79 | + |
| 80 | + // Collect latest state for each check |
| 81 | + // Events should be sorted chronologically, but we explicitly track timestamps to be safe |
| 82 | + for i := range events { |
| 83 | + e := &events[i] |
| 84 | + if (e.Kind == EventKindStatusCheck || e.Kind == EventKindCheckRun) && e.Body != "" { |
| 85 | + existing, exists := latestChecks[e.Body] |
| 86 | + // Update if: |
| 87 | + // 1. First occurrence (!exists) |
| 88 | + // 2. New event has a later timestamp |
| 89 | + // 3. Both timestamps are zero (fallback to slice order - later in slice wins) |
| 90 | + shouldUpdate := !exists || |
| 91 | + e.Timestamp.After(existing.timestamp) || |
| 92 | + (e.Timestamp.IsZero() && existing.timestamp.IsZero()) |
| 93 | + |
| 94 | + if shouldUpdate { |
| 95 | + latestChecks[e.Body] = checkInfo{ |
| 96 | + outcome: e.Outcome, |
| 97 | + description: e.Description, |
| 98 | + timestamp: e.Timestamp, |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // Collect checks and categorize them |
| 105 | + seen := make(map[string]bool) |
| 106 | + for name, info := range latestChecks { |
| 107 | + // Track required checks we've seen |
| 108 | + for _, req := range requiredChecks { |
| 109 | + if req == name { |
| 110 | + seen[req] = true |
| 111 | + break |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + // Categorize the check (each check goes into exactly one category) |
| 116 | + switch info.outcome { |
| 117 | + case "success": |
| 118 | + summary.Success[name] = info.description |
| 119 | + case "failure", "error", "timed_out", "action_required": |
| 120 | + summary.Failing[name] = info.description |
| 121 | + case "cancelled": |
| 122 | + summary.Cancelled[name] = info.description |
| 123 | + case "pending", "queued", "in_progress", "waiting": |
| 124 | + summary.Pending[name] = info.description |
| 125 | + case "skipped": |
| 126 | + summary.Skipped[name] = info.description |
| 127 | + case "stale": |
| 128 | + summary.Stale[name] = info.description |
| 129 | + case "neutral": |
| 130 | + summary.Neutral[name] = info.description |
| 131 | + default: |
| 132 | + // Unknown outcome, ignore |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + // Add missing required checks as pending |
| 137 | + for _, req := range requiredChecks { |
| 138 | + if !seen[req] { |
| 139 | + summary.Pending[req] = "Expected — Waiting for status to be reported" |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + return summary |
| 144 | +} |
| 145 | + |
| 146 | +// CalculateApprovalSummary analyzes review events and categorizes approvals by reviewer's write access. |
| 147 | +func CalculateApprovalSummary(events []Event) *ApprovalSummary { |
| 148 | + summary := &ApprovalSummary{} |
| 149 | + |
| 150 | + // Track the latest review state from each user |
| 151 | + latestReviews := make(map[string]Event) |
| 152 | + |
| 153 | + for i := range events { |
| 154 | + e := &events[i] |
| 155 | + if e.Kind == EventKindReview && e.Outcome != "" { |
| 156 | + latestReviews[e.Actor] = *e |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + // Check permissions for each reviewer and categorize their reviews |
| 161 | + for actor := range latestReviews { |
| 162 | + review := latestReviews[actor] |
| 163 | + switch review.Outcome { |
| 164 | + case "approved": |
| 165 | + // Use the WriteAccess field that was already populated in the event |
| 166 | + switch review.WriteAccess { |
| 167 | + case WriteAccessDefinitely: |
| 168 | + // Confirmed write access (OWNER, COLLABORATOR, or verified MEMBER) |
| 169 | + summary.ApprovalsWithWriteAccess++ |
| 170 | + case WriteAccessNo: |
| 171 | + // Confirmed no write access (explicitly denied) |
| 172 | + summary.ApprovalsWithoutWriteAccess++ |
| 173 | + default: |
| 174 | + // Unknown/uncertain write access or unexpected values - treat as unknown |
| 175 | + summary.ApprovalsWithUnknownAccess++ |
| 176 | + } |
| 177 | + case "changes_requested": |
| 178 | + summary.ChangesRequested++ |
| 179 | + default: |
| 180 | + // Ignore other review states like "commented" |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + return summary |
| 185 | +} |
| 186 | + |
| 187 | +// CalculateParticipantAccess builds a map of all PR participants to their write access levels. |
| 188 | +// Includes the PR author, assignees, reviewers, and all event actors. |
| 189 | +func CalculateParticipantAccess(events []Event, pr *PullRequest) map[string]int { |
| 190 | + participants := make(map[string]int) |
| 191 | + |
| 192 | + // Add the PR author |
| 193 | + if pr.Author != "" { |
| 194 | + participants[pr.Author] = pr.AuthorWriteAccess |
| 195 | + } |
| 196 | + |
| 197 | + // Add assignees (write access unknown) |
| 198 | + for _, assignee := range pr.Assignees { |
| 199 | + if assignee != "" { |
| 200 | + if _, exists := participants[assignee]; !exists { |
| 201 | + participants[assignee] = WriteAccessNA |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + // Add reviewers (write access unknown at this point) |
| 207 | + for reviewer := range pr.Reviewers { |
| 208 | + if reviewer != "" { |
| 209 | + if _, exists := participants[reviewer]; !exists { |
| 210 | + participants[reviewer] = WriteAccessNA |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + // Collect all unique actors from events and upgrade write access where known |
| 216 | + for i := range events { |
| 217 | + e := &events[i] |
| 218 | + if e.Actor != "" { |
| 219 | + // Keep the highest write access level if we see the same actor multiple times |
| 220 | + if existing, ok := participants[e.Actor]; !ok { |
| 221 | + // New participant |
| 222 | + participants[e.Actor] = e.WriteAccess |
| 223 | + } else if e.WriteAccess > existing { |
| 224 | + // Upgrade to higher write access level |
| 225 | + participants[e.Actor] = e.WriteAccess |
| 226 | + } |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + return participants |
| 231 | +} |
0 commit comments