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 15:54:07 -07:00
committed by GitHub
parent f714f589c5
commit c5b8611c5a
13 changed files with 248 additions and 14 deletions
+21
View File
@@ -2599,6 +2599,27 @@ func convertChatDiffStatus(chatID uuid.UUID, status *database.ChatDiffStatus) co
result.Additions = status.Additions
result.Deletions = status.Deletions
result.ChangedFiles = status.ChangedFiles
if status.AuthorLogin.Valid {
result.AuthorLogin = &status.AuthorLogin.String
}
if status.AuthorAvatarUrl.Valid {
result.AuthorAvatarURL = &status.AuthorAvatarUrl.String
}
if status.BaseBranch.Valid {
result.BaseBranch = &status.BaseBranch.String
}
if status.PrNumber.Valid {
result.PRNumber = &status.PrNumber.Int32
}
if status.Commits.Valid {
result.Commits = &status.Commits.Int32
}
if status.Approved.Valid {
result.Approved = &status.Approved.Bool
}
if status.ReviewerCount.Valid {
result.ReviewerCount = &status.ReviewerCount.Int32
}
if status.RefreshedAt.Valid {
refreshedAt := status.RefreshedAt.Time
result.RefreshedAt = &refreshedAt
+8 -1
View File
@@ -1200,7 +1200,14 @@ CREATE TABLE chat_diff_statuses (
git_branch text DEFAULT ''::text NOT NULL,
git_remote_origin text DEFAULT ''::text NOT NULL,
pull_request_title text DEFAULT ''::text NOT NULL,
pull_request_draft boolean DEFAULT false NOT NULL
pull_request_draft boolean DEFAULT false NOT NULL,
author_login text,
author_avatar_url text,
base_branch text,
pr_number integer,
commits integer,
approved boolean,
reviewer_count integer
);
CREATE TABLE chat_files (
@@ -0,0 +1,7 @@
ALTER TABLE chat_diff_statuses DROP COLUMN author_login;
ALTER TABLE chat_diff_statuses DROP COLUMN author_avatar_url;
ALTER TABLE chat_diff_statuses DROP COLUMN base_branch;
ALTER TABLE chat_diff_statuses DROP COLUMN pr_number;
ALTER TABLE chat_diff_statuses DROP COLUMN commits;
ALTER TABLE chat_diff_statuses DROP COLUMN approved;
ALTER TABLE chat_diff_statuses DROP COLUMN reviewer_count;
@@ -0,0 +1,7 @@
ALTER TABLE chat_diff_statuses ADD COLUMN author_login TEXT;
ALTER TABLE chat_diff_statuses ADD COLUMN author_avatar_url TEXT;
ALTER TABLE chat_diff_statuses ADD COLUMN base_branch TEXT;
ALTER TABLE chat_diff_statuses ADD COLUMN pr_number INTEGER;
ALTER TABLE chat_diff_statuses ADD COLUMN commits INTEGER;
ALTER TABLE chat_diff_statuses ADD COLUMN approved BOOLEAN;
ALTER TABLE chat_diff_statuses ADD COLUMN reviewer_count INTEGER;
+7
View File
@@ -4046,6 +4046,13 @@ type ChatDiffStatus struct {
GitRemoteOrigin string `db:"git_remote_origin" json:"git_remote_origin"`
PullRequestTitle string `db:"pull_request_title" json:"pull_request_title"`
PullRequestDraft bool `db:"pull_request_draft" json:"pull_request_draft"`
AuthorLogin sql.NullString `db:"author_login" json:"author_login"`
AuthorAvatarUrl sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
BaseBranch sql.NullString `db:"base_branch" json:"base_branch"`
PrNumber sql.NullInt32 `db:"pr_number" json:"pr_number"`
Commits sql.NullInt32 `db:"commits" json:"commits"`
Approved sql.NullBool `db:"approved" json:"approved"`
ReviewerCount sql.NullInt32 `db:"reviewer_count" json:"reviewer_count"`
}
type ChatFile struct {
+85 -8
View File
@@ -3030,10 +3030,10 @@ WITH acquired AS (
LIMIT
$1::int
)
RETURNING chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft
RETURNING chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count
)
SELECT
acquired.chat_id, acquired.url, acquired.pull_request_state, acquired.changes_requested, acquired.additions, acquired.deletions, acquired.changed_files, acquired.refreshed_at, acquired.stale_at, acquired.created_at, acquired.updated_at, acquired.git_branch, acquired.git_remote_origin, acquired.pull_request_title, acquired.pull_request_draft,
acquired.chat_id, acquired.url, acquired.pull_request_state, acquired.changes_requested, acquired.additions, acquired.deletions, acquired.changed_files, acquired.refreshed_at, acquired.stale_at, acquired.created_at, acquired.updated_at, acquired.git_branch, acquired.git_remote_origin, acquired.pull_request_title, acquired.pull_request_draft, acquired.author_login, acquired.author_avatar_url, acquired.base_branch, acquired.pr_number, acquired.commits, acquired.approved, acquired.reviewer_count,
c.owner_id
FROM
acquired
@@ -3057,6 +3057,13 @@ type AcquireStaleChatDiffStatusesRow struct {
GitRemoteOrigin string `db:"git_remote_origin" json:"git_remote_origin"`
PullRequestTitle string `db:"pull_request_title" json:"pull_request_title"`
PullRequestDraft bool `db:"pull_request_draft" json:"pull_request_draft"`
AuthorLogin sql.NullString `db:"author_login" json:"author_login"`
AuthorAvatarUrl sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
BaseBranch sql.NullString `db:"base_branch" json:"base_branch"`
PrNumber sql.NullInt32 `db:"pr_number" json:"pr_number"`
Commits sql.NullInt32 `db:"commits" json:"commits"`
Approved sql.NullBool `db:"approved" json:"approved"`
ReviewerCount sql.NullInt32 `db:"reviewer_count" json:"reviewer_count"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
}
@@ -3085,6 +3092,13 @@ func (q *sqlQuerier) AcquireStaleChatDiffStatuses(ctx context.Context, limitVal
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
&i.OwnerID,
); err != nil {
return nil, err
@@ -3581,7 +3595,7 @@ func (q *sqlQuerier) GetChatCostSummary(ctx context.Context, arg GetChatCostSumm
const getChatDiffStatusByChatID = `-- name: GetChatDiffStatusByChatID :one
SELECT
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count
FROM
chat_diff_statuses
WHERE
@@ -3607,13 +3621,20 @@ func (q *sqlQuerier) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
)
return i, err
}
const getChatDiffStatusesByChatIDs = `-- name: GetChatDiffStatusesByChatIDs :many
SELECT
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count
FROM
chat_diff_statuses
WHERE
@@ -3645,6 +3666,13 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
); err != nil {
return nil, err
}
@@ -4527,6 +4555,13 @@ INSERT INTO chat_diff_statuses (
additions,
deletions,
changed_files,
author_login,
author_avatar_url,
base_branch,
pr_number,
commits,
approved,
reviewer_count,
refreshed_at,
stale_at
) VALUES (
@@ -4539,8 +4574,15 @@ INSERT INTO chat_diff_statuses (
$7::integer,
$8::integer,
$9::integer,
$10::timestamptz,
$11::timestamptz
$10::text,
$11::text,
$12::text,
$13::integer,
$14::integer,
$15::boolean,
$16::integer,
$17::timestamptz,
$18::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
@@ -4552,11 +4594,18 @@ SET
additions = EXCLUDED.additions,
deletions = EXCLUDED.deletions,
changed_files = EXCLUDED.changed_files,
author_login = EXCLUDED.author_login,
author_avatar_url = EXCLUDED.author_avatar_url,
base_branch = EXCLUDED.base_branch,
pr_number = EXCLUDED.pr_number,
commits = EXCLUDED.commits,
approved = EXCLUDED.approved,
reviewer_count = EXCLUDED.reviewer_count,
refreshed_at = EXCLUDED.refreshed_at,
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
RETURNING
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count
`
type UpsertChatDiffStatusParams struct {
@@ -4569,6 +4618,13 @@ type UpsertChatDiffStatusParams struct {
Additions int32 `db:"additions" json:"additions"`
Deletions int32 `db:"deletions" json:"deletions"`
ChangedFiles int32 `db:"changed_files" json:"changed_files"`
AuthorLogin sql.NullString `db:"author_login" json:"author_login"`
AuthorAvatarUrl sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
BaseBranch sql.NullString `db:"base_branch" json:"base_branch"`
PrNumber sql.NullInt32 `db:"pr_number" json:"pr_number"`
Commits sql.NullInt32 `db:"commits" json:"commits"`
Approved sql.NullBool `db:"approved" json:"approved"`
ReviewerCount sql.NullInt32 `db:"reviewer_count" json:"reviewer_count"`
RefreshedAt time.Time `db:"refreshed_at" json:"refreshed_at"`
StaleAt time.Time `db:"stale_at" json:"stale_at"`
}
@@ -4584,6 +4640,13 @@ func (q *sqlQuerier) UpsertChatDiffStatus(ctx context.Context, arg UpsertChatDif
arg.Additions,
arg.Deletions,
arg.ChangedFiles,
arg.AuthorLogin,
arg.AuthorAvatarUrl,
arg.BaseBranch,
arg.PrNumber,
arg.Commits,
arg.Approved,
arg.ReviewerCount,
arg.RefreshedAt,
arg.StaleAt,
)
@@ -4604,6 +4667,13 @@ func (q *sqlQuerier) UpsertChatDiffStatus(ctx context.Context, arg UpsertChatDif
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
)
return i, err
}
@@ -4639,7 +4709,7 @@ SET
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
RETURNING
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count
`
type UpsertChatDiffStatusReferenceParams struct {
@@ -4675,6 +4745,13 @@ func (q *sqlQuerier) UpsertChatDiffStatusReference(ctx context.Context, arg Upse
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
)
return i, err
}
+21
View File
@@ -364,6 +364,13 @@ INSERT INTO chat_diff_statuses (
additions,
deletions,
changed_files,
author_login,
author_avatar_url,
base_branch,
pr_number,
commits,
approved,
reviewer_count,
refreshed_at,
stale_at
) VALUES (
@@ -376,6 +383,13 @@ INSERT INTO chat_diff_statuses (
@additions::integer,
@deletions::integer,
@changed_files::integer,
sqlc.narg('author_login')::text,
sqlc.narg('author_avatar_url')::text,
sqlc.narg('base_branch')::text,
sqlc.narg('pr_number')::integer,
sqlc.narg('commits')::integer,
sqlc.narg('approved')::boolean,
sqlc.narg('reviewer_count')::integer,
@refreshed_at::timestamptz,
@stale_at::timestamptz
)
@@ -389,6 +403,13 @@ SET
additions = EXCLUDED.additions,
deletions = EXCLUDED.deletions,
changed_files = EXCLUDED.changed_files,
author_login = EXCLUDED.author_login,
author_avatar_url = EXCLUDED.author_avatar_url,
base_branch = EXCLUDED.base_branch,
pr_number = EXCLUDED.pr_number,
commits = EXCLUDED.commits,
approved = EXCLUDED.approved,
reviewer_count = EXCLUDED.reviewer_count,
refreshed_at = EXCLUDED.refreshed_at,
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
+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
}
+7
View File
@@ -316,6 +316,13 @@ func (r *Refresher) refreshOne(
Additions: status.DiffStats.Additions,
Deletions: status.DiffStats.Deletions,
ChangedFiles: status.DiffStats.ChangedFiles,
AuthorLogin: sql.NullString{String: status.AuthorLogin, Valid: status.AuthorLogin != ""},
AuthorAvatarUrl: sql.NullString{String: status.AuthorAvatarURL, Valid: status.AuthorAvatarURL != ""},
BaseBranch: sql.NullString{String: status.BaseBranch, Valid: status.BaseBranch != ""},
PrNumber: sql.NullInt32{Int32: int32(status.PRNumber), Valid: true},
Commits: sql.NullInt32{Int32: status.Commits, Valid: true},
Approved: sql.NullBool{Bool: status.Approved, Valid: true},
ReviewerCount: sql.NullInt32{Int32: status.ReviewerCount, Valid: true},
RefreshedAt: now,
StaleAt: now.Add(DiffStatusTTL),
}
+9
View File
@@ -140,12 +140,21 @@ func chatDiffStatusFromRow(row database.AcquireStaleChatDiffStatusesRow) databas
Additions: row.Additions,
Deletions: row.Deletions,
ChangedFiles: row.ChangedFiles,
AuthorLogin: row.AuthorLogin,
AuthorAvatarUrl: row.AuthorAvatarUrl,
BaseBranch: row.BaseBranch,
PrNumber: row.PrNumber,
Commits: row.Commits,
Approved: row.Approved,
ReviewerCount: row.ReviewerCount,
RefreshedAt: row.RefreshedAt,
StaleAt: row.StaleAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
GitBranch: row.GitBranch,
GitRemoteOrigin: row.GitRemoteOrigin,
PullRequestTitle: row.PullRequestTitle,
PullRequestDraft: row.PullRequestDraft,
}
}
+7
View File
@@ -630,6 +630,13 @@ type ChatDiffStatus struct {
Additions int32 `json:"additions"`
Deletions int32 `json:"deletions"`
ChangedFiles int32 `json:"changed_files"`
AuthorLogin *string `json:"author_login,omitempty"`
AuthorAvatarURL *string `json:"author_avatar_url,omitempty"`
BaseBranch *string `json:"base_branch,omitempty"`
PRNumber *int32 `json:"pr_number,omitempty"`
Commits *int32 `json:"commits,omitempty"`
Approved *bool `json:"approved,omitempty"`
ReviewerCount *int32 `json:"reviewer_count,omitempty"`
RefreshedAt *time.Time `json:"refreshed_at,omitempty" format:"date-time"`
StaleAt *time.Time `json:"stale_at,omitempty" format:"date-time"`
}
+7
View File
@@ -1190,6 +1190,13 @@ export interface ChatDiffStatus {
readonly additions: number;
readonly deletions: number;
readonly changed_files: number;
readonly author_login?: string;
readonly author_avatar_url?: string;
readonly base_branch?: string;
readonly pr_number?: number;
readonly commits?: number;
readonly approved?: boolean;
readonly reviewer_count?: number;
readonly refreshed_at?: string;
readonly stale_at?: string;
}