test(rebase): temp repositories run no background maintenance (#717)

The fixture repositories are removed with rmSync in each test's finally,
and that cleanup sometimes failed with ENOTEMPTY on work/.git/objects.
commit, fetch, rebase and the receiving side of push all start
`git maintenance run --auto` / `git gc --auto`; recent git (2.47+)
detaches auto maintenance by default, and a detached run creates
objects/maintenance.lock after the command has returned, i.e. while
rmSync is already walking the tree.

The environment the test already uses for core.autocrlf now also sets
maintenance.auto=false and gc.auto=0 for the working clones. The bare
origin gets receive.autogc=false, maintenance.auto=false and gc.auto=0
in its own config, since git drops GIT_CONFIG_* for the local transport's
receive-pack. The cleanup keeps rmSync in every finally (temp-dir hygiene
rule) with maxRetries/retryDelay, so a file that still appears under it
is retried instead of failing the test. No assertion changed.

Issue: #717
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-09-30 18:24:39 +00:00
committed by claude[bot]
parent 07d90be28f
commit 445a9a0cdf
+21 -6
View File
@@ -17,22 +17,37 @@ import { buildIndex } from '../scripts/reviews-index.mjs';
// конфиг владельца ради теста нельзя — конфиг передаётся окружением.
// #643: GIT_* родителя снимаются целиком (урок #633) — запущенный из pre-push
// хука тест иначе унаследует GIT_DIR и ребейзит репозиторий хука.
// #717: commit, fetch, rebase и приём push зовут `git maintenance run --auto`
// / `git gc --auto`. В свежих git (2.47+) это обслуживание по умолчанию уходит
// в фон и создаёт `objects/maintenance.lock`, когда команда уже вернулась, —
// `rmSync` в finally ловил ENOTEMPTY на `.git/objects`. Во временных
// репозиториях автообслуживание выключено, а очистка повторяет удаление,
// если файл всё же появился под ней.
for (const key of Object.keys(process.env)) if (/^GIT_/i.test(key)) delete process.env[key];
Object.assign(process.env, {
GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t',
GIT_CONFIG_COUNT: '2',
GIT_CONFIG_COUNT: '4',
GIT_CONFIG_KEY_0: 'core.autocrlf', GIT_CONFIG_VALUE_0: 'false',
GIT_CONFIG_KEY_1: 'core.eol', GIT_CONFIG_VALUE_1: 'lf',
GIT_CONFIG_KEY_2: 'maintenance.auto', GIT_CONFIG_VALUE_2: 'false',
GIT_CONFIG_KEY_3: 'gc.auto', GIT_CONFIG_VALUE_3: '0',
});
const git = (cwd, ...args) => execFileSync('git', args, {
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
/** Опции очистки временного репозитория: повтор на ENOTEMPTY/EBUSY (#717). */
const RM_TREE = { recursive: true, force: true, maxRetries: 10, retryDelay: 50 };
function repo({ conflictInSrc, reviews = false }) {
const root = mkdtempSync(join(tmpdir(), 'hp-rebase-'));
const origin = join(root, 'origin.git'); const work = join(root, 'work');
git(root, 'init', '--bare', '-q', '-b', 'dev', origin);
// #717: приём push — процесс origin, а GIT_CONFIG_* git локальному транспорту
// не передаёт; его автообслуживание выключается в конфиге самого origin.
for (const [key, value] of [['receive.autogc', 'false'], ['maintenance.auto', 'false'], ['gc.auto', '0']]) {
git(origin, 'config', key, value);
}
git(root, 'clone', '-q', origin, work);
git(work, 'checkout', '-q', '-b', 'dev');
mkdirSync(join(work, 'dist')); mkdirSync(join(work, 'src'));
@@ -91,7 +106,7 @@ test('#643 AC3/#657: бандл и INDEX.md конфликтуют в одном
assert.match(index, /CODE-REVIEW-8-r1\.md/);
assert.match(index, /CODE-REVIEW-9-r1\.md/);
assert.equal(readFileSync(join(work, 'dist/a.js'), 'utf8'), 'built:dev\n', '#657: бандл ветки не пересобирается');
} finally { rmSync(root, { recursive: true, force: true }); }
} finally { rmSync(root, RM_TREE); }
});
test('конфликт только в бандле: ребейз доведён, бандл = версия dev, без пересборки и амендинга (#479 AC5, #657)', () => {
@@ -107,7 +122,7 @@ test('конфликт только в бандле: ребейз доведён
assert.equal(readFileSync(join(work, 'dist/a.js'), 'utf8'), 'built:dev\n', 'бандл — версия dev, ветка его не несёт (#657)');
assert.equal(git(work, 'diff', '--name-only', 'origin/dev', 'HEAD', '--', 'dist'), '', 'бандл ветки совпал с dev');
assert.equal(readFileSync(join(work, 'src/z.ts'), 'utf8'), 'dev\n', 'правка dev на месте');
} finally { rmSync(root, { recursive: true, force: true }); }
} finally { rmSync(root, RM_TREE); }
});
test('конфликт в src/**: ребейз отменён, дерево и HEAD как были (#479 AC5)', () => {
@@ -118,7 +133,7 @@ test('конфликт в src/**: ребейз отменён, дерево и H
assert.equal(git(work, 'rev-parse', 'HEAD'), before);
assert.equal(git(work, 'status', '--porcelain'), '');
assert.equal(readFileSync(join(work, 'src/y.ts'), 'utf8'), 'branch\n');
} finally { rmSync(root, { recursive: true, force: true }); }
} finally { rmSync(root, RM_TREE); }
});
test('--dry-run предсказывает конфликт по бандлу и не трогает дерево (#479)', () => {
@@ -131,7 +146,7 @@ test('--dry-run предсказывает конфликт по бандлу и
assert.deepEqual(result.predicted.generated, ['dist/a.js']);
assert.equal(git(work, 'rev-parse', 'HEAD'), before);
assert.ok(lines.some((l) => l.includes('dry-run')));
} finally { rmSync(root, { recursive: true, force: true }); }
} finally { rmSync(root, RM_TREE); }
});
test('грязное дерево и ветка dev отвергаются до любого действия (#479)', () => {
@@ -142,5 +157,5 @@ test('грязное дерево и ветка dev отвергаются до
git(work, 'checkout', '-q', '--', 'src/x.ts');
git(work, 'checkout', '-q', 'dev');
assert.throws(() => rebaseOnDev({ cwd: work, log: () => {} }), /ветка dev/);
} finally { rmSync(root, { recursive: true, force: true }); }
} finally { rmSync(root, RM_TREE); }
});