feat(gitsync): enrich PR status with author, base branch, review info (#23038)

## Summary

Adds 7 new fields to the PR status stored by gitsync, all sourced from
the existing GitHub API calls (**zero additional HTTP requests**):

| Field | Source | Purpose |
|---|---|---|
| `author_login` | `pull.user.login` | PR author username |
| `author_avatar_url` | `pull.user.avatar_url` | PR author avatar for UI
|
| `base_branch` | `pull.base.ref` | Target branch (e.g. `main`) |
| `pr_number` | `pull.number` | Explicit PR number |
| `commits` | `pull.commits` | Number of commits in PR |
| `approved` | Derived from reviews | True when ≥1 approved, no
outstanding changes requested |
| `reviewer_count` | Derived from reviews | Distinct reviewers with a
decisive state |

## Changes

- **`gitprovider/gitprovider.go`**: Added 7 fields to `PRStatus` struct.
- **`gitprovider/github.go`**: Expanded the anonymous struct in
`FetchPullRequestStatus` to decode new JSON fields. Replaced
`hasOutstandingChangesRequested()` with `summarizeReviews()` returning a
`reviewStats` struct with `changesRequested`, `approved`, and
`reviewerCount`.
- **Migration 000434**: Adds 7 columns to `chat_diff_statuses`.
- **`queries/chats.sql`**: Updated `UpsertChatDiffStatus`
INSERT/VALUES/ON CONFLICT.
- **`gitsync/gitsync.go`**: Maps new `PRStatus` fields into upsert
params.
- **`gitsync/worker.go`**: Maps new columns in row-to-model converter.
- **`codersdk/chats.go`**: Added fields to SDK `ChatDiffStatus` type.
- **`coderd/chats.go`**: Maps new DB fields in
`convertChatDiffStatus()`.
- Auto-generated: `models.go`, `queries.sql.go`, `dump.sql`,
`typesGenerated.ts`.
This commit is contained in:
Kyle Carberry
2026-03-13 18:54:07 -04:00
committed by GitHub
parent f714f589c5
commit c5b8611c5a
13 changed files with 248 additions and 14 deletions
+45 -5
View File
@@ -265,9 +265,18 @@ func (g *githubProvider) FetchPullRequestStatus(
Additions int32 `json:"additions"`
Deletions int32 `json:"deletions"`
ChangedFiles int32 `json:"changed_files"`
Number int `json:"number"`
Commits int32 `json:"commits"`
Head struct {
SHA string `json:"sha"`
} `json:"head"`
User struct {
Login string `json:"login"`
AvatarURL string `json:"avatar_url"`
} `json:"user"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
}
if err := g.decodeJSON(ctx, pullEndpoint, token, &pull); err != nil {
return nil, err
@@ -298,6 +307,8 @@ func (g *githubProvider) FetchPullRequestStatus(
state = PRStateMerged
}
reviewInfo := summarizeReviews(reviews)
return &PRStatus{
Title: pull.Title,
State: state,
@@ -308,7 +319,14 @@ func (g *githubProvider) FetchPullRequestStatus(
Deletions: pull.Deletions,
ChangedFiles: pull.ChangedFiles,
},
ChangesRequested: hasOutstandingChangesRequested(reviews),
ChangesRequested: reviewInfo.changesRequested,
Approved: reviewInfo.approved,
ReviewerCount: reviewInfo.reviewerCount,
AuthorLogin: pull.User.Login,
AuthorAvatarURL: pull.User.AvatarURL,
BaseBranch: pull.Base.Ref,
PRNumber: pull.Number,
Commits: pull.Commits,
FetchedAt: g.clock.Now().UTC(),
}, nil
}
@@ -495,7 +513,18 @@ func ParseRetryAfter(h http.Header, clk quartz.Clock) time.Duration {
return 0
}
func hasOutstandingChangesRequested(
// reviewStats holds aggregated review statistics for a PR.
type reviewStats struct {
changesRequested bool
approved bool
reviewerCount int32
}
// summarizeReviews extracts review statistics from a list of
// reviews. For each reviewer, only the latest decisive review
// (by ID) is considered. "Decisive" means APPROVED,
// CHANGES_REQUESTED, or DISMISSED.
func summarizeReviews(
reviews []struct {
ID int64 `json:"id"`
State string `json:"state"`
@@ -503,7 +532,7 @@ func hasOutstandingChangesRequested(
Login string `json:"login"`
} `json:"user"`
},
) bool {
) reviewStats {
type reviewerState struct {
reviewID int64
state string
@@ -533,10 +562,21 @@ func hasOutstandingChangesRequested(
}
}
var result reviewStats
result.reviewerCount = int32(len(statesByReviewer))
hasApproval := false
for _, state := range statesByReviewer {
if state.state == "CHANGES_REQUESTED" {
return true
result.changesRequested = true
}
if state.state == "APPROVED" {
hasApproval = true
}
}
return false
// Approved is true only when at least one reviewer approved
// and no reviewer has outstanding changes requested.
result.approved = hasApproval && !result.changesRequested
return result
}
@@ -79,6 +79,23 @@ type PRStatus struct {
// ChangesRequested is a convenience boolean: true if any
// reviewer's current state is "changes_requested".
ChangesRequested bool
// AuthorLogin is the login/username of the PR author.
AuthorLogin string
// AuthorAvatarURL is the avatar URL of the PR author.
AuthorAvatarURL string
// BaseBranch is the target branch the PR will merge into.
BaseBranch string
// PRNumber is the PR number (e.g. 1347).
PRNumber int
// Commits is the number of commits in the PR.
Commits int32
// Approved is true when at least one reviewer has approved
// and no reviewer has outstanding changes requested.
Approved bool
// ReviewerCount is the number of distinct reviewers who
// have left a decisive review (approved, changes_requested,
// or dismissed).
ReviewerCount int32
// FetchedAt is when this status was fetched.
FetchedAt time.Time
}