Release ForgeFlow 0.5.2

This commit is contained in:
NuklearRabbit
2026-07-25 01:07:54 +02:00
parent 602309b203
commit cf1f67a823
23 changed files with 550 additions and 72 deletions
+49 -3
View File
@@ -57,6 +57,50 @@ class GitService {
return result.stdout.trim();
}
pathspecInput(paths) {
const selected = assertRepositoryRelativePaths(paths);
return selected.length ? `${selected.join('\0')}\0` : '';
}
async runWithPathspec(root, args, paths, options = {}) {
const selected = assertRepositoryRelativePaths(paths);
if (!selected.length) return run('git', args, { cwd: root, ...options });
return run('git', [...args, '--pathspec-from-file=-', '--pathspec-file-nul'], {
cwd: root,
input: this.pathspecInput(selected),
...options
});
}
async getIndexLockInfo(repoPath) {
const root = await this.ensureRepository(repoPath);
const lockPath = path.join(root, '.git', 'index.lock');
const stat = await fs.stat(lockPath).catch(() => null);
return stat ? { exists: true, lockPath, ageMs: Math.max(0, Date.now() - stat.mtimeMs) } : { exists: false, lockPath, ageMs: 0 };
}
async removeStaleIndexLock(repoPath, minimumAgeMs = 30_000) {
const info = await this.getIndexLockInfo(repoPath);
if (!info.exists) return { removed: false, reason: 'missing', ...info };
if (info.ageMs < minimumAgeMs) {
const error = new Error('The Git index lock is recent. Close other Git tools and try again before removing it.');
error.code = 'INDEX_LOCK_RECENT';
throw error;
}
await fs.rm(info.lockPath, { force: true });
return { removed: true, ...info };
}
async setRemoteUrl(repoPath, remoteUrl, remote = 'origin') {
const root = await this.ensureRepository(repoPath);
const safeRemote = assertCloneRemote(remoteUrl);
const name = String(remote || 'origin').trim();
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error('Invalid Git remote name.');
await run('git', ['remote', 'set-url', name, safeRemote], { cwd: root, timeout: 30_000 });
return this.status(root);
}
async diff(repoPath, filePath, staged = false) {
const root = await this.ensureRepository(repoPath);
const safeFile = filePath ? assertRepositoryRelativePath(filePath) : '';
@@ -109,7 +153,7 @@ class GitService {
// renames are already ready for commit and must therefore be left alone.
const selected = await this.expandSelectedPaths(root, requested, { unstagedOnly: true });
if (selected.length) {
await run('git', ['add', '-A', '--', ...selected], { cwd: root, timeout: 60_000 });
await this.runWithPathspec(root, ['add', '-A'], selected, { timeout: 120_000 });
}
return this.status(root);
}
@@ -119,9 +163,11 @@ class GitService {
const selected = await this.expandSelectedPaths(root, files);
const hasHead = await run('git', ['rev-parse', '--verify', 'HEAD'], { cwd: root, allowExitCodes: [128] });
if (hasHead.exitCode === 0) {
await run('git', selected.length ? ['restore', '--staged', '--', ...selected] : ['restore', '--staged', '.'], { cwd: root });
if (selected.length) await this.runWithPathspec(root, ['restore', '--staged'], selected, { timeout: 120_000 });
else await run('git', ['restore', '--staged', '.'], { cwd: root });
} else {
await run('git', selected.length ? ['rm', '--cached', '--ignore-unmatch', '--', ...selected] : ['rm', '--cached', '-r', '.'], { cwd: root, allowExitCodes: [1] });
if (selected.length) await this.runWithPathspec(root, ['rm', '--cached', '--ignore-unmatch'], selected, { timeout: 120_000, allowExitCodes: [1] });
else await run('git', ['rm', '--cached', '-r', '.'], { cwd: root, allowExitCodes: [1] });
}
return this.status(root);
}