fix: resolve pending and jumping

This commit is contained in:
2026-08-03 13:31:45 +02:00
parent 4fcc479779
commit 6f963c7660
9 changed files with 330 additions and 60 deletions

View File

@@ -27,28 +27,30 @@ const (
)
type mutationOperation struct {
ID string `json:"id"`
Kind mutationKind `json:"kind"`
Owner string `json:"owner"`
Repository string `json:"repository"`
Number int `json:"number"`
PRID string `json:"pull_request_id"`
ThreadID string `json:"thread_id,omitempty"`
Body string `json:"body,omitempty"`
Resolved bool `json:"resolved,omitempty"`
Viewer string `json:"viewer,omitempty"`
Original PullRequestMetadata `json:"original,omitempty"`
Update PullRequestMetadata `json:"update,omitempty"`
Permissions ViewerPermissions `json:"permissions"`
ThreadCanReply bool `json:"thread_can_reply,omitempty"`
ThreadCanResolve bool `json:"thread_can_resolve,omitempty"`
ThreadCanUnresolve bool `json:"thread_can_unresolve,omitempty"`
PeopleDone bool `json:"people_done,omitempty"`
Attempted bool `json:"attempted,omitempty"`
Blocked bool `json:"blocked,omitempty"`
Ambiguous bool `json:"ambiguous,omitempty"`
LastError string `json:"last_error,omitempty"`
EnqueuedAt time.Time `json:"enqueued_at"`
ID string `json:"id"`
Kind mutationKind `json:"kind"`
Owner string `json:"owner"`
Repository string `json:"repository"`
Number int `json:"number"`
PRID string `json:"pull_request_id"`
ThreadID string `json:"thread_id,omitempty"`
Body string `json:"body,omitempty"`
Resolved bool `json:"resolved,omitempty"`
Viewer string `json:"viewer,omitempty"`
Original PullRequestMetadata `json:"original,omitempty"`
Update PullRequestMetadata `json:"update,omitempty"`
Permissions ViewerPermissions `json:"permissions"`
ThreadCanReply bool `json:"thread_can_reply,omitempty"`
ThreadCanResolve bool `json:"thread_can_resolve,omitempty"`
ThreadCanUnresolve bool `json:"thread_can_unresolve,omitempty"`
PeopleDone bool `json:"people_done,omitempty"`
Attempted bool `json:"attempted,omitempty"`
AwaitingVerification bool `json:"awaiting_verification,omitempty"`
Unverified bool `json:"unverified,omitempty"`
Blocked bool `json:"blocked,omitempty"`
Ambiguous bool `json:"ambiguous,omitempty"`
LastError string `json:"last_error,omitempty"`
EnqueuedAt time.Time `json:"enqueued_at"`
}
type mutationQueueEnvelope struct {
@@ -235,6 +237,7 @@ type mutationReplayState int
const (
mutationReplayApplied mutationReplayState = iota
mutationReplayVerifying
mutationReplayWaiting
mutationReplayBlocked
)
@@ -303,18 +306,51 @@ func executeQueuedMutation(
operation mutationOperation, details PRDetails,
) mutationReplayMsg {
blocked := func(reason string, ambiguous bool) mutationReplayMsg {
operation.AwaitingVerification = false
operation.Unverified = true
if operation.Kind == mutationPREdit {
operation.PeopleDone = false
}
operation.Blocked, operation.Ambiguous, operation.LastError = true, ambiguous, reason
if err := store.update(operation); err != nil {
return mutationReplayMsg{operation: operation, state: mutationReplayBlocked, details: details, reason: reason, err: err}
}
return mutationReplayMsg{operation: operation, state: mutationReplayBlocked, details: details, reason: reason}
}
verifying := func() mutationReplayMsg {
operation.AwaitingVerification = true
operation.Blocked, operation.Ambiguous, operation.LastError = false, false, ""
if err := store.update(operation); err != nil {
return blocked("could not checkpoint successful mutation for verification", true)
}
return mutationReplayMsg{
operation: operation, state: mutationReplayVerifying, details: details,
}
}
markUnverified := func() *mutationReplayMsg {
if !operation.AwaitingVerification {
return nil
}
operation.AwaitingVerification = false
operation.Unverified = true
if operation.Kind == mutationPREdit {
operation.PeopleDone = false
}
if err := store.update(operation); err != nil {
result := blocked("could not record failed mutation verification", true)
return &result
}
return nil
}
thread := findReviewThread(details.Threads, operation.ThreadID)
switch operation.Kind {
case mutationReply:
if queuedReplyPresent(details, operation) {
return mutationReplayMsg{operation: operation, state: mutationReplayApplied, details: details}
}
if result := markUnverified(); result != nil {
return *result
}
if thread == nil {
return blocked("the review thread no longer exists", false)
}
@@ -339,7 +375,7 @@ func executeQueuedMutation(
if _, err := writer.ReplyToThread(ctx, operation.ThreadID, operation.Body); err != nil {
return mutationReplayMsg{operation: operation, state: mutationReplayWaiting, details: details, err: err}
}
return mutationReplayMsg{operation: operation, state: mutationReplayApplied, details: details}
return verifying()
case mutationResolution:
if thread == nil {
return blocked("the review thread no longer exists", false)
@@ -347,6 +383,9 @@ func executeQueuedMutation(
if thread.IsResolved == operation.Resolved {
return mutationReplayMsg{operation: operation, state: mutationReplayApplied, details: details}
}
if result := markUnverified(); result != nil {
return *result
}
if operation.Resolved && !thread.ViewerCanResolve {
return blocked("GitHub no longer grants resolve permission for this thread", false)
}
@@ -368,8 +407,14 @@ func executeQueuedMutation(
if _, err := writer.SetThreadResolved(ctx, operation.ThreadID, operation.Resolved); err != nil {
return mutationReplayMsg{operation: operation, state: mutationReplayWaiting, details: details, err: err}
}
return mutationReplayMsg{operation: operation, state: mutationReplayApplied, details: details}
return verifying()
case mutationPREdit:
if queuedPREditPresent(details, operation.Update) {
return mutationReplayMsg{operation: operation, state: mutationReplayApplied, details: details}
}
if result := markUnverified(); result != nil {
return *result
}
if !details.Permissions.CanUpdatePR {
return blocked("GitHub no longer grants permission to update this pull request", false)
}
@@ -404,12 +449,18 @@ func executeQueuedMutation(
}
details.Title, details.Body, details.BaseRef = result.Title, result.Body, result.BaseRef
}
return mutationReplayMsg{operation: operation, state: mutationReplayApplied, details: details}
return verifying()
default:
return blocked("queued mutation has an unsupported kind", false)
}
}
func queuedPREditPresent(details PRDetails, update PullRequestMetadata) bool {
return samePRMetadataCore(update, currentMetadata(details)) &&
slices.Equal(normalizedLogins(update.Reviewers), normalizedLogins(details.RequestedReviewers)) &&
slices.Equal(normalizedLogins(update.Assignees), normalizedLogins(details.Assignees))
}
func findReviewThread(threads []ReviewThread, id string) *ReviewThread {
for index := range threads {
if threads[index].ID == id {
@@ -494,30 +545,38 @@ func (m App) projectQueuedMutations(details PRDetails) PRDetails {
}
pendingID := "pending:" + operation.ID
found := false
for _, comment := range thread.Comments {
found = found || comment.ID == pendingID
for index := range thread.Comments {
if thread.Comments[index].ID == pendingID {
thread.Comments[index].Pending = mutationNeedsAttention(operation)
found = true
}
}
if !found {
thread.Comments = append(thread.Comments, ReviewComment{
ID: pendingID, Author: operation.Viewer, Body: operation.Body,
CreatedAt: operation.EnqueuedAt, Pending: true,
CreatedAt: operation.EnqueuedAt, Pending: mutationNeedsAttention(operation),
})
}
case mutationResolution:
if thread := findReviewThread(details.Threads, operation.ThreadID); thread != nil {
thread.IsResolved, thread.Pending = operation.Resolved, true
thread.IsResolved = operation.Resolved
thread.Pending = mutationNeedsAttention(operation)
}
case mutationPREdit:
details.Title, details.Body, details.BaseRef = operation.Update.Title, operation.Update.Body, operation.Update.BaseRef
details.RequestedReviewers = slices.Clone(operation.Update.Reviewers)
details.Assignees = slices.Clone(operation.Update.Assignees)
details.Reviewers = projectRequestedReviewers(details.Reviewers, operation.Update.Reviewers)
details.Pending = true
details.Pending = mutationNeedsAttention(operation)
}
}
return details
}
func mutationNeedsAttention(operation mutationOperation) bool {
return operation.Blocked || operation.Unverified
}
func (m App) projectQueuedPullRequests(prs []PullRequest) []PullRequest {
result := slices.Clone(prs)
for _, operation := range m.mutations.list() {
@@ -527,7 +586,8 @@ func (m App) projectQueuedPullRequests(prs []PullRequest) []PullRequest {
for index := range result {
if result[index].Owner == operation.Owner && result[index].Repository == operation.Repository &&
result[index].Number == operation.Number {
result[index].Title, result[index].Pending = operation.Update.Title, true
result[index].Title = operation.Update.Title
result[index].Pending = mutationNeedsAttention(operation)
}
}
}
@@ -592,7 +652,8 @@ func (m *App) resolveBlockedMutation(choice int) tea.Cmd {
}
return command
case "Retry this mutation now":
operation.Attempted, operation.Blocked = false, false
operation.Attempted, operation.AwaitingVerification = false, false
operation.Unverified, operation.Blocked = false, false
operation.Ambiguous, operation.LastError = false, ""
if err := m.mutations.update(operation); err != nil {
m.err = err