From 02df68641a201099e5565b2a098788d6f5d7fb34 Mon Sep 17 00:00:00 2001 From: Matt Morgan Date: Thu, 11 Jun 2026 15:12:24 -0700 Subject: [PATCH 1/2] =?UTF-8?q?fix(betterer=20=F0=9F=90=9B):=20don't=20rei?= =?UTF-8?q?nitialize=20an=20existing=20git=20repo=20(corrupts=20shared=20c?= =?UTF-8?q?onfig=20in=20worktrees)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BettererGitΩ._init()` ran `git init` on every run, even though `_findGitRoot()` has already established that we're inside a git repository. Re-initialising is redundant, and in a linked-worktree setup (where `/.git` is a gitfile pointing into a shared common git dir) it reinitialises against the *common* git dir and can rewrite shared config — flipping `core.bare` and breaking every worktree on the repo (`fatal: this operation must be run in a work tree`). Guard the init behind `git.checkIsRepo()` so an already-initialised repo is never reinitialised. Also fix the retry loop, whose `i >= retries` guard could never be true and so silently swallowed all `git init` errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/betterer/src/fs/git.ts | 12 +++++- test/worktree.spec.ts | 73 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 test/worktree.spec.ts diff --git a/packages/betterer/src/fs/git.ts b/packages/betterer/src/fs/git.ts index e1ad7ca5b..6387ade1e 100644 --- a/packages/betterer/src/fs/git.ts +++ b/packages/betterer/src/fs/git.ts @@ -105,12 +105,22 @@ export class BettererGitΩ implements BettererVersionControl { } private async _init(git: SimpleGit): Promise { + // `_findGitRoot` has already established that we are inside a git repository, so there is no + // need to initialise one. Re-running `git init` is not only redundant, it is dangerous: in a + // linked worktree (where `/.git` is a gitfile pointing into a shared common git dir) + // it reinitialises against the *common* git dir and can rewrite shared config, flipping + // `core.bare` and breaking every worktree on the repo: + if (await git.checkIsRepo()) { + return; + } + const retries = 3; for (let i = 0; i < retries; i++) { try { await git.init(); + return; } catch (error) { - if (i >= retries) { + if (i === retries - 1) { throw error; } } diff --git a/test/worktree.spec.ts b/test/worktree.spec.ts new file mode 100644 index 000000000..1479d747d --- /dev/null +++ b/test/worktree.spec.ts @@ -0,0 +1,73 @@ +// eslint-disable-next-line require-extensions/require-extensions -- tests not ESM ready yet +import { createFixture } from './fixture'; + +import { exec as execΩ } from 'node:child_process'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +const exec = promisify(execΩ); + +describe('betterer', () => { + it('should not reinitialise the repository when run inside a git worktree', async () => { + const { betterer } = await import('@betterer/betterer'); + + // The fixture directory is our scratch space. Inside it we build the popular "bare repo + + // linked worktree" layout, where the worktree's `.git` is a gitfile pointing into a shared + // common (bare) git dir. Running `git init` from inside the worktree reinitialises against + // that *shared* dir and flips its `core.bare` to `false`, breaking every worktree on the repo. + const { paths, cleanup } = await createFixture('worktree', {}); + + const root = paths.cwd; + const sourcePath = path.posix.join(root, 'source'); + const barePath = path.posix.join(root, 'repo.git'); + const worktreePath = path.posix.join(root, 'worktree'); + + async function git(cwd: string, command: string): Promise { + const { stdout } = await exec(`git ${command}`, { cwd }); + return stdout.trim(); + } + + // 1. Create a source repository with a Betterer config and a file to lint: + await fs.mkdir(path.posix.join(sourcePath, 'src'), { recursive: true }); + await fs.writeFile( + path.posix.join(sourcePath, '.betterer.js'), + [ + `const { regexp } = require('@betterer/regexp');`, + ``, + `module.exports = {`, + ` test: () => regexp(/(\\/\\/\\s*HACK)/i).include('./src/**/*.ts')`, + `};` + ].join('\n') + ); + await fs.writeFile(path.posix.join(sourcePath, 'src', 'index.ts'), `// HACK:`); + await git(sourcePath, 'init -q'); + await git(sourcePath, 'config user.email test@betterer.dev'); + await git(sourcePath, 'config user.name "Betterer Test"'); + await git(sourcePath, 'add -A'); + await git(sourcePath, 'commit -qm "init"'); + const branch = await git(sourcePath, 'symbolic-ref --short HEAD'); + + // 2. Create a bare clone to act as the shared common git dir: + await git(root, `clone -q --bare "${sourcePath}" "${barePath}"`); + + // 3. Add a linked worktree off the bare repo: + await git(barePath, `worktree add -q "${worktreePath}" ${branch}`); + + // The shared common dir is bare to begin with: + expect(await git(barePath, 'config --get core.bare')).toBe('true'); + + // 4. Run Betterer inside the worktree: + await betterer({ + cwd: worktreePath, + configPaths: [path.posix.join(worktreePath, '.betterer.js')], + resultsPath: path.posix.join(worktreePath, '.betterer.results'), + workers: false + }); + + // 5. The shared common-dir config must be untouched: + expect(await git(barePath, 'config --get core.bare')).toBe('true'); + + await cleanup(); + }); +}); From 226988884b3c9712d80662a2524d3861148c6513 Mon Sep 17 00:00:00 2001 From: Matt Morgan Date: Thu, 11 Jun 2026 18:29:39 -0700 Subject: [PATCH 2/2] =?UTF-8?q?refactor(betterer=20=F0=9F=94=A7):=20addres?= =?UTF-8?q?s=20review=20=E2=80=94=20drop=20comments,=20use=20fixture=20uti?= =?UTF-8?q?ls=20and=20simple-git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the explanatory comments from `_init` and the worktree test, write the test fixtures via `createFixture` utilities (cleanup handles teardown), and drive the git setup through `simple-git` instead of direct `exec` calls. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/betterer/src/fs/git.ts | 5 --- test/worktree.spec.ts | 76 ++++++++++++--------------------- 2 files changed, 27 insertions(+), 54 deletions(-) diff --git a/packages/betterer/src/fs/git.ts b/packages/betterer/src/fs/git.ts index 6387ade1e..92053f289 100644 --- a/packages/betterer/src/fs/git.ts +++ b/packages/betterer/src/fs/git.ts @@ -105,11 +105,6 @@ export class BettererGitΩ implements BettererVersionControl { } private async _init(git: SimpleGit): Promise { - // `_findGitRoot` has already established that we are inside a git repository, so there is no - // need to initialise one. Re-running `git init` is not only redundant, it is dangerous: in a - // linked worktree (where `/.git` is a gitfile pointing into a shared common git dir) - // it reinitialises against the *common* git dir and can rewrite shared config, flipping - // `core.bare` and breaking every worktree on the repo: if (await git.checkIsRepo()) { return; } diff --git a/test/worktree.spec.ts b/test/worktree.spec.ts index 1479d747d..2c78cef80 100644 --- a/test/worktree.spec.ts +++ b/test/worktree.spec.ts @@ -1,72 +1,50 @@ // eslint-disable-next-line require-extensions/require-extensions -- tests not ESM ready yet import { createFixture } from './fixture'; -import { exec as execΩ } from 'node:child_process'; -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import { promisify } from 'node:util'; - -const exec = promisify(execΩ); +import { simpleGit } from 'simple-git'; describe('betterer', () => { it('should not reinitialise the repository when run inside a git worktree', async () => { const { betterer } = await import('@betterer/betterer'); - // The fixture directory is our scratch space. Inside it we build the popular "bare repo + - // linked worktree" layout, where the worktree's `.git` is a gitfile pointing into a shared - // common (bare) git dir. Running `git init` from inside the worktree reinitialises against - // that *shared* dir and flips its `core.bare` to `false`, breaking every worktree on the repo. - const { paths, cleanup } = await createFixture('worktree', {}); + const { resolve, cleanup } = await createFixture('worktree', { + 'source/.betterer.js': ` +const { regexp } = require('@betterer/regexp'); - const root = paths.cwd; - const sourcePath = path.posix.join(root, 'source'); - const barePath = path.posix.join(root, 'repo.git'); - const worktreePath = path.posix.join(root, 'worktree'); +module.exports = { + test: () => regexp(/(\\/\\/\\s*HACK)/i).include('./src/**/*.ts') +}; + `, + 'source/src/index.ts': `// HACK:` + }); - async function git(cwd: string, command: string): Promise { - const { stdout } = await exec(`git ${command}`, { cwd }); - return stdout.trim(); - } + const sourcePath = resolve('source'); + const barePath = resolve('repo.git'); + const worktreePath = resolve('worktree'); - // 1. Create a source repository with a Betterer config and a file to lint: - await fs.mkdir(path.posix.join(sourcePath, 'src'), { recursive: true }); - await fs.writeFile( - path.posix.join(sourcePath, '.betterer.js'), - [ - `const { regexp } = require('@betterer/regexp');`, - ``, - `module.exports = {`, - ` test: () => regexp(/(\\/\\/\\s*HACK)/i).include('./src/**/*.ts')`, - `};` - ].join('\n') - ); - await fs.writeFile(path.posix.join(sourcePath, 'src', 'index.ts'), `// HACK:`); - await git(sourcePath, 'init -q'); - await git(sourcePath, 'config user.email test@betterer.dev'); - await git(sourcePath, 'config user.name "Betterer Test"'); - await git(sourcePath, 'add -A'); - await git(sourcePath, 'commit -qm "init"'); - const branch = await git(sourcePath, 'symbolic-ref --short HEAD'); + const source = simpleGit(sourcePath); + await source.init(); + await source.addConfig('user.email', 'test@betterer.dev'); + await source.addConfig('user.name', 'Betterer Test'); + await source.add('.'); + await source.commit('init'); + const branch = (await source.revparse(['--abbrev-ref', 'HEAD'])).trim(); - // 2. Create a bare clone to act as the shared common git dir: - await git(root, `clone -q --bare "${sourcePath}" "${barePath}"`); + await simpleGit(resolve('.')).clone(sourcePath, barePath, ['--bare']); - // 3. Add a linked worktree off the bare repo: - await git(barePath, `worktree add -q "${worktreePath}" ${branch}`); + const bare = simpleGit(barePath); + await bare.raw('worktree', 'add', worktreePath, branch); - // The shared common dir is bare to begin with: - expect(await git(barePath, 'config --get core.bare')).toBe('true'); + expect((await bare.raw('config', '--get', 'core.bare')).trim()).toBe('true'); - // 4. Run Betterer inside the worktree: await betterer({ cwd: worktreePath, - configPaths: [path.posix.join(worktreePath, '.betterer.js')], - resultsPath: path.posix.join(worktreePath, '.betterer.results'), + configPaths: [resolve('worktree/.betterer.js')], + resultsPath: resolve('worktree/.betterer.results'), workers: false }); - // 5. The shared common-dir config must be untouched: - expect(await git(barePath, 'config --get core.bare')).toBe('true'); + expect((await bare.raw('config', '--get', 'core.bare')).trim()).toBe('true'); await cleanup(); });