feat(staged): queue branch git actions behind running sessions - #902
feat(staged): queue branch git actions behind running sessions#902matt2e wants to merge 3 commits into
Conversation
Rebase and Squash were disabled whenever the branch had a queued or
running session ("Session in progress"), even though the backend already
knew how to enqueue them: `start_or_queue_commit_pipeline_for_branch`
creates a queued session plus pending-commit artifact that
`drain_queued_sessions_for_branch` picks up in FIFO order. Only the
frontend stood in the way. Phase 1 of the queue-git-actions plan removes
that block and makes the queued state legible.
Backend:
- `rebase_branch`/`squash_commits` now return `BranchPipelineResponse`
(`sessionId` + `sessionStatus`) instead of a bare session id, so the
caller can tell a started pipeline from a queued one. Mirrored in the
`web_server.rs` dispatch arms.
- The queue-vs-run decision, the dedupe scan, and the insert all run
under `branch_session_launch_lock_for`, replacing a racy
check-then-insert. Clicking Rebase twice now reuses the queued session
rather than stacking a second one. Dedupe keys on kind *and* persisted
rebase target so "onto base" and "onto origin" stay distinct.
- `web_server.rs` also forwards `target`, so web mode no longer silently
downgrades "Rebase onto Origin" to a base rebase.
Frontend:
- Dropped `branchSessionBusy` from the `startBranchCommandPipeline`
guard and from `branchCommandDisabledReason`; identity warnings
(detached HEAD, wrong branch) still disable both actions.
- A queued response now renders a `queued-commit` stub labelled with the
pipeline name and "Queued — waiting for current session…", matching the
persisted row instead of flashing "Rebasing…" for work that hasn't
started.
- `BranchTimeline`'s `rebaseBranchDisabledReason` gated push, force-push,
and reset-to-origin rather than rebase. Renamed it to
`immediateGitActionDisabledReason` and fed it a reason that keeps the
busy check, so those three stay blocked while sessions are in flight —
their queueing is Phases 2 and 3, and their handlers still guard on
`branchSessionBusy`. Without the split, removing the busy arm would
have made them look clickable while silently doing nothing.
- `BranchCardActionsBar`'s `newCommitDisabled` only ever gated the
Rebase/Squash menu items, so it is now `rebaseSquashDisabled`.
Tests: seven `prs.rs` cases cover run-when-idle, queue-when-busy, the
queued label and pending-commit link, repeat-click dedupe, per-kind and
per-target separation, and a queued pipeline alone keeping the branch
busy. A `commands.test.ts` case pins the new return shapes. `just
fmt-check`, `just lint`, `just typecheck`, `just test` (559), and
`pnpm test` (482) all pass.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 2 of the queued git actions plan: push and force push now join the per-branch session queue instead of being disabled while the branch is busy, and a push in flight blocks other branch work the way a commit does. Backend: - Migration 0021 adds nullable `sessions.branch_id`. Push pipelines create no commit/note/review, so the artifact joins could not see them; `get_queued_sessions_for_branch`, `has_running_session_for_branch` and the branch/project resolvers now also match on that column. - `PipelineKind::Push` plus a persisted `push_force` flag, so a queued push re-derives the same command on dequeue and a queued normal push is distinguishable from a queued force push. - New `GitPipeline` schedule kind: exclusive and queue-blocking. Queued pushes drain through `start_queued_git_pipeline_for_branch`, which rebuilds the steps from the branch's current name rather than replaying the queued ones. - `push_branch` returns `BranchPipelineResponse` (running vs queued); queue-vs-run, dedupe on `(kind, push_force)`, and the insert all happen under the branch launch lock, mirroring rebase/squash. - Bonus fix: because a running push resolves to a `GitPipeline` schedule, it now blocks new notes/commits/reviews. Previously a push was invisible to the queue and blocked nothing. Frontend: - `pushState` gains a `queued` state; the PR button and the git-push / diverged timeline rows show it and cancel the queued session on click. The queued entry flips to `pushing` on the drain's "running" event, with the 5s poller as a fallback. - Push and force-push no longer check `branchSessionBusy` — the backend owns that decision. Reset to origin still does: it is validated against a point-in-time preview of what would be discarded, so it stays immediate-only, as does discard changes. `BranchTimeline` therefore takes a separate `queueableGitActionDisabledReason` alongside the existing `immediateGitActionDisabledReason`. Known gap: the queued badge lives only in the frontend store, so after an app restart a still-queued push drains without the UI showing it as queued first. Also note that per AGENTS.md the new `Session.branch_id` and `PipelineExecution.push_force` fields are data-model additions that want human review; they are the shape the plan note specified. Tests: migration schema version/column, branch-linked queued and running push queries, drain-scan blocking and FIFO ordering for pushes, queue-vs-run and dedupe for push vs force push, step rebuilding, and the new `pushBranch` response shape. Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 3 of the branch git-action queue: pull now follows the same start-or-queue path as rebase/squash (Phase 1) and push/force-push (Phase 2). When the branch is idle the pull still runs as an instant direct git operation; when the branch has work in flight it enqueues a `PipelineKind::Pull` session that the branch drainer runs headlessly in FIFO order. Backend: - Add `PipelineKind::Pull` and route it through `PipelineBranchLink::Branch` (no commit artifact), so it schedules as `BranchSessionScheduleKind::GitPipeline`. - Replace the `pull_branch_ff_only` command with `pull_or_queue_branch` in `prs.rs`, which takes the branch launch lock, dedupes against an existing queued pull, and otherwise falls through to `pull_branch_ff_only_impl`. It returns the queued session id, or null when the pull ran immediately. - Generalize `start_queued_git_pipeline_for_branch` over push/pull and add `build_pull_pipeline_steps` (fetch, then `merge --ff-only origin/<branch>`, both aborting rather than handing off to an agent). - Surface drained-pull failures: an aborted pull now ends the session in `error` with the failing step's label and output, while an aborted push still completes so the UI can offer force push. Frontend: - Add a `pullState` store mirroring `pushState`, badge the origin-ahead timeline row with "Pull queued", and turn its button into Cancel while queued. - Loosen the dirty-worktree gate: pull moves to `queueableGitActionDisabledReason`, so agent-transient dirt no longer disables it while the branch is busy. - Toast queued-pull failures from the session-status listener, and teach the session registry / labels about the `pull` session type. Tests: pull coverage in the git-pipeline scheduling tests, new `aborted_pipeline_error` unit tests (pull with and without step output, push unaffected), and a `commands.test.ts` case distinguishing an immediate pull from a queued one. Data-model note for review: `PipelineKind::Pull` is a new persisted enum variant. The queued badge lives in frontend state only, matching Phase 2, so a badge is lost across an app restart (re-clicking Pull hits the backend dedupe and restores it). Signed-off-by: Matt Toohey <contact@matttoohey.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b037fce1b7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if !branch_has_work_in_flight(store, branch_id)? { | ||
| return Ok(None); | ||
| } |
There was a problem hiding this comment.
Guard idle pipeline starts under the branch lock
When the branch is idle, this returns None and drops the launch lock before the caller creates the running session. Two concurrent git-action requests against the same branch can both pass this check, both create running pipelines, and mutate the same worktree/remote concurrently instead of queueing; the regular branch-session path avoids this by rechecking and inserting the running row under the same lock.
Useful? React with 👍 / 👎.
| case 'originAhead': { | ||
| const disabledReason = pullDisabledReason(state); | ||
| // A queued pull keeps the button live so it can cancel the queued session. | ||
| const disabledReason = pullQueuedOrigin ? undefined : pullDisabledReason(state); | ||
| rows.push({ |
There was a problem hiding this comment.
Keep queued pulls visible after the branch diverges
The queued-pull cancel affordance is only rendered while the cached git state is originAhead. If a pull is queued behind a commit/rebase and that earlier work creates a local commit before the pull drains, the branch becomes diverged while pullQueuedOrigin is still true, so the queued pull disappears from the UI and cannot be cancelled before it later runs into the expected --ff-only failure.
Useful? React with 👍 / 👎.
Rebase, squash, push, force-push, and pull were all disabled whenever a branch had a queued or running session ("Session in progress"). This lands all three phases of the queue-git-actions plan: those actions now join the per-branch session queue instead of being blocked, and they drain headlessly in FIFO order.
Phase 1 — rebase / squash
rebase_branch/squash_commitsreturnBranchPipelineResponse(sessionId+sessionStatus) so callers can tell a started pipeline from a queued one;web_server.rsmirrors the new shape and now forwardstarget, so web mode no longer silently downgrades "Rebase onto Origin" to a base rebase.branch_session_launch_lock_for, replacing a racy check-then-insert. Dedupe keys on kind and rebase target.queued-commitstub ("Queued — waiting for current session…") instead of flashing "Rebasing…" for work that hasn't started.Phase 2 — push / force push
sessions.branch_id. Push pipelines create no commit/note/review, so the artifact joins could not see them; the branch queue/resolver queries now also match on that column.PipelineKind::Pushplus a persistedpush_forceflag, so a dequeued push re-derives the same command and a queued normal push stays distinct from a queued force push.GitPipelineschedule kind (exclusive, queue-blocking). Queued pushes drain throughstart_queued_git_pipeline_for_branch, which rebuilds steps from the branch's current name rather than replaying queued ones.Phase 3 — pull
pull_branch_ff_onlybecomespull_or_queue_branch: runs the direct git op when the branch is idle, otherwise enqueues aPipelineKind::Pullsession. Returns the queued session id, or null when the pull ran immediately.build_pull_pipeline_stepsdoes fetch thenmerge --ff-only origin/<branch>, both aborting rather than handing off to an agent. An aborted pull ends the session inerrorwith the failing step's label and output; an aborted push still completes so the UI can offer force push.UI
pushStategains aqueuedstate and a newpullStatestore mirrors it; the PR button and the git-push / diverged / origin-ahead timeline rows show "Queued" and cancel the queued session on click. Queued entries flip to running on the drain's "running" event, with the 5s poller as fallback.BranchTimelinesplits the busy gate intoqueueableGitActionDisabledReasonandimmediateGitActionDisabledReason. Reset-to-origin and discard-changes stay immediate-only (both validate against a point-in-time preview of what would be discarded), so they still respectbranchSessionBusy.Notes for review
Session.branch_idandPipelineExecution.push_forceare data-model additions, andPipelineKind::Push/Pullare new persisted enum variants — flagging per AGENTS.md.Tests
Rust: run-when-idle / queue-when-busy, queued label + pending-commit link, repeat-click dedupe, per-kind and per-target separation, migration schema version/column, branch-linked queued and running push queries, drain-scan blocking and FIFO ordering, step rebuilding, and
aborted_pipeline_errorcases for pull (with and without step output) and push. Frontend:commands.test.tspins the new response shapes and distinguishes an immediate pull from a queued one.