Files
openchamber/packages/web/server/lib/git/issue-2746-longpaths.test.js
10606d79d3 fix(git): enable core.longpaths for worktree population (#2746) (#2747)
* fix(git): enable core.longpaths for worktree population

Worktrees live under a deep OpenCode data-dir path, so Windows checkouts
of deeply nested repos failed bootstrap with "Filename too long". Enable
Git core.longpaths before git reset --hard (web + VS Code) and surface
clearer path-length guidance when the filesystem still rejects a path.

Fixes #2746

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore(vscode): keep ensureWorktreeLongpaths private

Avoid an unused export in the VS Code git service; the helper stays
local to populateWorktreeWithLockRecovery.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-07 09:52:48 +03:00

207 lines
7.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
createWorktree,
ensureWorktreeLongpaths,
getWorktreeBootstrapStatus,
populateWorktreeWithLockRecovery,
} from './service.js';
// ---------------------------------------------------------------------------
// Regression for https://github.com/openchamber/openchamber/issues/2746
//
// "[Bug] new worktree Filename too long"
//
// OpenChamber places worktrees under:
// <XDG_DATA_HOME>/opencode/worktree/<40-char root commit hash>/<worktree name>
// and populates them with `git reset --hard`. On Windows, that deep prefix plus
// a deeply nested repo file (e.g. yudao ~173 chars) exceeds MAX_PATH (260) and
// git aborts with "Filename too long" unless `core.longpaths` is enabled.
// ---------------------------------------------------------------------------
const tempDirs = [];
const createTempDir = () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-git-issue2746-'));
tempDirs.push(dir);
return dir;
};
const runGit = (cwd, args, input) =>
execFileSync('git', args, {
cwd,
encoding: 'utf8',
input,
stdio: ['pipe', 'pipe', 'pipe'],
});
const canRunGit = () => {
try {
execFileSync('git', ['--version'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe('issue #2746 - worktree long path support', () => {
it('enables core.longpaths and populates a deeply nested worktree checkout', async () => {
if (!canRunGit()) return;
const previousXdgDataHome = process.env.XDG_DATA_HOME;
const dataHome = createTempDir();
process.env.XDG_DATA_HOME = dataHome;
try {
const repo = createTempDir();
runGit(repo, ['init', '-b', 'main']);
runGit(repo, ['config', 'user.email', 'test@example.com']);
runGit(repo, ['config', 'user.name', 'Test User']);
// Realistic reporter path: many nested segments, each component well under
// NAME_MAX. On Windows the managed worktree prefix + this relative path
// exceeds MAX_PATH unless core.longpaths is enabled.
const deepRelative = path.join(
'server',
'yudao-framework',
'yudao-spring-boot-starter-biz-data-permission',
'src',
'main',
'java',
'cn',
'iocoder',
'yudao',
'framework',
'datapermission',
'config',
'YudaoDataPermissionAutoConfiguration.java',
);
fs.mkdirSync(path.dirname(path.join(repo, deepRelative)), { recursive: true });
fs.writeFileSync(path.join(repo, deepRelative), '// yudao\n');
fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
runGit(repo, ['add', 'README.md', deepRelative]);
runGit(repo, ['commit', '-qm', 'init']);
const created = await createWorktree(repo, {
mode: 'new',
worktreeName: 'issue-2746',
branchName: 'openchamber/issue-2746',
});
expect(created.directoryCreated).toBe(true);
await expect.poll(async () => {
const status = await getWorktreeBootstrapStatus(created.path);
return status?.status;
}, { timeout: 10_000 }).toBe('ready');
const longpaths = runGit(created.path, ['config', '--get', 'core.longpaths']).trim();
expect(longpaths).toBe('true');
expect(fs.existsSync(path.join(created.path, deepRelative))).toBe(true);
expect(fs.existsSync(path.join(created.path, 'README.md'))).toBe(true);
} finally {
if (previousXdgDataHome === undefined) {
delete process.env.XDG_DATA_HOME;
} else {
process.env.XDG_DATA_HOME = previousXdgDataHome;
}
}
});
it('ensureWorktreeLongpaths is idempotent when already enabled', async () => {
if (!canRunGit()) return;
const repo = createTempDir();
runGit(repo, ['init', '-b', 'main']);
runGit(repo, ['config', 'user.email', 'test@example.com']);
runGit(repo, ['config', 'user.name', 'Test User']);
fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
runGit(repo, ['add', 'README.md']);
runGit(repo, ['commit', '-qm', 'init']);
runGit(repo, ['config', 'core.longpaths', 'true']);
await expect(ensureWorktreeLongpaths(repo)).resolves.toBeUndefined();
expect(runGit(repo, ['config', '--get', 'core.longpaths']).trim()).toBe('true');
});
it('surfaces guided bootstrap failure when a path component exceeds the filesystem name limit', async () => {
if (!canRunGit()) return;
const previousXdgDataHome = process.env.XDG_DATA_HOME;
const dataHome = createTempDir();
process.env.XDG_DATA_HOME = dataHome;
try {
const repo = createTempDir();
runGit(repo, ['init', '-b', 'main']);
runGit(repo, ['config', 'user.email', 'test@example.com']);
runGit(repo, ['config', 'user.name', 'Test User']);
// Linux/macOS NAME_MAX equivalent of the Windows failure mode: a single
// path component longer than 255 cannot be materialized. core.longpaths
// cannot fix this; bootstrap must fail clearly instead of leaving a
// silent half-populated worktree.
const longComponent = 'x'.repeat(300);
const longPath = `server/${longComponent}/YudaoDataPermissionAutoConfiguration.java`;
const blobHash = runGit(repo, ['hash-object', '-w', '--stdin'], '// test\n').trim();
runGit(repo, ['update-index', '--add', '--cacheinfo', `100644,${blobHash},${longPath}`]);
runGit(repo, ['commit', '-qm', 'init']);
fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
runGit(repo, ['add', 'README.md']);
runGit(repo, ['commit', '-qm', 'add readme']);
const created = await createWorktree(repo, {
mode: 'new',
worktreeName: 'issue-2746-namemax',
branchName: 'openchamber/issue-2746-namemax',
});
expect(created.directoryCreated).toBe(true);
await expect.poll(async () => {
const status = await getWorktreeBootstrapStatus(created.path);
return status?.status;
}, { timeout: 10_000 }).toBe('failed');
const status = await getWorktreeBootstrapStatus(created.path);
expect(status?.error).toMatch(/file name too long|filename too long/i);
expect(status?.error).toMatch(/path-length limit/i);
expect(runGit(created.path, ['config', '--get', 'core.longpaths']).trim()).toBe('true');
} finally {
if (previousXdgDataHome === undefined) {
delete process.env.XDG_DATA_HOME;
} else {
process.env.XDG_DATA_HOME = previousXdgDataHome;
}
}
});
it('populateWorktreeWithLockRecovery enables longpaths before reset', async () => {
if (!canRunGit()) return;
const repo = createTempDir();
runGit(repo, ['init', '-b', 'main']);
runGit(repo, ['config', 'user.email', 'test@example.com']);
runGit(repo, ['config', 'user.name', 'Test User']);
fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
runGit(repo, ['add', 'README.md']);
runGit(repo, ['commit', '-qm', 'init']);
const worktree = createTempDir();
fs.rmSync(worktree, { recursive: true, force: true });
runGit(repo, ['worktree', 'add', '--no-checkout', '-b', 'feature/longpaths-populate', worktree, 'HEAD']);
await expect(populateWorktreeWithLockRecovery(worktree)).resolves.toBeUndefined();
expect(runGit(worktree, ['config', '--get', 'core.longpaths']).trim()).toBe('true');
expect(fs.readFileSync(path.join(worktree, 'README.md'), 'utf8')).toBe('# Test\n');
});
});