Offer "After review" in the Merge dialog while a review runs

This commit is contained in:
Ian Gulliver
2026-10-16 22:34:14 -07:00
parent c2d0ff77d7
commit b28968a0bc
25 changed files with 835 additions and 292 deletions
+94 -3
View File
@@ -456,6 +456,10 @@ UPDATE tasks SET state = ` + taskStateExpr + `;
ALTER TABLE tasks ADD COLUMN finalizing_commit TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN finalizing_notes TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN finalizing_unchanged INTEGER NOT NULL DEFAULT 0;
`}, {"add-task-merge-after-review", `
ALTER TABLE tasks ADD COLUMN merge_after_review_by TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN merge_after_review_id TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN merge_after_review_target TEXT NOT NULL DEFAULT '';
`}}
const (
@@ -1082,6 +1086,93 @@ func (s *Store) TakeQueuedMerge(ctx context.Context, id string) (string, error)
return login, err
}
type MergeAfterReview struct {
By string
ReviewID string
Target api.MergeTaskJSONBodyTarget
}
func (s *Store) SetMergeAfterReview(ctx context.Context, id string, mar MergeAfterReview) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
snapshotN := 0
err := tx.QueryRowContext(ctx, `
UPDATE tasks SET merge_after_review_by = ?, merge_after_review_id = ?, merge_after_review_target = ?
WHERE id = ? AND status = ? AND discard_state = '' AND merging_by = '' AND finalizing = '' AND merge_queued_by = '' AND parent_merge_by = ''
AND EXISTS (SELECT 1 FROM reviews WHERE reviews.id = ? AND reviews.task_id = tasks.id AND reviews.state = ?)
RETURNING project_id, (SELECT snapshot_n FROM reviews WHERE reviews.id = ?)`,
mar.By, mar.ReviewID, string(mar.Target), id, string(api.TaskStatusActive), mar.ReviewID, string(api.ReviewStateRunning), mar.ReviewID).Scan(&projectID, &snapshotN)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_after_review", ProjectId: &projectID, TaskId: &id, Actor: mar.By, Payload: map[string]any{"id": id, "review_id": mar.ReviewID, "snapshot_n": snapshotN, "target": string(mar.Target)}})
})
}
func (s *Store) CancelMergeAfterReview(ctx context.Context, id, actor string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_after_review_by = '', merge_after_review_id = '', merge_after_review_target = '' WHERE id = ? AND merge_after_review_by != '' RETURNING project_id`, id).Scan(&projectID)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_after_review_cancelled", ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: map[string]any{"id": id}})
})
}
func (s *Store) FinishedMergeAfterReviews(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT tasks.merge_after_review_id FROM tasks JOIN reviews ON reviews.id = tasks.merge_after_review_id
WHERE tasks.merge_after_review_by != '' AND reviews.state != ?`, string(api.ReviewStateRunning))
if err != nil {
return nil, err
}
defer rows.Close()
ids := []string{}
for rows.Next() {
id := ""
err = rows.Scan(&id)
if err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
func (s *Store) TakeMergeAfterReview(ctx context.Context, id, reviewID string) (MergeAfterReview, error) {
mar := MergeAfterReview{}
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID, target := "", ""
err := tx.QueryRowContext(ctx, `SELECT project_id, merge_after_review_by, merge_after_review_id, merge_after_review_target FROM tasks WHERE id = ?`, id).
Scan(&projectID, &mar.By, &mar.ReviewID, &target)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
mar.Target = api.MergeTaskJSONBodyTarget(target)
if mar.By == "" || (reviewID != "" && mar.ReviewID != reviewID) {
mar = MergeAfterReview{}
return nil
}
_, err = tx.ExecContext(ctx, `UPDATE tasks SET merge_after_review_by = '', merge_after_review_id = '', merge_after_review_target = '' WHERE id = ?`, id)
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_after_review_ended", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}})
})
return mar, err
}
func (s *Store) SetTaskSummary(ctx context.Context, id, summary, actor string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
@@ -1160,7 +1251,7 @@ func (s *Store) DiscardTask(ctx context.Context, id, actor string) (api.Task, er
func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus, commit, actor, kind string, payload map[string]any) (api.Task, error) {
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
res, err := tx.ExecContext(ctx, `UPDATE tasks SET status = ?, closed_at = ?, merge_commit = ?, finalizing = '', finalizing_commit = '', finalizing_notes = '', finalizing_unchanged = 0 WHERE id = ? AND status = ?`,
res, err := tx.ExecContext(ctx, `UPDATE tasks SET status = ?, closed_at = ?, merge_commit = ?, finalizing = '', finalizing_commit = '', finalizing_notes = '', finalizing_unchanged = 0, merge_after_review_by = '', merge_after_review_id = '', merge_after_review_target = '' WHERE id = ? AND status = ?`,
string(status), now(), commit, id, string(api.TaskStatusActive))
if err != nil {
return err
@@ -1749,7 +1840,7 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error {
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
@@ -1784,7 +1875,7 @@ func scanTask(row scanner) (api.Task, error) {
parentTaskID := ""
parentMergeBy := ""
taskState := ""
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq)
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq)
if err != nil {
return t, err
}