~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The merge box's stack note is for those who can queue, and a flow adds a stack

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:stack/followup
.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}