chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The merge box's stack note is for those who can queue, and a flow adds a stack
.changeset/stacked-pulls-order.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# Waiting pulls list as they land | ||
| ||
`knotless queue list` lists pulls waiting for the pull below them lowest first, the order they land in. | ||
crates/knotless/src/queue.rs+20−1
| @@ -190,6 +190,17 @@ pub async fn enqueue(ctx: &Ctx, url: &RepoUrl, pull: &str) -> Result<Place> { | ||
Ok(at) | ||
} | ||
| ||
/// How many pulls `item` waits for. | ||
fn lands_after(items: &[Item], item: &Item) -> usize { | ||
let mut at = item; | ||
let mut seen = vec![item.uri.as_str()]; | ||
while let Some(lower) = below(items, at).filter(|l| !seen.contains(&l.uri.as_str())) { | ||
seen.push(&lower.uri); | ||
at = lower; | ||
} | ||
seen.len() | ||
} | ||
| ||
/// The place of a pull waiting for the pull below it, or of one just | ||
/// queued that the queue reads as out of line: at the back of the branch | ||
/// it lands on. | ||
| @@ -760,7 +771,10 @@ pub fn waiting(items: &[Item], checks: &[Check], record: &RepoRecord, owner: &st | ||
let open = || items.iter().filter(|i| i.state == "open"); | ||
let state = |i: &Item, state: &str| i.queue.as_ref().is_some_and(|q| q.state == state); | ||
let stacked = open().filter(|i| state(i, QUEUED) && !i.draft && below(items, i).is_some()); | ||
waiting.extend(stacked.map(|i| out_of_line(i, stage::STACKED))); | ||
let mut stacked: Vec<&Item> = stacked.collect(); | ||
// In the order they land, as those in line are. | ||
stacked.sort_by_key(|i| lands_after(items, i)); | ||
waiting.extend(stacked.into_iter().map(|i| out_of_line(i, stage::STACKED))); | ||
let ejected = open().filter(|i| state(i, EJECTED)); | ||
waiting.extend(ejected.map(|i| out_of_line(i, ""))); | ||
waiting | ||
| @@ -1238,6 +1252,11 @@ mod tests { | ||
assert_eq!(behind(&items, &top.uri).position, 2); | ||
let all = waiting(&items, &[], &RepoRecord::default(), "did:o"); | ||
let said: Vec<String> = all.iter().map(Waiting::said).collect(); | ||
// However they are read, those waiting are listed as they land. | ||
let backwards = [top.clone(), mid.clone(), low.clone()]; | ||
let read = waiting(&backwards, &[], &RepoRecord::default(), "did:o"); | ||
let order: Vec<&str> = read.iter().map(|w| w.item.title.as_str()).collect(); | ||
assert_eq!(order, ["low", "mid", "top"]); | ||
let stacked = "Waits for the pull below it to land"; | ||
assert_eq!(said[1], stacked); | ||
assert_eq!(said[2], stacked); | ||
harness/e2e/flows.spec.mjs+32−1
| @@ -3,7 +3,7 @@ | ||
// read anew, from the index. | ||
| ||
import { expect, test } from '@playwright/test' | ||
import { world, who, url, records, go, anew, delegate, status, switchTo, comment, editor, choose, logIn, loggedInAs } from './pages.mjs' | ||
import { world, who, url, records, go, anew, knotless, delegate, status, switchTo, comment, editor, choose, logIn, loggedInAs } from './pages.mjs' | ||
| ||
test.describe.configure({ mode: 'serial' }) | ||
| ||
| @@ -165,6 +165,37 @@ test('leaving the merge queue and joining it again', async () => { | ||
await anew(page, (expect) => expect(page.locator('.merge-box').getByRole('button', { name: 'Leave queue' })).toBeVisible()) | ||
}) | ||
| ||
test('adding a stack to the merge queue', async () => { | ||
const fork = 'knotless://bob.test/tally' | ||
const layer = (branch, on) => { | ||
knotless('bob', 'branch', 'create', fork, branch, on) | ||
knotless('bob', 'repo', 'commit', fork, '-b', branch, '-m', `Add ${branch}`, '--put', `${branch}.txt`, '/dev/null') | ||
return knotless('bob', 'pull', 'create', fork, on, fork, branch, `Layer ${branch}`) | ||
} | ||
const low = layer('stack-low', 'main') | ||
const high = layer('stack-high', 'stack-low') | ||
const page_of = (uri) => `/bob.test/tally/pull/${who.bob.did}/${uri.split('/').pop()}` | ||
const box = () => page.locator('.merge-box') | ||
const queued = async () => (await records('bob', 'dev.knotless.queue')).filter((r) => r.value.state === 'queued').map((r) => r.value.subject?.uri ?? r.value.subject) | ||
| ||
await go(page, page_of(high)) | ||
await anew(page, (expect) => expect(box().getByRole('button', { name: 'Add all 2 to merge queue' })).toBeEnabled()) | ||
await box().getByRole('button', { name: 'Add all 2 to merge queue' }).click() | ||
await expect(box().getByRole('button', { name: 'Leave queue' })).toBeVisible() | ||
expect(await queued()).toEqual(expect.arrayContaining([low, high])) | ||
await anew(page, (expect) => expect(page.locator('.merge-box').getByText('Waits for the pull below it to land')).toBeVisible()) | ||
expect(knotless('bob', 'queue', 'list', fork)).toContain(`stacked\t${high}`) | ||
| ||
// The lowest leaves, and the one waiting on it goes too. | ||
await go(page, page_of(low)) | ||
await anew(page, (expect) => expect(box().getByRole('button', { name: 'Leave queue' })).toBeVisible()) | ||
await box().getByRole('button', { name: 'Leave queue' }).click() | ||
await expect(box().getByRole('button', { name: 'Add to merge queue' })).toBeVisible() | ||
await expect.poll(() => knotless('bob', 'queue', 'list', fork)).not.toContain(high) | ||
knotless('bob', 'pull', 'close', fork, high) | ||
knotless('bob', 'pull', 'close', fork, low) | ||
}) | ||
| ||
test('reverting a merged pull', async () => { | ||
await go(page, world.views['pull-ejected']) | ||
await page.getByRole('button', { name: 'Revert' }).click() | ||
web/src/lib/organisms/MergeBox.svelte+1−1
| @@ -149,7 +149,7 @@ | ||
{#if checked}<MergeChecks {checks} {base} standing={status?.checks} {minutes} end={rerun} />{/if} | ||
{/if} | ||
{#if stack && !merged} | ||
<div class="row"><Icon name="stack" /><div class="grow"><b>{`Part of a stack of ${stack.pulls.length}`}</b>{#if under.length && !queued}<p class="mute">{`Add this alone and it lands on ${base}. Add all ${under.length + 1} and they land on ${stack.into ?? base} in turn, lowest first.`}</p>{:else if queued}<p class="mute">Leaving the queue takes any pull waiting for this one with it.</p>{/if}</div></div> | ||
<div class="row"><Icon name="stack" /><div class="grow"><b>{`Part of a stack of ${stack.pulls.length}`}</b>{#if under.length && !queued && status?.may.queue}<p class="mute">{`Add this alone and it lands on ${base}. Add all ${under.length + 1} and they land on ${stack.into ?? base} in turn, lowest first.`}</p>{:else if queued && status?.may.queue}<p class="mute">Leaving the queue takes any pull waiting for this one with it.</p>{/if}</div></div> | ||
<PullStack {stack} current={pull} base={repoHref(repo.owner.handle, repo.name)} /> | ||
{/if} | ||
{#if logged && !merged && status} | ||