Hold a task's merge while another task in the project is merging

This commit is contained in:
Ian Gulliver
2026-10-15 12:11:35 -07:00
parent ff22f33025
commit 606cf4b5c0
16 changed files with 546 additions and 190 deletions
+98 -17
View File
@@ -268,6 +268,9 @@ UPDATE tasks SET port_base = (
SELECT 41000 + 10 * (count(*) - 1) FROM tasks AS earlier
WHERE earlier.status = 'active' AND (earlier.created_at < tasks.created_at OR (earlier.created_at = tasks.created_at AND earlier.id <= tasks.id))
) WHERE status = 'active' AND port_base = 0;
`}, {"add-task-merge-queue", `
ALTER TABLE tasks ADD COLUMN merge_queued_by TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN merge_queued_at TEXT NOT NULL DEFAULT '';
`}}
const (
@@ -570,21 +573,7 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, actor string) error
}
func (s *Store) ListTasks(ctx context.Context, projectID string) ([]api.Task, error) {
rows, err := s.db.QueryContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
tasks := []api.Task{}
for rows.Next() {
t, err := scanTask(rows)
if err != nil {
return nil, err
}
tasks = append(tasks, t)
}
return tasks, rows.Err()
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, projectID)
}
func (s *Store) GetTask(ctx context.Context, id string) (api.Task, error) {
@@ -633,6 +622,90 @@ func (s *Store) TakeMergingBy(ctx context.Context, id string) (string, error) {
return login, err
}
func (s *Store) MergingTasks(ctx context.Context) ([]api.Task, error) {
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE status = ? AND merging_by != ''`, string(api.TaskStatusActive))
}
func (s *Store) QueuedMerges(ctx context.Context, projectID string) ([]api.Task, error) {
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? AND status = ? AND merge_queued_by != '' ORDER BY merge_queued_at`,
projectID, string(api.TaskStatusActive))
}
func (s *Store) ProjectsWithQueuedMerges(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT project_id FROM tasks WHERE status = ? AND merge_queued_by != ''`, string(api.TaskStatusActive))
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) queryTasks(ctx context.Context, query string, args ...any) ([]api.Task, error) {
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
tasks := []api.Task{}
for rows.Next() {
t, err := scanTask(rows)
if err != nil {
return nil, err
}
tasks = append(tasks, t)
}
return tasks, rows.Err()
}
func (s *Store) QueueMerge(ctx context.Context, id, login string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_queued_by = ?, merge_queued_at = ? WHERE id = ? AND status = ? AND merge_queued_by = '' RETURNING project_id`,
login, now(), id, string(api.TaskStatusActive)).Scan(&projectID)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_queued", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id}})
})
}
func (s *Store) TakeQueuedMerge(ctx context.Context, id string) (string, error) {
login := ""
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `SELECT project_id, merge_queued_by FROM tasks WHERE id = ?`, id).Scan(&projectID, &login)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
if login == "" {
return nil
}
_, err = tx.ExecContext(ctx, `UPDATE tasks SET merge_queued_by = '', merge_queued_at = '' WHERE id = ?`, id)
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_unqueued", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}})
})
return login, err
}
func (s *Store) MergeTask(ctx context.Context, id, commit, pushed, actor string) (api.Task, error) {
return s.closeTask(ctx, id, api.TaskStatusMerged, commit, actor, "task.merged", map[string]any{"id": id, "commit": commit, "pushed": pushed})
}
@@ -1007,7 +1080,7 @@ func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) er
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, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at`
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
@@ -1035,7 +1108,8 @@ func scanTask(row scanner) (api.Task, error) {
todoItem := ""
forkedFrom := ""
forkedAt := 0
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt)
mergeQueuedAt := ""
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt)
if err != nil {
return t, err
}
@@ -1064,6 +1138,13 @@ func scanTask(row scanner) (api.Task, error) {
t.ForkedFrom = &forkedFrom
t.ForkedAt = &forkedAt
}
if mergeQueuedAt != "" {
qt, err := parseTime(mergeQueuedAt)
if err != nil {
return t, err
}
t.MergeQueuedAt = &qt
}
return t, nil
}