agent-ecosystem/src/main/services/team/GitDiffFallback.ts
iliya 032d9b478b feat: enhance team member validation and improve logging
- Added validation to prevent the use of reserved names ("user" and "team-lead") for team members, ensuring clearer error messages during member addition.
- Updated IPC handlers to improve logging functionality, enhancing observability of team-related actions.
- Implemented normalization of file paths across various services to ensure consistent handling on different platforms.
- Enhanced UI components to provide better feedback on team member statuses and actions.

Made-with: Cursor
2026-03-03 23:58:19 +02:00

159 lines
4.5 KiB
TypeScript

import * as path from 'node:path';
import { execFile } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
const GIT_TIMEOUT = 10_000; // 10s timeout for all git operations
const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10MB
function toRepoRelativePath(projectPath: string, filePath: string): string | null {
const normalizedProject = path.resolve(projectPath);
const normalizedFile = path.isAbsolute(filePath) ? path.resolve(filePath) : filePath;
// If we have an absolute file path, require it to be under projectPath.
if (path.isAbsolute(normalizedFile)) {
const rel = path.relative(normalizedProject, normalizedFile);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
// Git pathspecs use forward slashes even on Windows.
const gitPath = rel.replace(/\\/g, '/');
if (gitPath.includes(':')) return null;
return gitPath;
}
// Relative path: normalize separators for git.
const gitPath = normalizedFile.replace(/\\/g, '/').replace(/^\.\/+/, '');
if (
!gitPath ||
gitPath.startsWith('/') ||
/^[a-zA-Z]:\//.test(gitPath) ||
gitPath.includes(':')
) {
return null;
}
return gitPath;
}
export class GitDiffFallback {
private gitRepoCache = new Map<string, boolean>();
/**
* Get file contents at a specific commit.
* Used when file-history-snapshot is unavailable.
*/
async getFileAtCommit(
projectPath: string,
filePath: string,
commitHash: string
): Promise<string | null> {
try {
const relativePath = toRepoRelativePath(projectPath, filePath);
if (!relativePath) return null;
const { stdout } = await execFileAsync('git', ['show', `${commitHash}:${relativePath}`], {
cwd: projectPath,
maxBuffer: GIT_MAX_BUFFER,
timeout: GIT_TIMEOUT,
});
return stdout;
} catch {
return null;
}
}
/**
* Find the commit closest to (but before) a given timestamp for a file.
*/
async findCommitNearTimestamp(
projectPath: string,
filePath: string,
timestamp: string
): Promise<string | null> {
try {
const relativePath = toRepoRelativePath(projectPath, filePath);
if (!relativePath) return null;
const { stdout } = await execFileAsync(
'git',
['log', '--format=%H', '--before', timestamp, '-1', '--', relativePath],
{ cwd: projectPath, timeout: GIT_TIMEOUT }
);
return stdout.trim() || null;
} catch {
return null;
}
}
/**
* Get git diff for a file between two refs.
*/
async getGitDiff(
projectPath: string,
filePath: string,
fromCommit: string,
toCommit: string = 'HEAD'
): Promise<string | null> {
try {
const relativePath = toRepoRelativePath(projectPath, filePath);
if (!relativePath) return null;
const { stdout } = await execFileAsync(
'git',
['diff', fromCommit, toCommit, '--', relativePath],
{ cwd: projectPath, timeout: GIT_TIMEOUT }
);
return stdout || null;
} catch {
return null;
}
}
/**
* Get file change log (for timeline enrichment).
*/
async getFileLog(
projectPath: string,
filePath: string,
maxCount: number = 20
): Promise<{ hash: string; timestamp: string; message: string }[]> {
try {
const relativePath = toRepoRelativePath(projectPath, filePath);
if (!relativePath) return [];
const { stdout } = await execFileAsync(
'git',
['log', `--max-count=${maxCount}`, '--format=%H|%aI|%s', '--', relativePath],
{ cwd: projectPath, timeout: GIT_TIMEOUT }
);
return stdout
.trim()
.split('\n')
.filter((line) => line.includes('|'))
.map((line) => {
const [hash, timestamp, ...msgParts] = line.split('|');
return { hash, timestamp, message: msgParts.join('|') };
});
} catch {
return [];
}
}
/**
* Check if a path is inside a git repository.
* Result is cached per projectPath for the session lifetime.
*/
async isGitRepo(projectPath: string): Promise<boolean> {
const cached = this.gitRepoCache.get(projectPath);
if (cached !== undefined) return cached;
try {
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
cwd: projectPath,
timeout: GIT_TIMEOUT,
});
this.gitRepoCache.set(projectPath, true);
return true;
} catch {
this.gitRepoCache.set(projectPath, false);
return false;
}
}
}