Skip to content

Commit 00cc09c

Browse files
Add mq next for commit and push waits
Co-authored-by: Andrew <andrewxhill@gmail.com>
1 parent 5e51150 commit 00cc09c

8 files changed

Lines changed: 507 additions & 5 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ For branch-oriented repair without a submission id:
123123
mq rebase --repo /path/to/topic-worktree --branch feature/login --json
124124
```
125125

126+
If you only need the next protected-main advance, use:
127+
128+
```bash
129+
mq next --json
130+
mq next commit --json
131+
```
132+
126133
For agent-heavy and factory-style repos, set `[publish].Mode = 'auto'` in
127134
`mainline.toml` unless there is a real operator reason to keep publish manual.
128135
That file is the runtime config authority for the repo. The SQLite store keeps

docs/FLOWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ Use `submission_id` plus `mq wait --submission ...` as the normal follow path.
4949
Do not use sleeps, branch-name polling, `mq logs`, `mq events`, or `mq watch`
5050
as the primary completion path.
5151

52+
If you do not care which submission lands next and only need the next protected
53+
`main` advance:
54+
55+
```bash
56+
mq next --json
57+
mq next commit --json
58+
```
59+
60+
- `mq next` / `mq next push` waits for the next successful publish
61+
- `mq next commit` waits for the next local protected-branch integration
62+
5263
## Worktree-Heavy Repo
5364

5465
Keep one canonical protected-branch worktree and many topic worktrees:

docs/JSON_CONTRACTS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,40 @@ For machine consumers, prefer the explicit queue booleans to distinguish cases
347347
like “a publish is actively running while an older submission is separately
348348
blocked.”
349349

350+
## `mq next --json`
351+
352+
Returns one JSON object for the next protected-branch advance observed after the
353+
command starts.
354+
355+
Stable fields:
356+
357+
- `repository_root`
358+
- `protected_branch`
359+
- `target`
360+
- `protected_sha`
361+
- `commit_subject`
362+
- `committed_at`
363+
- `queue_state`
364+
- `queue_length`
365+
- `has_blocked_submissions`
366+
- `has_running_publishes`
367+
- `has_running_submissions`
368+
- `has_queued_work`
369+
- `observed_at`
370+
371+
Optional fields:
372+
373+
- `submission_id`
374+
- `publish_request_id`
375+
- `branch`
376+
- `source_ref`
377+
- `ref_kind`
378+
379+
`target` is:
380+
381+
- `push` for `mq next` / `mq next push`
382+
- `commit` for `mq next commit`
383+
350384
## `mq blocked --json`
351385

352386
Returns one JSON object with:

internal/app/app_test.go

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ func TestCLIHelp(t *testing.T) {
4949
if !strings.Contains(stdout.String(), "wait --submission 42 --for landed --json --timeout 30m") {
5050
t.Fatalf("expected wait guidance in help, got %q", stdout.String())
5151
}
52+
if !strings.Contains(stdout.String(), "next --json") {
53+
t.Fatalf("expected next guidance in help, got %q", stdout.String())
54+
}
5255
}
5356

5457
func TestMQHelpUsesMQIdentity(t *testing.T) {
@@ -147,6 +150,122 @@ func TestGlobalJSONCompletionReportsStructuredScript(t *testing.T) {
147150
}
148151
}
149152

153+
func TestNextCommitReportsNextIntegratedProtectedAdvance(t *testing.T) {
154+
repoRoot, _ := createTestRepoWithRemote(t)
155+
initRepoForWorker(t, repoRoot)
156+
157+
featurePath := filepath.Join(t.TempDir(), "feature-next-commit")
158+
runTestCommand(t, repoRoot, "git", "worktree", "add", "-b", "feature/next-commit", featurePath)
159+
writeFileAndCommit(t, featurePath, "next-commit.txt", "next commit\n", "next commit feature")
160+
featureHead := trimNewline(runTestCommand(t, featurePath, "git", "rev-parse", "HEAD"))
161+
162+
resultCh := make(chan nextResult, 1)
163+
errCh := make(chan error, 1)
164+
go func() {
165+
var stdout bytes.Buffer
166+
var stderr bytes.Buffer
167+
err := runNext([]string{"commit", "--repo", repoRoot, "--json", "--timeout", "10s", "--poll-interval", "10ms"}, newStepPrinter(&stdout), &stderr)
168+
if err != nil {
169+
errCh <- err
170+
return
171+
}
172+
var result nextResult
173+
if unmarshalErr := json.Unmarshal(stdout.Bytes(), &result); unmarshalErr != nil {
174+
errCh <- unmarshalErr
175+
return
176+
}
177+
resultCh <- result
178+
}()
179+
180+
time.Sleep(100 * time.Millisecond)
181+
submitBranch(t, featurePath)
182+
runOnce(t, repoRoot)
183+
184+
select {
185+
case err := <-errCh:
186+
t.Fatalf("runNext returned error: %v", err)
187+
case result := <-resultCh:
188+
if result.Target != nextTargetCommit {
189+
t.Fatalf("expected commit target, got %+v", result)
190+
}
191+
if result.ProtectedSHA != featureHead {
192+
t.Fatalf("expected protected sha %q, got %+v", featureHead, result)
193+
}
194+
if result.CommitSubject != "next commit feature" {
195+
t.Fatalf("expected commit subject, got %+v", result)
196+
}
197+
if result.Branch != "feature/next-commit" {
198+
t.Fatalf("expected branch in result, got %+v", result)
199+
}
200+
if result.QueueState == "" {
201+
t.Fatalf("expected queue state summary, got %+v", result)
202+
}
203+
case <-time.After(12 * time.Second):
204+
t.Fatal("timed out waiting for mq next commit result")
205+
}
206+
}
207+
208+
func TestNextPushReportsNextPublishedProtectedAdvance(t *testing.T) {
209+
repoRoot, remoteDir := createTestRepoWithRemote(t)
210+
initRepoForWorker(t, repoRoot)
211+
updatePublishMode(t, repoRoot, "auto")
212+
213+
featurePath := filepath.Join(t.TempDir(), "feature-next-push")
214+
runTestCommand(t, repoRoot, "git", "worktree", "add", "-b", "feature/next-push", featurePath)
215+
writeFileAndCommit(t, featurePath, "next-push.txt", "next push\n", "next push feature")
216+
featureHead := trimNewline(runTestCommand(t, featurePath, "git", "rev-parse", "HEAD"))
217+
218+
resultCh := make(chan nextResult, 1)
219+
errCh := make(chan error, 1)
220+
go func() {
221+
var stdout bytes.Buffer
222+
var stderr bytes.Buffer
223+
err := runNext([]string{"--repo", repoRoot, "--json", "--timeout", "10s", "--poll-interval", "10ms"}, newStepPrinter(&stdout), &stderr)
224+
if err != nil {
225+
errCh <- err
226+
return
227+
}
228+
var result nextResult
229+
if unmarshalErr := json.Unmarshal(stdout.Bytes(), &result); unmarshalErr != nil {
230+
errCh <- unmarshalErr
231+
return
232+
}
233+
resultCh <- result
234+
}()
235+
236+
time.Sleep(100 * time.Millisecond)
237+
submitBranch(t, featurePath)
238+
runOnce(t, repoRoot)
239+
runOnce(t, repoRoot)
240+
241+
select {
242+
case err := <-errCh:
243+
t.Fatalf("runNext returned error: %v", err)
244+
case result := <-resultCh:
245+
if result.Target != nextTargetPush {
246+
t.Fatalf("expected push target, got %+v", result)
247+
}
248+
if result.ProtectedSHA != featureHead {
249+
t.Fatalf("expected protected sha %q, got %+v", featureHead, result)
250+
}
251+
if result.CommitSubject != "next push feature" {
252+
t.Fatalf("expected commit subject, got %+v", result)
253+
}
254+
if result.PublishRequestID == 0 {
255+
t.Fatalf("expected publish request id, got %+v", result)
256+
}
257+
if result.QueueState == "" {
258+
t.Fatalf("expected queue state summary, got %+v", result)
259+
}
260+
remoteHead := trimNewline(runTestCommand(t, remoteDir, "git", "rev-parse", "refs/heads/main"))
261+
if remoteHead != featureHead {
262+
t.Fatalf("expected remote head %q, got %q", featureHead, remoteHead)
263+
}
264+
case <-time.After(12 * time.Second):
265+
t.Fatal("timed out waiting for mq next push result")
266+
}
267+
}
268+
150269
func TestGlobalJSONForwardsToRunOnceAndPublish(t *testing.T) {
151270
repoRoot, _ := createTestRepoWithRemote(t)
152271
initRepoForWorker(t, repoRoot)
@@ -1254,7 +1373,7 @@ func TestCLIAcceptsSubcommandFlagsForPlannedCommands(t *testing.T) {
12541373
if !strings.Contains(output, "complete -F _mainline_completions mainline") {
12551374
t.Fatalf("expected completion script output, got %q", output)
12561375
}
1257-
if !strings.Contains(output, "land submit status confidence run-once wait rebase blocked retry cancel publish") {
1376+
if !strings.Contains(output, "land submit status next confidence run-once wait rebase blocked retry cancel publish") {
12581377
t.Fatalf("expected completion script to include rebase and blocked workflow, got %q", output)
12591378
}
12601379
if !strings.Contains(output, "--repo --branch --sha --worktree --requested-by --priority --allow-newer-head --json --check --check-only --queue-only --wait --for --timeout --poll-interval") {

internal/app/cli.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func newCLICommandTree(programName string, stdout *stepPrinter, stderr io.Writer
7070
newLegacyCommand("land", "submit and wait for integrate plus publish", true, stdout, stderr, runLand),
7171
newLegacyCommand("submit", "queue a topic worktree or detached sha", true, stdout, stderr, runSubmit),
7272
newLegacyCommand("status", "show queue and protected-branch state", true, stdout, stderr, runStatus),
73+
newLegacyCommand("next", "wait for the next protected-main commit or push", true, stdout, stderr, runNext),
7374
newLegacyCommand("confidence", "summarize evidence and promotion gates", true, stdout, stderr, runConfidence),
7475
newLegacyCommand("run-once", "run one integration or publish cycle", true, stdout, stderr, runRunOnce),
7576
newLegacyCommand("wait", "wait on a submission id for integration or landed outcome", true, stdout, stderr, runWait),
@@ -196,6 +197,7 @@ Turbo paths:
196197
%s land --json --timeout 30m
197198
%s submit --wait --for landed --timeout 30m --json
198199
%s wait --submission 42 --for landed --json --timeout 30m
200+
%s next --json
199201
%s events --follow --json --lifecycle --repo /path/to/repo-root
200202
201203
Operator:
@@ -212,6 +214,7 @@ Commands:
212214
land submit and wait for integrate plus publish
213215
submit queue a topic worktree or detached sha
214216
status show queue and protected-branch state
217+
next wait for the next protected-main commit or push
215218
confidence summarize evidence and promotion gates
216219
run-once run one integration or publish cycle
217220
wait wait on a submission id for integration or landed outcome
@@ -234,7 +237,7 @@ Commands:
234237
repo show inspect repo config and worktrees
235238
236239
Use "%s <command> --help" for command-specific examples.
237-
`, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName)
240+
`, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName)
238241
}
239242

240243
func appendJSONFlag(args []string) []string {

internal/app/completion.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ _mainline_completions()
6262
_init_completion || return
6363
6464
if [[ ${cword} -eq 1 ]]; then
65-
COMPREPLY=( $(compgen -W "land submit status confidence run-once wait rebase blocked retry cancel publish logs watch events doctor completion version config repo registry" -- "$cur") )
65+
COMPREPLY=( $(compgen -W "land submit status next confidence run-once wait rebase blocked retry cancel publish logs watch events doctor completion version config repo registry" -- "$cur") )
6666
return
6767
fi
6868
@@ -96,6 +96,9 @@ _mainline_completions()
9696
status)
9797
COMPREPLY=( $(compgen -W "--repo --json --events" -- "$cur") )
9898
;;
99+
next)
100+
COMPREPLY=( $(compgen -W "commit push --repo --json --timeout --poll-interval" -- "$cur") )
101+
;;
99102
confidence)
100103
COMPREPLY=( $(compgen -W "--repo --json --events --soak-summary --cert-report" -- "$cur") )
101104
;;
@@ -172,6 +175,7 @@ _mainline() {
172175
commands=(
173176
'submit:queue a source worktree'
174177
'land:submit and wait for integrate plus publish'
178+
'next:wait for the next protected-main commit or push'
175179
'wait:wait on a durable submission id'
176180
'rebase:rebase a topic branch onto protected main'
177181
'blocked:list blocked submissions and recovery actions'
@@ -240,6 +244,10 @@ _mainline() {
240244
_arguments '--repo[repository path]:path:_files -/' '--submission[submission id]:id:' '--for[wait target]:target:(integrated landed)' '--json[json output]' '--timeout[maximum wait time]:duration:' '--poll-interval[wait interval between worker checks]:duration:'
241245
return
242246
;;
247+
next)
248+
_arguments '1:target:(commit push)' '--repo[repository path]:path:_files -/' '--json[json output]' '--timeout[maximum wait time]:duration:' '--poll-interval[poll interval while waiting for events]:duration:'
249+
return
250+
;;
243251
rebase)
244252
_arguments '--repo[repository path]:path:_files -/' '--submission[blocked submission id]:id:' '--branch[branch name]:branch:' '--worktree[source worktree override]:path:_files -/' '--json[json output]'
245253
return
@@ -308,8 +316,8 @@ _mainline "$@"
308316
}
309317

310318
func fishCompletionScript() string {
311-
return `complete -c mainline -f -n "__fish_use_subcommand" -a "land submit status confidence run-once wait retry cancel publish logs watch events doctor completion version config repo registry"
312-
complete -c mq -f -n "__fish_use_subcommand" -a "land submit status confidence run-once wait retry cancel publish logs watch events doctor completion version config repo registry"
319+
return `complete -c mainline -f -n "__fish_use_subcommand" -a "land submit status next confidence run-once wait retry cancel publish logs watch events doctor completion version config repo registry"
320+
complete -c mq -f -n "__fish_use_subcommand" -a "land submit status next confidence run-once wait retry cancel publish logs watch events doctor completion version config repo registry"
313321
314322
complete -c mainline -f -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from init show audit root" -a "init show audit root"
315323
complete -c mq -f -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from init show audit root" -a "init show audit root"
@@ -325,6 +333,14 @@ complete -c mainline -l repo
325333
complete -c mq -l repo
326334
complete -c mainline -n "__fish_seen_subcommand_from status doctor repo show repo audit repo root" -l json
327335
complete -c mq -n "__fish_seen_subcommand_from status doctor repo show repo audit repo root" -l json
336+
complete -c mainline -n "__fish_seen_subcommand_from next" -l json
337+
complete -c mq -n "__fish_seen_subcommand_from next" -l json
338+
complete -c mainline -f -n "__fish_seen_subcommand_from next; and not __fish_seen_subcommand_from commit push" -a "commit push"
339+
complete -c mq -f -n "__fish_seen_subcommand_from next; and not __fish_seen_subcommand_from commit push" -a "commit push"
340+
complete -c mainline -n "__fish_seen_subcommand_from next" -l timeout
341+
complete -c mq -n "__fish_seen_subcommand_from next" -l timeout
342+
complete -c mainline -n "__fish_seen_subcommand_from next" -l poll-interval
343+
complete -c mq -n "__fish_seen_subcommand_from next" -l poll-interval
328344
complete -c mainline -n "__fish_seen_subcommand_from doctor" -l fix
329345
complete -c mq -n "__fish_seen_subcommand_from doctor" -l fix
330346
complete -c mainline -n "__fish_seen_subcommand_from logs events" -l json

0 commit comments

Comments
 (0)