Add resolve and reply support

This commit is contained in:
2026-07-28 11:27:20 +02:00
parent fdaee6a69e
commit 0e880fa9b0
8 changed files with 664 additions and 44 deletions

112
github.go
View File

@@ -20,6 +20,11 @@ type GitHubService interface {
GetPullRequest(context.Context, string, string, int) (PRDetails, error)
}
type GitHubWriteService interface {
SetThreadResolved(context.Context, string, bool) (ReviewThread, error)
ReplyToThread(context.Context, string, string) (ReviewComment, error)
}
type GitHubClient struct {
endpoint string
token string
@@ -422,6 +427,38 @@ query CheckAnnotationsPage($id: ID!, $after: String) {
}
}`
const resolveThreadMutation = `
mutation ResolveReviewThread($input: ResolveReviewThreadInput!) {
resolveReviewThread(input: $input) {
thread {
id isResolved isOutdated viewerCanResolve viewerCanUnresolve viewerCanReply path
line originalLine diffSide startLine originalStartLine startDiffSide
}
}
}`
const unresolveThreadMutation = `
mutation UnresolveReviewThread($input: UnresolveReviewThreadInput!) {
unresolveReviewThread(input: $input) {
thread {
id isResolved isOutdated viewerCanResolve viewerCanUnresolve viewerCanReply path
line originalLine diffSide startLine originalStartLine startDiffSide
}
}
}`
const replyToThreadMutation = `
mutation ReplyToReviewThread($input: AddPullRequestReviewThreadReplyInput!) {
addPullRequestReviewThreadReply(input: $input) {
comment {
id body diffHunk createdAt url outdated
line startLine originalLine originalStartLine
originalCommit { oid }
author { login }
}
}
}`
type githubActor struct {
Login string `json:"login"`
Name string `json:"name"`
@@ -1047,6 +1084,63 @@ func (c *GitHubClient) GetPullRequest(ctx context.Context, owner, name string, n
return details, nil
}
func (c *GitHubClient) SetThreadResolved(
ctx context.Context, threadID string, resolved bool,
) (ReviewThread, error) {
query := resolveThreadMutation
variables := map[string]any{"input": map[string]any{"threadId": threadID}}
if resolved {
var data struct {
ResolveReviewThread *struct {
Thread *githubReviewThread
}
}
if err := c.query(ctx, query, variables, &data); err != nil {
return ReviewThread{}, err
}
if data.ResolveReviewThread == nil || data.ResolveReviewThread.Thread == nil {
return ReviewThread{}, errors.New("GitHub returned no resolved review thread")
}
return convertReviewThread(*data.ResolveReviewThread.Thread), nil
}
var data struct {
UnresolveReviewThread *struct {
Thread *githubReviewThread
}
}
if err := c.query(ctx, unresolveThreadMutation, variables, &data); err != nil {
return ReviewThread{}, err
}
if data.UnresolveReviewThread == nil || data.UnresolveReviewThread.Thread == nil {
return ReviewThread{}, errors.New("GitHub returned no unresolved review thread")
}
return convertReviewThread(*data.UnresolveReviewThread.Thread), nil
}
func (c *GitHubClient) ReplyToThread(
ctx context.Context, threadID, body string,
) (ReviewComment, error) {
var data struct {
AddPullRequestReviewThreadReply *struct {
Comment *githubReviewComment
}
}
if err := c.query(ctx, replyToThreadMutation, map[string]any{
"input": map[string]any{
"pullRequestReviewThreadId": threadID,
"body": body,
},
}, &data); err != nil {
return ReviewComment{}, err
}
if data.AddPullRequestReviewThreadReply == nil ||
data.AddPullRequestReviewThreadReply.Comment == nil {
return ReviewComment{}, errors.New("GitHub returned no review reply")
}
return convertReviewComment(*data.AddPullRequestReviewThreadReply.Comment), nil
}
func convertReviewThread(thread githubReviewThread) ReviewThread {
item := ReviewThread{
ID: thread.ID, Path: thread.Path, DiffSide: thread.DiffSide,
@@ -1068,17 +1162,21 @@ func convertReviewThread(thread githubReviewThread) ReviewThread {
item.DiffSide = thread.StartDiffSide
}
for _, comment := range thread.Comments.Nodes {
item.Comments = append(item.Comments, ReviewComment{
ID: comment.ID, Author: actorLogin(comment.Author), Body: comment.Body,
DiffHunk: comment.DiffHunk, CreatedAt: comment.CreatedAt, URL: comment.URL,
Line: intValue(comment.Line), StartLine: intValue(comment.StartLine),
OriginalLine: intValue(comment.OriginalLine), OriginalStartLine: intValue(comment.OriginalStartLine),
OriginalCommitOID: commitOID(comment.OriginalCommit), Outdated: comment.Outdated,
})
item.Comments = append(item.Comments, convertReviewComment(comment))
}
return item
}
func convertReviewComment(comment githubReviewComment) ReviewComment {
return ReviewComment{
ID: comment.ID, Author: actorLogin(comment.Author), Body: comment.Body,
DiffHunk: comment.DiffHunk, CreatedAt: comment.CreatedAt, URL: comment.URL,
Line: intValue(comment.Line), StartLine: intValue(comment.StartLine),
OriginalLine: intValue(comment.OriginalLine), OriginalStartLine: intValue(comment.OriginalStartLine),
OriginalCommitOID: commitOID(comment.OriginalCommit), Outdated: comment.Outdated,
}
}
func actorLogin(actor *githubActor) string {
if actor == nil || actor.Login == "" {
return "[ghost]"