Skip to content

feat(staged): queue branch git actions behind running sessions - #902

Open
matt2e wants to merge 3 commits into
mainfrom
queue-git-actions
Open

feat(staged): queue branch git actions behind running sessions#902
matt2e wants to merge 3 commits into
mainfrom
queue-git-actions

Conversation

@matt2e

@matt2e matt2e commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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_commits return BranchPipelineResponse (sessionId + sessionStatus) so callers can tell a started pipeline from a queued one; web_server.rs mirrors the new shape and now forwards target, so web mode no longer silently downgrades "Rebase onto Origin" to a base rebase.
  • Queue-vs-run decision, dedupe scan, and insert all run under branch_session_launch_lock_for, replacing a racy check-then-insert. Dedupe keys on kind and rebase target.
  • Frontend renders a queued-commit stub ("Queued — waiting for current session…") instead of flashing "Rebasing…" for work that hasn't started.

Phase 2 — push / force push

  • Migration 0021 adds nullable 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.
  • New PipelineKind::Push plus a persisted push_force flag, so a dequeued push re-derives the same command and a queued normal push stays distinct from a queued force push.
  • New GitPipeline schedule kind (exclusive, queue-blocking). Queued pushes drain through start_queued_git_pipeline_for_branch, which rebuilds steps from the branch's current name rather than replaying queued ones.
  • Side effect of the above: a running push now blocks new notes/commits/reviews. Previously it was invisible to the queue and blocked nothing.

Phase 3 — pull

  • pull_branch_ff_only becomes pull_or_queue_branch: runs the direct git op when the branch is idle, otherwise enqueues a PipelineKind::Pull session. Returns the queued session id, or null when the pull ran immediately.
  • build_pull_pipeline_steps does fetch then merge --ff-only origin/<branch>, both aborting rather than handing off to an agent. An aborted pull ends the session in error with the failing step's label and output; an aborted push still completes so the UI can offer force push.

UI

  • pushState gains a queued state and a new pullState store 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.
  • BranchTimeline splits the busy gate into queueableGitActionDisabledReason and immediateGitActionDisabledReason. 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 respect branchSessionBusy.
  • Queued-pull failures surface as toasts from the session-status listener.

Notes for review

  • Session.branch_id and PipelineExecution.push_force are data-model additions, and PipelineKind::Push / Pull are new persisted enum variants — flagging per AGENTS.md.
  • Known gap: the queued badge lives in frontend state only, so a still-queued action drains without the UI badging it after an app restart. Re-clicking hits the backend dedupe and restores the badge.

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_error cases for pull (with and without step output) and push. Frontend: commands.test.ts pins the new response shapes and distinguishes an immediate pull from a queued one.

matt2e added 3 commits August 4, 2026 15:05
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>
@matt2e
matt2e requested review from baxen and wesbillman as code owners August 4, 2026 06:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +576 to +578
if !branch_has_work_in_flight(store, branch_id)? {
return Ok(None);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines 460 to 463
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({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant