Files
gstack/test/gitattributes-eol.test.ts
T
6e250405fe v1.91.90.0 fix(windows): LF for nested scripts with a setup CRLF repair, Stop hook slug fast path, setup-gbrain smoke test via temp file (Wave 9 Lane G) (#3142)
.gitattributes pins nested bin/, hook shims, lib/cso/images and scripts/app
to LF, and setup rewrites copies an older checkout left with CRLF, leaving
locally edited files alone with a warning (#3110). The timeline Stop hook
reads the slug from gstack-slug's cache and hands a miss to a detached,
locked, negatively cached repair child (#3103). The /setup-gbrain smoke test
writes its page to a temp file instead of piping into gbrain put (#1675).

Co-authored-by: Mr-B-1 <214900604+Mr-B-1@users.noreply.github.com>
Co-authored-by: Andrei Bacu <63974644+Peaky8linders@users.noreply.github.com>
Co-authored-by: Elliot Drel <156480527+ElliotDrel@users.noreply.github.com>
2026-10-11 17:59:13 +00:00

84 lines
3.2 KiB
TypeScript

/**
* .gitattributes line endings for executables (#3110).
*
* Git for Windows checks out with core.autocrlf=true. A tracked script with no
* `eol` rule is written with CRLF there, and a Linux bash reading that
* checkout (a synced tree, WSL, a bind mount) fails on `set -o pipefail\r`.
* `bin/*` is anchored at the root, so nested bin/ dirs, the hook shims, the
* CSO image scripts and the app launcher had no rule. This pins the rules for
* every tracked file that starts with `#!`, and keeps the broad patterns from
* marking a binary file as text.
*/
import { describe, test, expect } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
function git(args: string[], input?: string): string {
const r = spawnSync('git', args, { cwd: ROOT, encoding: 'utf-8', input, timeout: 30_000, maxBuffer: 64 * 1024 * 1024 });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`);
return r.stdout;
}
function trackedShebangFiles(): string[] {
return git(['ls-files', '-z']).split('\0').filter((f) => {
if (!f) return false;
const full = path.join(ROOT, f);
let fd: number | undefined;
try {
if (!fs.statSync(full).isFile()) return false;
fd = fs.openSync(full, 'r');
const head = Buffer.alloc(2);
return fs.readSync(fd, head, 0, 2, 0) === 2 && head.toString('latin1') === '#!';
} catch {
return false;
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
});
}
describe('.gitattributes eol rules (#3110)', () => {
test('every tracked file that starts with #! resolves to eol=lf', () => {
const files = trackedShebangFiles();
expect(files.length).toBeGreaterThan(100);
const out = git(['check-attr', '-z', '--stdin', 'eol'], files.join('\0') + '\0').split('\0');
const missing: string[] = [];
for (let i = 0; i + 2 < out.length; i += 3) {
if (out[i + 2] !== 'lf') missing.push(`${out[i]} (eol: ${out[i + 2]})`);
}
expect(missing).toEqual([]);
});
test('the nested executables named in #3110 are covered', () => {
const named = [
'autoplan/bin/phase-publication-hook',
'plan-ceo-review/bin/mode-handoff-hook',
'hosts/claude/hooks/timeline-stop-hook',
'browse/bin/remote-slug',
'lib/cso/images/entrypoint',
'scripts/app/gstack-browser',
'browse/test/fixtures/mock-claude/claude',
];
const out = git(['check-attr', 'eol', '--', ...named]);
for (const f of named) expect(out).toContain(`${f}: eol: lf`);
});
test('no file git stores as binary is marked text by the broad patterns', () => {
const wronglyText = git(['ls-files', '--eol'])
.split('\n')
.filter((line) => line.startsWith('i/-text') && /\sattr\/text\b/.test(line));
expect(wronglyText).toEqual([]);
});
test('every shebang file is stored with LF in the index', () => {
const files = new Set(trackedShebangFiles());
const crlf = git(['ls-files', '--eol'])
.split('\n')
.filter((line) => /^i\/(crlf|mixed)\s/.test(line) && files.has(line.split('\t')[1] ?? ''));
expect(crlf).toEqual([]);
});
});