diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bae8b..19b9ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- add `Last Reviewer` PR field, who left the most recent submitted review; refreshed on open PRs too + ## v0.4.0 (2026-09-23) - add `Reviewed At` PR field, the date of the most recent submitted review; refreshed on open PRs too diff --git a/cli/cmds.go b/cli/cmds.go index 9ef46c6..f52fa6e 100644 --- a/cli/cmds.go +++ b/cli/cmds.go @@ -74,7 +74,7 @@ to override the default field list, and --dry-run to preview.`, PreRunE: ValidateParams([]string{"token", "project-owner", "project-number"}), RunE: CmdPRsRefresh, } - refreshCmd.Flags().Bool("include-open", false, "also refresh open prs, limited to fields safe without full pr data (PR#, User, Created At, Open Days, Reviewed By, Approved By, Changes Requested By, Reviewed At)") + refreshCmd.Flags().Bool("include-open", false, "also refresh open prs, limited to fields safe without full pr data (PR#, User, Created At, Open Days, Reviewed By, Approved By, Changes Requested By, Reviewed At, Last Reviewer)") if err := viper.BindPFlag("include-open", refreshCmd.Flags().Lookup("include-open")); err != nil { return nil, fmt.Errorf("error binding 'include-open' flag: %w", err) } diff --git a/cli/cmds_prs_fields.go b/cli/cmds_prs_fields.go index c31f9f2..4df8994 100644 --- a/cli/cmds_prs_fields.go +++ b/cli/cmds_prs_fields.go @@ -209,6 +209,15 @@ var PRFields = map[string]PRFieldDef{ return ctx.PR.ReviewedAt.Format(time.RFC3339) }, }, + "Last Reviewer": { + Type: gh.ItemValueTypeText, + ComputeFn: func(ctx PRFieldContext) any { + if ctx.PR.LastReviewer == "" { + return nil // never reviewed + } + return ctx.PR.LastReviewer + }, + }, "Filtered Review Count": { Type: gh.ItemValueTypeNumber, ComputeFn: func(ctx PRFieldContext) any { diff --git a/cli/cmds_prs_refresh.go b/cli/cmds_prs_refresh.go index b6c8a79..65265ed 100644 --- a/cli/cmds_prs_refresh.go +++ b/cli/cmds_prs_refresh.go @@ -14,13 +14,13 @@ import ( // prRefreshDefaultFields are the fields refreshed by default for closed/merged PRs. Fields needing // data we don't fetch here (review counts, waiting days) are excluded so we don't overwrite real // values with zeros; use --pr-populate-fields to override. -var prRefreshDefaultFields = []string{"Status", "PR#", "User", "Open Days", "Created At", "Closed At", "Merged At", "Merged By", "Reviewed By", "Approved By", "Changes Requested By", "Reviewed At", "CI", "Mergeable"} +var prRefreshDefaultFields = []string{"Status", "PR#", "User", "Open Days", "Created At", "Closed At", "Merged At", "Merged By", "Reviewed By", "Approved By", "Changes Requested By", "Reviewed At", "Last Reviewer", "CI", "Mergeable"} // prRefreshOpenFields are the fields safe to refresh on open PRs with --include-open: the REST // lookup can't see the review decision, so Status would incorrectly knock "Approved" PRs back // to "Waiting", and waiting/count data isn't available at all. CI/Mergeable are fetched // separately via GraphQL when needed. -var prRefreshOpenFields = map[string]bool{"PR#": true, "User": true, "Created At": true, "Open Days": true, "Reviewed By": true, "Approved By": true, "Changes Requested By": true, "Reviewed At": true, "CI": true, "Mergeable": true} +var prRefreshOpenFields = map[string]bool{"PR#": true, "User": true, "Created At": true, "Open Days": true, "Reviewed By": true, "Approved By": true, "Changes Requested By": true, "Reviewed At": true, "Last Reviewer": true, "CI": true, "Mergeable": true} // CmdPRsRefresh walks the project board itself and refreshes fields on PR items that are now // closed or merged, rather than syncing PRs from a repo. This catches PRs that were added while @@ -67,7 +67,7 @@ func CmdPRsRefresh(_ *cobra.Command, _ []string) error { needReviews, needChangesRequested, needMergeable := false, false, false for _, fieldName := range prFields { - if fieldName == "Reviewed By" || fieldName == "Approved By" || fieldName == "Changes Requested By" || fieldName == "Reviewed At" { + if fieldName == "Reviewed By" || fieldName == "Approved By" || fieldName == "Changes Requested By" || fieldName == "Reviewed At" || fieldName == "Last Reviewer" { needReviews = true } if fieldName == "Changes Requested By" { @@ -198,6 +198,7 @@ func CmdPRsRefresh(_ *cobra.Command, _ []string) error { login := review.GetUser().GetLogin() if review.GetState() != "PENDING" && review.GetSubmittedAt().After(pr.ReviewedAt) { pr.ReviewedAt = review.GetSubmittedAt().Time + pr.LastReviewer = login } switch review.GetState() { case "APPROVED": diff --git a/lib/gh/pr_gql.go b/lib/gh/pr_gql.go index 84d8a8d..1274575 100644 --- a/lib/gh/pr_gql.go +++ b/lib/gh/pr_gql.go @@ -34,6 +34,7 @@ type PullRequest struct { MergedAt time.Time MergedBy string ReviewedAt time.Time // when the most recent submitted review (any state except pending) was left, zero when unreviewed + LastReviewer string // who left that review Draft bool Milestone string Mergeable string // MERGEABLE, CONFLICTING, or UNKNOWN (github may still be computing) @@ -271,6 +272,7 @@ func (q pullRequestsQuery) flatten(reviewers map[string]struct{}) []PullRequest for _, review := range pullRequest.Reviews.Nodes { if review.State != string(githubv4.PullRequestReviewStatePending) && review.SubmittedAt.After(pr.ReviewedAt) { pr.ReviewedAt = review.SubmittedAt + pr.LastReviewer = review.Author.Login } // requesters ordered by their first change request, comment counts summed across all of them