add reviewer assigning

This commit is contained in:
2026-07-29 09:49:51 +02:00
parent a82f5e7e9f
commit 1d90e364ee
17 changed files with 1475 additions and 47 deletions

102
github.go
View File

@@ -30,6 +30,12 @@ type GitHubPullRequestWriteService interface {
UpdatePullRequest(context.Context, string, PullRequestMetadata) (PullRequestMetadata, error)
}
type GitHubPullRequestPeopleWriteService interface {
UpdatePullRequestPeople(
context.Context, string, string, int, PullRequestPeopleUpdate,
) (PullRequestPeople, error)
}
type GitHubMergeService interface {
SetPullRequestAutoMerge(context.Context, string, string, string, bool) (*AutoMergeRequest, error)
MergePullRequest(context.Context, string, string, string) (PullRequestMergeResult, error)
@@ -39,6 +45,10 @@ type GitHubBranchService interface {
ListBranches(context.Context, string, string) ([]RepositoryBranch, error)
}
type GitHubRepositoryPeopleService interface {
ListRepositoryUsers(context.Context, string, string) ([]RepositoryUser, error)
}
type GitHubEnrichmentService interface {
EnrichPullRequest(context.Context, PRDetails) PRDetailsEnrichment
}
@@ -464,7 +474,10 @@ query PullRequestDetails($owner: String!, $name: String!, $number: Int!) {
}
}
author { login }
assignees(first: 20) { nodes { login } }
assignees(first: 100) {
pageInfo { hasNextPage endCursor }
nodes { id login name }
}
labels(first: 20) { nodes { name } }
milestone { title }
additions deletions changedFiles
@@ -565,6 +578,18 @@ query TimelinePage($owner: String!, $name: String!, $number: Int!, $after: Strin
}
}`
const assigneesPageQuery = `
query AssigneesPage($owner: String!, $name: String!, $number: Int!, $after: String) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
assignees(first: 100, after: $after) {
pageInfo { hasNextPage endCursor }
nodes { id login name }
}
}
}
}`
const checkContextsPageQuery = `
query CheckContextsPage($id: ID!, $after: String) {
node(id: $id) {
@@ -722,6 +747,11 @@ type githubPRCommentConnection struct {
Nodes []githubPRComment `json:"nodes"`
}
type githubUserConnection struct {
PageInfo githubPageInfo `json:"pageInfo"`
Nodes []githubActor `json:"nodes"`
}
type githubReviewSummary struct {
ID, Body, State, URL string
SubmittedAt time.Time
@@ -819,10 +849,8 @@ type githubPullRequestDetails struct {
Position, EstimatedTimeToMerge int
EnqueuedAt time.Time
}
Assignees struct {
Nodes []githubActor `json:"nodes"`
}
Labels struct {
Assignees githubUserConnection
Labels struct {
Nodes []struct {
Name string `json:"name"`
} `json:"nodes"`
@@ -954,6 +982,36 @@ func (c *GitHubClient) allConversationComments(
return nodes, nil
}
func (c *GitHubClient) allAssignees(
ctx context.Context, owner, name string, number int, connection githubUserConnection,
) ([]githubActor, error) {
nodes := append([]githubActor(nil), connection.Nodes...)
for pages := 0; connection.PageInfo.HasNextPage; pages++ {
if pages >= 100 {
return nil, errors.New("assignee pagination exceeded 100 pages")
}
var data struct {
Repository *struct {
PullRequest *struct {
Assignees githubUserConnection `json:"assignees"`
} `json:"pullRequest"`
} `json:"repository"`
}
if err := c.query(ctx, assigneesPageQuery, map[string]any{
"owner": owner, "name": name, "number": number,
"after": connection.PageInfo.EndCursor,
}, &data); err != nil {
return nil, fmt.Errorf("load more assignees: %w", err)
}
if data.Repository == nil || data.Repository.PullRequest == nil {
return nil, errors.New("pull request disappeared while loading assignees")
}
connection = data.Repository.PullRequest.Assignees
nodes = append(nodes, connection.Nodes...)
}
return nodes, nil
}
func (c *GitHubClient) allReviewSummaries(
ctx context.Context, owner, name string, number int, connection githubReviewSummaryConnection,
) ([]githubReviewSummary, error) {
@@ -1112,22 +1170,30 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
node := data.Repository.PullRequest
var (
threadNodes []githubReviewThread
assigneeNodes []githubActor
conversationNodes []githubPRComment
reviewNodes []githubReviewSummary
timelineNodes []githubTimelineNode
checkNodes []githubCheckContext
threadErr error
assigneeErr error
conversationErr error
reviewErr error
timelineErr error
checkErr error
wait sync.WaitGroup
)
wait.Add(4)
wait.Add(5)
go func() {
defer wait.Done()
threadNodes, threadErr = c.allReviewThreads(ctx, owner, name, number, node.ReviewThreads)
}()
go func() {
defer wait.Done()
assigneeNodes, assigneeErr = c.allAssignees(
ctx, owner, name, number, node.Assignees,
)
}()
go func() {
defer wait.Done()
conversationNodes, conversationErr = c.allConversationComments(ctx, owner, name, number, node.Comments)
@@ -1152,6 +1218,9 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
if threadErr != nil {
threadNodes = append([]githubReviewThread(nil), node.ReviewThreads.Nodes...)
}
if assigneeErr != nil {
assigneeNodes = append([]githubActor(nil), node.Assignees.Nodes...)
}
if conversationErr != nil {
conversationNodes = append([]githubPRComment(nil), node.Comments.Nodes...)
}
@@ -1186,6 +1255,7 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
Permissions: ViewerPermissions{
Repository: data.Repository.ViewerPermission,
CanUpdatePR: node.ViewerCanUpdate, CanReact: node.ViewerCanReact,
CanAssign: viewerCanAssign(data.Repository.ViewerPermission),
CanSubscribe: node.ViewerCanSubscribe, CanEnableMerge: node.ViewerCanEnableAutoMerge,
CanDisableMerge: node.ViewerCanDisableAutoMerge,
},
@@ -1211,7 +1281,8 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
}
for component, err := range map[string]error{
"review threads": threadErr, "conversation": conversationErr,
"submitted reviews": reviewErr, "timeline": timelineErr, "checks": checkErr,
"submitted reviews": reviewErr, "assignees": assigneeErr,
"timeline": timelineErr, "checks": checkErr,
} {
if err != nil {
details.DataIssues = append(details.DataIssues, DataIssue{
@@ -1260,7 +1331,7 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
if node.Milestone != nil {
details.Milestone = node.Milestone.Title
}
for _, assignee := range node.Assignees.Nodes {
for _, assignee := range assigneeNodes {
details.Assignees = append(details.Assignees, assignee.Login)
}
reviewers := map[string]string{}
@@ -1269,6 +1340,11 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
if login != "" {
reviewers[login] = "REVIEW_REQUESTED"
}
if request.RequestedReviewer.Login != "" {
details.RequestedReviewers = append(
details.RequestedReviewers, request.RequestedReviewer.Login,
)
}
}
for _, review := range node.LatestReviews.Nodes {
if review.Author != nil {
@@ -1279,6 +1355,7 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
details.Reviewers = append(details.Reviewers, Reviewer{Login: login, State: state})
}
sort.Slice(details.Reviewers, func(i, j int) bool { return details.Reviewers[i].Login < details.Reviewers[j].Login })
sort.Strings(details.RequestedReviewers)
if len(node.Commits.Nodes) > 0 && node.Commits.Nodes[0].Commit.StatusCheckRollup != nil {
rollup := node.Commits.Nodes[0].Commit.StatusCheckRollup
details.CheckState = rollup.State
@@ -1646,6 +1723,15 @@ func actorLogin(actor *githubActor) string {
return actor.Login
}
func viewerCanAssign(permission string) bool {
switch strings.ToUpper(permission) {
case "TRIAGE", "WRITE", "MAINTAIN", "ADMIN":
return true
default:
return false
}
}
func intValue(value *int) int {
if value == nil {
return 0