diff --git a/docs/releases-maintenance.md b/docs/releases-maintenance.md index ccd7e54a6b..47066baafb 100644 --- a/docs/releases-maintenance.md +++ b/docs/releases-maintenance.md @@ -50,6 +50,34 @@ CONSOLE_BUMP=patch scripts/bump-objectui.sh # force the bump type scripts/bump-objectui.sh --no-changeset # opt out (rarely) ``` +#### The pin must be a commit on objectui `main` (#10495) + +`bump-objectui.sh` pins `git rev-parse HEAD` of your objectui checkout, which +answers *what is checked out* — never *is it on main*. Bump with a feature branch +checked out and you pin a revision that is not on main. So the bump now asks the +question and **warns** — it does not refuse: + +* **On `origin/main`** — the pin line says `(on origin/main)` and nothing else is + printed. "Checked, and fine" is stated, not inferred from silence. +* **Not on `origin/main`** — a loud warning naming the branch(es) the commit *is* + on, and saying which situation it is: pushed onto a branch that never merged, or + never pushed at all. The pin is still written; deciding is yours. +* **Cannot be answered** — no `origin/main` in the checkout, or the commit object + is absent — it says *that*, and never borrows the wording of either verdict. + (`git merge-base --is-ancestor` exits **128** on an absent object: an error, not + a "no". And `git rev-parse HEAD` exits 0 for a commit whose object is missing, + so the pin arriving is no proof the object is there.) + +It is a warning rather than a gate on purpose: `origin/main` is only as fresh as +your last fetch, so a hard failure here would reject a legitimately-just-merged +commit, and this script stays usable offline. It never fetches for you — it prints +the `git fetch origin main` you may want. The hard gate lives at the chokepoint +that must fail closed: `.github/workflows/cut-rc.yml` re-asks the same question +against a fresh full clone and **refuses to cut** (#9450). Until an RC is cut, +though, a bad pin merges and `pnpm sdui:manifest` below would ratchet +spec↔registry parity against a tree that is not on main — which is why the +producer half warns at all. + #### After the pin moves: run the declaration-parity ratchet (#5960) The bump has a second half, and it is not optional: diff --git a/scripts/bump-objectui.sh b/scripts/bump-objectui.sh index 5cbe2a1997..664c550d45 100755 --- a/scripts/bump-objectui.sh +++ b/scripts/bump-objectui.sh @@ -14,6 +14,14 @@ # # on-demand gate by decision, never a CI job. Needs # # Playwright chromium. This script prints the reminder. # +# The pin must name a commit that is on objectui MAIN. This script WARNS — it +# does not refuse — when the revision being pinned is not reachable from the +# objectui checkout's `origin/main`, and names the branch(es) it IS on. It never +# fetches for you: `origin/main` is only as fresh as your last fetch and this +# script stays usable offline. The release cut re-asks the same question against +# a fresh full clone and REFUSES to cut (#9450). Three answers, never two — a +# checkout that cannot answer the question says so rather than guessing (#10495). +# # Env: # CONSOLE_BUMP=major|minor|patch # force the changeset bump type (default: auto — # # the HIGHEST level objectui itself declared in the @@ -122,15 +130,188 @@ else NEW_SHA="$(git -C "$OBJECTUI_ROOT" rev-parse HEAD)" fi +# --- Is the revision we are about to pin actually ON objectui main? (#10495) -- +# `rev-parse HEAD` answers "what is checked out", never "is it on main". Bump +# with a feature branch checked out — or pass a branch name — and the pin names +# a revision that is not on main, and this script used to report success. +# Measured on a fresh `--no-tags` clone of objectui, 2026-08-21: 941 remote +# branches, 118 branch tips not reachable from `main`, 291 commits present and +# not on main. Any of those is one `rev-parse HEAD` away from being the pin. +# +# A WARNING, NOT A GATE — deliberately, and the split is with #9450 / PR #10494: +# `.github/workflows/cut-rc.yml` now runs this same predicate against a fresh +# full clone and refuses to cut. That is the chokepoint that must fail closed. +# Here it must not: `origin/main` in a local checkout is only as fresh as the +# last fetch, so hard-failing would reject a legitimately-just-merged commit, +# and this script is deliberately usable offline and on a machine that cannot +# run the full procedure (same rationale as `print_sdui_next_step`). So: say it +# loudly, name the branch it IS on, and let the operator decide. Never fetch on +# the operator's behalf — unlike the `--unshallow` deepen below, which repairs +# an input this script is about to DERIVE from, a fetch here would only change +# the answer to a question the operator is being asked to judge. +# +# THREE ANSWERS, NOT TWO. `merge-base --is-ancestor` exits 0 (ancestor), 1 (not +# an ancestor) and **128 on an absent object** — an error, not a verdict. +# Measured, git 2.43.0: +# git merge-base --is-ancestor origin/main +# fatal: Not a valid commit name -> exit 128 +# Reading 128 as "not an ancestor" invents a false alarm; reading anything that +# is not 1 as "fine" reinstates exactly the silent pass this check exists to +# remove. A checkout with no `origin/main` cannot answer the question at all. +# So the exits are split three ways, and the third says what it could not +# determine and why — it never borrows the wording of either verdict. +# +# The absent-object case is reachable from the DEFAULT path, not just from an +# explicit argument. Measured, git 2.43.0: `git rev-parse HEAD` exits **0** and +# prints the sha even when that commit object is missing from the object store — +# it resolves the ref, it does not read the object. So NEW_SHA arriving here is +# not proof the object is present, and presence is asked as its own question. +REACH_TAG="" # parenthetical for the one line that is printed anyway +REACH_RECALL="" # tail recall, so a warning cannot scroll out of the run + +# Render at most 10 ref names. A cap that fires SAYS SO, with the real count — +# a capped list that reads as complete is this file's other standing lesson +# (CONSOLE_CHANGES_MAX, #4731). +reach_ref_list() { + local n="$#" + if (( n == 0 )); then printf 'none'; return 0; fi + local out + out="$(printf '%s, ' "${@:1:10}")" + out="${out%, }" + if (( n > 10 )); then out="${out} … (+$(( n - 10 )) more; ${n} total)"; fi + printf '%s' "$out" +} + +report_objectui_reachability() { + local sha="$1" + local short="${sha:0:12}" + local rc=0 + + # Q1 — can this checkout answer the question AT ALL? Asked first because the + # later questions are meaningless without it: with no origin/main, + # `--is-ancestor` would report "the pin left main", which is this check's own + # overclaim wearing a new message. + if ! git -C "$OBJECTUI_ROOT" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then + REACH_TAG=" (reachability UNKNOWN)" + REACH_RECALL=" — ⚠️ pin reachability UNKNOWN, see above" + { + echo + echo "⚠️ COULD NOT DETERMINE whether ${short} is on objectui main." + echo " ${OBJECTUI_ROOT} has no 'origin/main' ref, so the question cannot be asked" + echo " in this checkout at all." + echo " This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered," + echo " and the pin is being written anyway." + echo " To make it answerable here: git -C ${OBJECTUI_ROOT} fetch origin main" + echo " (not run for you — this script stays usable offline.)" + echo " The release cut re-asks it against a fresh full clone and fails closed (#9450)." + echo + } >&2 + return 0 + fi + + # Q2 — is the object even present? `--is-ancestor` cannot return a verdict for + # an object it cannot read; it exits 128 there, and 128 must not be read as + # "no". Asked separately so the report can name WHICH failure this was. + if ! git -C "$OBJECTUI_ROOT" cat-file -e "${sha}^{commit}" 2>/dev/null; then + REACH_TAG=" (reachability UNKNOWN)" + REACH_RECALL=" — ⚠️ pin reachability UNKNOWN, see above" + { + echo + echo "⚠️ COULD NOT DETERMINE whether ${short} is on objectui main." + echo " The commit object is NOT PRESENT in ${OBJECTUI_ROOT}" + echo " ('git cat-file -e ${short}^{commit}' exits 128). A ref can name a commit" + echo " whose object is missing — 'git rev-parse' resolves the ref without reading" + echo " the object — so this is reachable even from a plain HEAD bump." + echo " 'merge-base --is-ancestor' exits 128 here too: an error, not a verdict." + echo " This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered." + echo " Fetch or repair the objectui object store, then re-run this bump." + echo + } >&2 + return 0 + fi + + # Q3 — the verdict. 0 / 1 / anything else, distinguished on purpose. + git -C "$OBJECTUI_ROOT" merge-base --is-ancestor "$sha" origin/main 2>/dev/null || rc=$? + + if [[ "$rc" -eq 0 ]]; then + # Reachable. No banner — but the line that prints anyway says so, so that + # "checked, and fine" is stated rather than inferred from silence. + REACH_TAG=" (on origin/main)" + return 0 + fi + + if [[ "$rc" -ne 1 ]]; then + REACH_TAG=" (reachability UNKNOWN)" + REACH_RECALL=" — ⚠️ pin reachability UNKNOWN, see above" + { + echo + echo "⚠️ COULD NOT DETERMINE whether ${short} is on objectui main." + echo " 'git merge-base --is-ancestor ${short} origin/main' exited ${rc}; only 0" + echo " (is an ancestor) and 1 (is not) are verdicts. Anything else is an error," + echo " and is being reported as one rather than folded into either answer." + echo " This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered." + echo + } >&2 + return 0 + fi + + # rc == 1: a real verdict, and the one this card exists for. + REACH_TAG=" (NOT on origin/main)" + REACH_RECALL=" — ⚠️ pin is NOT on objectui origin/main, see above" + + # Enumerated only on this branch: with ~941 branches a --contains walk is not + # free, and the healthy path must not pay for it. + local -a local_refs=() remote_refs=() + mapfile -t local_refs < <( + git -C "$OBJECTUI_ROOT" for-each-ref --contains "$sha" --format='%(refname:short)' refs/heads 2>/dev/null || true + ) + mapfile -t remote_refs < <( + git -C "$OBJECTUI_ROOT" for-each-ref --contains "$sha" --format='%(refname:short)' refs/remotes 2>/dev/null || true + ) + + local main_tip + main_tip="$(git -C "$OBJECTUI_ROOT" log -1 --format='%h, committed %cr' origin/main 2>/dev/null || echo 'unknown')" + + { + echo + echo "⚠️ objectui pin ${short} is NOT reachable from origin/main in" + echo " ${OBJECTUI_ROOT} — it names a revision that is not on objectui main." + echo " on local branches : $(reach_ref_list ${local_refs[@]+"${local_refs[@]}"})" + echo " on remote branches: $(reach_ref_list ${remote_refs[@]+"${remote_refs[@]}"})" + # Which of the two situations this is, said out loud, so the operator does + # not have to go and look: a branch that never merged and a commit that was + # never pushed take different remedies. + if (( ${#remote_refs[@]} > 0 )); then + echo " → It IS pushed, but only onto branch(es) that have not merged into main." + elif (( ${#local_refs[@]} > 0 )); then + echo " → It is on a LOCAL branch only — this commit has not been pushed to objectui." + else + echo " → No branch in this checkout contains it (detached HEAD on a discarded commit?)." + fi + echo " Or your origin/main is simply STALE: it points at ${main_tip}." + echo " Refresh it with: git -C ${OBJECTUI_ROOT} fetch origin main" + echo " (not run for you — this script stays usable offline, and the answer to" + echo " this question is yours to judge.)" + echo " Pinning it anyway. Downstream: the release cut re-asks this against a fresh" + echo " full clone and REFUSES to cut (#9450), so a bad pin surfaces at RC time at" + echo " the earliest; until then 'pnpm sdui:manifest' would ratchet spec↔registry" + echo " declaration parity against a tree that is not on main (ADR-0082 D4)." + echo + } >&2 + return 0 +} + +report_objectui_reachability "$NEW_SHA" + OLD_SHA="$(cat "${FRAMEWORK_ROOT}/.objectui-sha" 2>/dev/null | tr -d '[:space:]' || echo '')" if [[ "$OLD_SHA" == "$NEW_SHA" ]]; then - echo "→ Already at ${NEW_SHA:0:12}, nothing to do." + echo "→ Already at ${NEW_SHA:0:12}${REACH_TAG}, nothing to do." exit 0 fi echo "$NEW_SHA" > "${FRAMEWORK_ROOT}/.objectui-sha" -echo "→ objectui pin: ${OLD_SHA:0:12} → ${NEW_SHA:0:12}" +echo "→ objectui pin: ${OLD_SHA:0:12} → ${NEW_SHA:0:12}${REACH_TAG}" SHORT="${NEW_SHA:0:12}" SUBJECT_LINE="$(git -C "$OBJECTUI_ROOT" log -1 --format=%s "$NEW_SHA")" @@ -299,7 +480,7 @@ print_sdui_next_step() { } if [[ "$NO_COMMIT" -eq 1 ]]; then - echo "→ --no-commit: leaving files unstaged." + echo "→ --no-commit: leaving files unstaged${REACH_RECALL}." print_sdui_next_step exit 0 fi @@ -311,5 +492,5 @@ git -C "$FRAMEWORK_ROOT" commit -m "chore: bump objectui to ${SHORT} ${SUBJECT_LINE} objectui@${NEW_SHA}" -- .objectui-sha ${CS_FILE:+"$CS_FILE"} -echo "✓ Committed. Push with: git push" +echo "✓ Committed${REACH_RECALL}. Push with: git push" print_sdui_next_step diff --git a/scripts/objectui-changeset-digest.mjs b/scripts/objectui-changeset-digest.mjs index 46f57fd715..feed8aa4e1 100644 --- a/scripts/objectui-changeset-digest.mjs +++ b/scripts/objectui-changeset-digest.mjs @@ -1190,6 +1190,12 @@ function selfTest() { '.github/workflows/y.yml': 'on: push\n', }); const head = g('rev-parse', 'HEAD').trim(); + // A real objectui checkout has a remote-tracking main, and since #10495 the + // bump asks whether the revision it is pinning is reachable from it. Without + // this ref every drive below would take the "cannot answer" path and bury + // the assertions it is here to make under reachability warnings — a fixture + // that is unrealistic in exactly the dimension a later reader would trust. + g('update-ref', 'refs/remotes/origin/main', head); // --- a "framework" root without pre.json (launch window, no RC) --- const fwPlain = join(tmp, 'fw-plain'); @@ -2232,6 +2238,9 @@ function selfTest() { const c6sink = commit6('feat(core): third — the future sink (#4003)', cs6(3)); const c6four = commit6('feat(core): fourth (#4004)', cs6(4)); const c6to = commit6('feat(core): fifth (#4005)', cs6(5)); + // As above (#10495): keep this fixture's truncation assertions independent + // of the reachability report by giving it the ref a real checkout has. + g6('update-ref', 'refs/remotes/origin/main', c6to); const shallowFile = join(ui6, '.git', 'shallow'); const whole = buildDigest({ @@ -2471,6 +2480,218 @@ function selfTest() { // truncated tree by accident. rmSync(shallowFile, { force: true }); + // ---- #10495: is the revision being PINNED actually on objectui main? ---- + // `bump-objectui.sh` pins `git rev-parse HEAD` of the operator's checkout. + // That answers "what is checked out", never "is it on main". Measured on a + // fresh --no-tags clone of objectui, 2026-08-21: 941 remote branches, 118 + // branch tips not reachable from main, 291 commits present and not on main. + // The release cut re-asks the question and fails closed (#9450 / PR #10494), + // but only at RC time — so in between a non-main pin merges and + // `pnpm sdui:manifest` ratchets spec↔registry parity against it. + // + // The producer half is a WARNING, not a gate: the script is deliberately + // usable offline and `origin/main` is only as fresh as the last fetch. So + // every case below asserts what it SAYS, and that it still exits 0 and + // still writes the pin. Three answers are pinned, never two — "cannot + // answer" must not borrow the wording of either verdict. + const mkFramework = (name, pinSha) => { + const dir = join(tmp, name); + mkdirSync(join(dir, 'scripts'), { recursive: true }); + mkdirSync(join(dir, '.changeset'), { recursive: true }); + writeFileSync(join(dir, '.objectui-sha'), `${pinSha}\n`); + for (const f of ['bump-objectui.sh', 'objectui-changeset-digest.mjs', 'invoked-as.mjs']) { + writeFileSync(join(dir, 'scripts', f), readFileSync(join(__dirname, f), 'utf8')); + } + return dir; + }; + // Offline by construction — a self-test must never reach the network. + const runBump = (fwDir, uiRoot, args) => + spawnSync('bash', [join(fwDir, 'scripts', 'bump-objectui.sh'), ...args], { + encoding: 'utf8', + env: { + ...process.env, + OBJECTUI_ROOT: uiRoot, + OBJECTUI_NO_DEEPEN: '1', + GIT_TERMINAL_PROMPT: '0', + }, + }); + const mkUi = (name) => { + const dir = join(tmp, name); + mkdirSync(join(dir, '.changeset'), { recursive: true }); + const gg = (...args) => git(dir, args); + gg('init', '-q', '-b', 'main'); + gg('config', 'user.email', 'selftest@objectstack.ai'); + gg('config', 'user.name', 'self test'); + gg('config', 'commit.gpgsign', 'false'); + let n = 0; + const c = (subject) => { + n += 1; + writeFileSync(join(dir, `src${n}.ts`), `${n}\n`); + writeFileSync( + join(dir, '.changeset', `entry-${n}.md`), + `---\n"@object-ui/core": patch\n---\n\n${subject}\n`, + ); + gg('add', '-A'); + gg('commit', '-q', '-m', subject); + return gg('rev-parse', 'HEAD').trim(); + }; + return { dir, gg, c }; + }; + + const uiR = mkUi('objectui-reach'); + const rBase = uiR.c('chore: base'); + const rMain = uiR.c('feat(core): the commit main really has'); + uiR.gg('update-ref', 'refs/remotes/origin/main', rMain); + uiR.gg('checkout', '-q', '-b', 'feature/pushed-never-merged'); + const rPushed = uiR.c('feat(core): on a pushed branch that never merged'); + uiR.gg('update-ref', 'refs/remotes/origin/feature/pushed-never-merged', rPushed); + uiR.gg('checkout', '-q', 'main'); + uiR.gg('checkout', '-q', '-b', 'wip/local-only'); + const rLocal = uiR.c('feat(core): never pushed anywhere'); + uiR.gg('checkout', '-q', 'main'); + + // R1 — ON main. The control: a check that warns about everything proves + // nothing, and a bump on the healthy path must stay quiet. + const r1 = runBump(mkFramework('fw-reach-main', rBase), uiR.dir, ['--no-commit', rMain]); + check( + '#10495 R1 a pin that IS on main raises no warning of any kind', + r1.status === 0 && + !r1.stderr.includes('COULD NOT DETERMINE') && + !r1.stderr.includes('is NOT reachable from origin/main'), + `status=${r1.status}\nSTDERR:${r1.stderr}`, + ); + check( + '#10495 R1b … and the line that prints anyway STATES the answer, so "checked and fine" is not inferred from silence', + r1.stdout.includes('(on origin/main)'), + r1.stdout, + ); + + // R2 — pushed, but on a branch that never merged. The 118-branch-tip case. + const fwPushed = mkFramework('fw-reach-pushed', rBase); + const r2 = runBump(fwPushed, uiR.dir, ['--no-commit', rPushed]); + check( + '#10495 R2 a pin that is NOT on main warns, and NAMES the branch it is on', + r2.stderr.includes('is NOT reachable from origin/main') && + r2.stderr.includes('origin/feature/pushed-never-merged'), + r2.stderr, + ); + check( + '#10495 R2b the warning separates "wrong branch" from "not pushed yet" — the operator does not have to go and look', + r2.stderr.includes('It IS pushed, but only onto branch(es) that have not merged') && + !r2.stderr.includes('has not been pushed to objectui'), + r2.stderr, + ); + check( + '#10495 R2c a warning is not a refusal: exit 0, the pin is written, and the summary line carries the verdict', + r2.status === 0 && + readFileSync(join(fwPushed, '.objectui-sha'), 'utf8').trim() === rPushed && + r2.stdout.includes('(NOT on origin/main)'), + `status=${r2.status}\nSTDOUT:${r2.stdout}`, + ); + check( + '#10495 R2d the warning is recalled on the LAST line of the run, so it cannot scroll away', + r2.stdout.includes('⚠️ pin is NOT on objectui origin/main, see above'), + r2.stdout, + ); + + // R3 — the card's own shape, driven through the FRONT DOOR: no argument at + // all, `git rev-parse HEAD`, with a feature branch checked out. + uiR.gg('checkout', '-q', 'wip/local-only'); + const r3 = runBump(mkFramework('fw-reach-local', rBase), uiR.dir, ['--no-commit']); + uiR.gg('checkout', '-q', 'main'); + check( + '#10495 R3 a plain `bump-objectui.sh` with a feature branch checked out warns — the defect, through the default path', + r3.status === 0 && + r3.stderr.includes('is NOT reachable from origin/main') && + r3.stderr.includes(rLocal.slice(0, 12)), + `status=${r3.status}\nSTDERR:${r3.stderr}`, + ); + check( + '#10495 R3b a LOCAL-only commit is reported as never pushed, not as an unmerged branch', + r3.stderr.includes('wip/local-only') && + r3.stderr.includes('on remote branches: none') && + r3.stderr.includes('has not been pushed to objectui') && + !r3.stderr.includes('It IS pushed'), + r3.stderr, + ); + + // R4 — STATE 3, cause 1: the checkout cannot answer the question at all. + // Must not be dressed as either verdict. + const uiNo = mkUi('objectui-no-origin-main'); + const noBase = uiNo.c('chore: base'); + const noHead = uiNo.c('feat(core): tip'); + const r4 = runBump(mkFramework('fw-reach-no-origin', noBase), uiNo.dir, ['--no-commit', noHead]); + check( + '#10495 R4 no origin/main → says it COULD NOT DETERMINE, and names that as the reason', + r4.status === 0 && + r4.stderr.includes('COULD NOT DETERMINE') && + r4.stderr.includes("has no 'origin/main' ref"), + `status=${r4.status}\nSTDERR:${r4.stderr}`, + ); + check( + '#10495 R4b unanswered is NOT a false alarm and NOT a pass — it borrows neither verdict', + !r4.stderr.includes('is NOT reachable from origin/main') && + !r4.stdout.includes('(on origin/main)') && + r4.stdout.includes('(reachability UNKNOWN)'), + `STDOUT:${r4.stdout}\nSTDERR:${r4.stderr}`, + ); + + // R5 — STATE 3, cause 2: the object is absent. Two git behaviours make this + // reachable and make 128 the thing that must not be read as "no"; both are + // MEASURED here rather than asserted in prose, so a future git that changes + // either one fails this test instead of silently flipping the branch taken. + const uiMiss = mkUi('objectui-missing-object'); + const missBase = uiMiss.c('chore: base'); + uiMiss.gg('update-ref', 'refs/remotes/origin/main', missBase); + const missHead = uiMiss.c('feat(core): tip whose object we are about to delete'); + rmSync(join(uiMiss.dir, '.git', 'objects', missHead.slice(0, 2), missHead.slice(2)), { + force: true, + }); + const revParseMissing = spawnSync('git', ['-C', uiMiss.dir, 'rev-parse', 'HEAD'], { + encoding: 'utf8', + }); + check( + '#10495 R5a `git rev-parse HEAD` exits 0 for a commit whose OBJECT is gone — so NEW_SHA arriving is no proof of presence', + revParseMissing.status === 0 && revParseMissing.stdout.trim() === missHead, + `status=${revParseMissing.status} stdout=${JSON.stringify(revParseMissing.stdout)}`, + ); + const isAncestorMissing = spawnSync( + 'git', + ['-C', uiMiss.dir, 'merge-base', '--is-ancestor', missHead, 'origin/main'], + { encoding: 'utf8' }, + ); + check( + '#10495 R5b `merge-base --is-ancestor` exits 128 on an absent object — an ERROR, not the "no" that 1 means', + isAncestorMissing.status === 128, + `status=${isAncestorMissing.status} stderr=${isAncestorMissing.stderr}`, + ); + const r5 = runBump(mkFramework('fw-reach-missing-object', missBase), uiMiss.dir, ['--no-commit']); + check( + '#10495 R5c an absent object is reported as UNDETERMINED and says which failure it was', + r5.stderr.includes('COULD NOT DETERMINE') && + r5.stderr.includes('NOT PRESENT') && + !r5.stderr.includes('is NOT reachable from origin/main'), + `status=${r5.status}\nSTDERR:${r5.stderr}`, + ); + + // R6 — the branch list is CAPPED, and a cap that fires says so with the real + // count. A capped list that reads as complete is the same defect class as + // the `head -40` this file's changeset list used to truncate with (#4731). + const uiMany = mkUi('objectui-many-branches'); + const manyBase = uiMany.c('chore: base'); + const manyMain = uiMany.c('feat(core): main tip'); + uiMany.gg('update-ref', 'refs/remotes/origin/main', manyMain); + uiMany.gg('checkout', '-q', '-b', 'feature/many-0'); + const manySha = uiMany.c('feat(core): a commit 13 local branches contain'); + for (let i = 1; i <= 12; i += 1) uiMany.gg('branch', `dup/branch-${i}`, manySha); + uiMany.gg('checkout', '-q', 'main'); + const r6 = runBump(mkFramework('fw-reach-many', manyBase), uiMany.dir, ['--no-commit', manySha]); + check( + '#10495 R6 a capped branch list SAYS it is capped, with the real count', + r6.stderr.includes('(+3 more; 13 total)'), + r6.stderr, + ); + } finally { rmSync(tmp, { recursive: true, force: true }); }