Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/opencode/test/skill/tracker-leak-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,36 @@ describe("scanner end-to-end (git behaviour)", () => {
const r = run(dir, `(delete) ${ZERO} refs/heads/gone ${"1".repeat(40)}\n`)
expect(r.status).toBe(0)
})

test("a real `git push` reads the refs from the hook's stdin and finishes", () => {
// Runs the script the way git does: a pre-push hook fed by git itself,
// pushing to a local bare repo. This guards the stdin contract end to end; it
// does not reproduce the intermittent ssh hang, which never happened on demand.
const dir = repo()
baseline(dir)
const remote = fs.mkdtempSync(path.join(os.tmpdir(), "tracker-remote-"))
created.push(remote)
spawnSync("git", ["init", "-q", "--bare", remote])
// Outside the work tree: `commit()` stages everything, and a tracked hook
// would vanish on the next branch switch.
const hooks = fs.mkdtempSync(path.join(os.tmpdir(), "tracker-hooks-"))
created.push(hooks)
fs.writeFileSync(path.join(hooks, "pre-push"), `#!/bin/sh\nexec bun ${JSON.stringify(SCRIPT)}\n`, { mode: 0o755 })
git(dir, "config", "core.hooksPath", hooks)
const push = (ref: string) =>
spawnSync("git", ["push", "-q", remote, ref], { cwd: dir, encoding: "utf-8", timeout: 30_000 })

const clean = push("main:refs/heads/clean")
expect(clean.signal).toBeNull()
expect(clean.status).toBe(0)

// HEAD stays clean, so only a scan of the pushed ref from stdin catches this.
git(dir, "checkout", "-qb", "dirty")
commit(dir, `hidden ${key(5151)}`, "leaky work")
git(dir, "checkout", "-q", "main")
const leaky = push("dirty:refs/heads/other")
expect(leaky.signal).toBeNull()
expect(leaky.status).not.toBe(0)
expect(leaky.stderr).toContain(key(5151))
})
})
27 changes: 16 additions & 11 deletions script/check-tracker-leaks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* job of a pre-push guard.
*/

import { $ } from "bun"
import { readFileSync } from "fs"

// altimate_change — #1052 D8 review-fix: drop the trailing word-boundary from
// the Jira-key regex so the guard catches suffix-adjacent leaks (letter,
Expand Down Expand Up @@ -79,15 +79,15 @@ type Hit = {
// (b) `catch { return "" }` swallowed real errors (missing ref, corrupted
// repo). A failed git command would look identical to a clean scan.
//
// `git` is invoked directly via Bun.$ (no shell). Args are passed through the
// tagged-template interpolation which quotes each interpolation as a single
// argv element — no shell parsing anywhere. `.nothrow()` lets us inspect the
// exit code instead of catching an exception. `exitOnFailure` distinguishes
// "expected empty result" (mergeBase against a diverged history) from
// "unexpected failure" (git binary missing, corrupt index) so the latter
// fails loud rather than reporting the branch as clean.
// `git` runs directly from an argv array (no shell), so a caller-supplied value
// is never re-parsed. The exit code is inspected rather than thrown.
// `failOnError` distinguishes "expected empty result" (mergeBase against a
// diverged history) from "unexpected failure" (git binary missing, corrupt
// index) so the latter fails loud rather than reporting the branch as clean.
// The call is synchronous and git gets no stdin: see readPushedRefs for why
// this script avoids waiting on the event loop.
async function git(args: string[], opts: { failOnError?: boolean } = { failOnError: true }): Promise<string> {
const r = await $`git ${args}`.quiet().nothrow()
const r = Bun.spawnSync({ cmd: ["git", ...args], stdin: "ignore", stdout: "pipe", stderr: "pipe" })
if (r.exitCode !== 0) {
if (opts.failOnError) {
process.stderr.write(
Expand All @@ -97,7 +97,7 @@ async function git(args: string[], opts: { failOnError?: boolean } = { failOnErr
}
return ""
}
return r.text().trim()
return r.stdout.toString().trim()
}

function scanText(text: string, source: string, hits: Hit[]) {
Expand Down Expand Up @@ -185,7 +185,12 @@ async function readPushedRefs(): Promise<{
}> {
if (process.stdin.isTTY) return { hadInput: false, tips: [] }
try {
const raw = await new Response(Bun.stdin.stream()).text()
// Blocking read of fd 0. Under `git push` over ssh (macOS) this script once
// sat for days with git waiting on the hook: bun idle in its event loop, no
// child process, and the stdin pipe's writer already closed. It was awaiting
// either the stdin stream or a `Bun.$` git call; the event loop missed the
// completion. Neither await is used now: stdin and git are synchronous.
const raw = readFileSync(0, "utf-8")
const rows = raw
.split("\n")
.map((l) => l.trim())
Expand Down
Loading