From 6d52c64b5d12878108207fd5a1a17f8dd98e06e7 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Tue, 3 Feb 2026 09:02:34 +0000 Subject: [PATCH] feat: add git utilities module --- src/utils/git-utils.ts | 271 +++++------------------------------------ 1 file changed, 31 insertions(+), 240 deletions(-) diff --git a/src/utils/git-utils.ts b/src/utils/git-utils.ts index fec3f3b..815081b 100644 --- a/src/utils/git-utils.ts +++ b/src/utils/git-utils.ts @@ -43,9 +43,7 @@ export async function createWorktree( mainBranch: string ): Promise { const git = createGit(basePath); - await fs.ensureDir(workspacePath); - try { await git.raw(['worktree', 'add', workspacePath, '-b', branchName]); } catch (error: any) { @@ -58,7 +56,6 @@ export async function createWorktree( export async function removeWorktree(basePath: string, workspacePath: string, branchName: string): Promise { const git = createGit(basePath); - try { await git.raw(['worktree', 'remove', workspacePath, '--force']); } catch (error: any) { @@ -81,10 +78,8 @@ export async function deleteBranch(git: SimpleGit, branchName: string, force: bo export async function listWorktrees(basePath: string): Promise<{ path: string; branch: string }[]> { const git = createGit(basePath); const result = await git.raw(['worktree', 'list', '--porcelain']); - const worktrees: { path: string; branch: string }[] = []; const lines = result.split('\n').filter(Boolean); - for (const line of lines) { const match = line.match(/^worktree (.+)$/); if (match) { @@ -96,84 +91,35 @@ export async function listWorktrees(basePath: string): Promise<{ path: string; b } } } - return worktrees; } export async function getWorktreeStatus(workspacePath: string): Promise { const git = createGit(workspacePath); - const status = await git.status(); const branch = await getCurrentBranch(git); const agentName = extractAgentName(branch); - const changes: FileChange[] = []; - for (const file of status.created) { - changes.push({ - file, - status: 'added', - staged: true, - author: 'agent', - timestamp: new Date().toISOString(), - diff: '' - }); + changes.push({ file, status: 'added', staged: true, author: 'agent', timestamp: new Date().toISOString(), diff: '' }); } - for (const file of status.modified) { const diff = await git.diff(['--no-color', file]); - changes.push({ - file, - status: 'modified', - staged: status.staged.includes(file), - author: 'agent', - timestamp: new Date().toISOString(), - diff - }); + changes.push({ file, status: 'modified', staged: status.staged.includes(file), author: 'agent', timestamp: new Date().toISOString(), diff }); } - for (const file of status.deleted) { - changes.push({ - file, - status: 'deleted', - staged: status.staged.includes(file), - author: 'agent', - timestamp: new Date().toISOString(), - diff: '' - }); + changes.push({ file, status: 'deleted', staged: status.staged.includes(file), author: 'agent', timestamp: new Date().toISOString(), diff: '' }); } - - return { - agentName, - branch, - path: workspacePath, - changes, - lastUpdated: new Date().toISOString(), - uncommittedCount: status.modified.length + status.created.length + status.deleted.length - }; + return { agentName, branch, path: workspacePath, changes, lastUpdated: new Date().toISOString(), uncommittedCount: status.modified.length + status.created.length + status.deleted.length }; } -export async function generateDiff( - basePath: string, - workspacePath: string, - agentName: string, - comparedBranch: string = 'main' -): Promise { +export async function generateDiff(basePath: string, workspacePath: string, agentName: string, comparedBranch: string = 'main'): Promise { const git = createGit(workspacePath); const branch = await getCurrentBranch(git); - const diffOutput = await git.diff(['--no-color', '--unified=3', comparedBranch]); - const files = parseDiffOutput(diffOutput); const summary = calculateDiffSummary(files); - - return { - agentName, - branch, - comparedBranch, - files, - summary - }; + return { agentName, branch, comparedBranch, files, summary }; } export function parseDiffOutput(output: string): DiffFile[] { @@ -183,239 +129,84 @@ export function parseDiffOutput(output: string): DiffFile[] { let currentChanges: DiffLine[] = []; let oldLineNum: number | null = null; let newLineNum: number | null = null; - for (const line of lines) { const fileMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/); if (fileMatch) { - if (currentFile) { - currentFile.changes = currentChanges; - files.push(currentFile); - } - currentFile = { - file: fileMatch[2] || fileMatch[1], - oldFile: fileMatch[1] !== fileMatch[2] ? fileMatch[1] : undefined, - changes: [], - additions: 0, - deletions: 0 - }; - currentChanges = []; - oldLineNum = null; - newLineNum = null; - continue; + if (currentFile) { currentFile.changes = currentChanges; files.push(currentFile); } + currentFile = { file: fileMatch[2] || fileMatch[1], oldFile: fileMatch[1] !== fileMatch[2] ? fileMatch[1] : undefined, changes: [], additions: 0, deletions: 0 }; + currentChanges = []; oldLineNum = null; newLineNum = null; continue; } - - const indexMatch = line.match(/^index [a-f0-9]+\.\.[a-f0-9]+/); - if (indexMatch && currentFile) { - continue; - } - - const newFileMatch = line.match(/^new file mode .+$/); - if (newFileMatch && currentFile) { - continue; - } - - const oldFileMatch = line.match(/^deleted file mode .+$/); - if (oldFileMatch && currentFile) { - continue; - } - const hunkMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/); if (hunkMatch && currentFile) { oldLineNum = parseInt(hunkMatch[1], 10); newLineNum = parseInt(hunkMatch[3], 10); continue; } - if (currentFile) { let lineType: 'context' | 'added' | 'deleted' = 'context'; - - if (line.startsWith('+') && !line.startsWith('+++')) { - lineType = 'added'; - currentFile.additions++; - } else if (line.startsWith('-') && !line.startsWith('---')) { - lineType = 'deleted'; - currentFile.deletions++; - } - - currentChanges.push({ - type: lineType, - lineNumber: { - old: line.startsWith('+') || line.startsWith(' ') ? oldLineNum : null, - new: line.startsWith('-') || line.startsWith(' ') ? newLineNum : null - }, - content: line - }); - - if (lineType !== 'added' && line !== '') { - oldLineNum = oldLineNum ? oldLineNum + 1 : null; - } - if (lineType !== 'deleted' && line !== '') { - newLineNum = newLineNum ? newLineNum + 1 : null; - } + if (line.startsWith('+') && !line.startsWith('+++')) { lineType = 'added'; currentFile.additions++; } + else if (line.startsWith('-') && !line.startsWith('---')) { lineType = 'deleted'; currentFile.deletions++; } + currentChanges.push({ type: lineType, lineNumber: { old: line.startsWith('+') || line.startsWith(' ') ? oldLineNum : null, new: line.startsWith('-') || line.startsWith(' ') ? newLineNum : null }, content: line }); + if (lineType !== 'added' && line !== '') oldLineNum = oldLineNum ? oldLineNum + 1 : null; + if (lineType !== 'deleted' && line !== '') newLineNum = newLineNum ? newLineNum + 1 : null; } } - - if (currentFile) { - currentFile.changes = currentChanges; - files.push(currentFile); - } - + if (currentFile) { currentFile.changes = currentChanges; files.push(currentFile); } return files; } - function calculateDiffSummary(files: DiffFile[]): { filesChanged: number; additions: number; deletions: number } { - let additions = 0; - let deletions = 0; - - for (const file of files) { - additions += file.additions; - deletions += file.deletions; - } - - return { - filesChanged: files.length, - additions, - deletions - }; + let additions = 0, deletions = 0; + for (const file of files) { additions += file.additions; deletions += file.deletions; } + return { filesChanged: files.length, additions, deletions }; } -export async function mergeToMain( - basePath: string, - workspacePath: string, - agentName: string, - mainBranch: string, - message?: string, - dryRun: boolean = false -): Promise { - const git = createGit(workspacePath); +export async function mergeToMain(basePath: string, workspacePath: string, agentName: string, mainBranch: string, message?: string, dryRun: boolean = false): Promise { const workspaceGit = createGit(workspacePath); const branch = await getCurrentBranch(workspaceGit); - - const commitMessage = message || `Merge agent-${agentName} changes into ${mainBranch} - -Agent: ${agentName} -Branch: ${branch} -Date: ${new Date().toISOString()} - -This commit was automatically generated by git-agent-sync`; - - if (dryRun) { - return { - success: true, - message: `[DRY RUN] Would merge ${branch} into ${mainBranch}`, - commitSha: 'dry-run' - }; - } - + const commitMessage = message || `Merge agent-${agentName} changes into ${mainBranch}\n\nAgent: ${agentName}\nBranch: ${branch}\nDate: ${new Date().toISOString()}`; + if (dryRun) return { success: true, message: `[DRY RUN] Would merge ${branch} into ${mainBranch}`, commitSha: 'dry-run' }; await workspaceGit.checkout(mainBranch); await workspaceGit.pull(); - try { - const mergeResult = await git.raw([ - 'merge', - '--no-ff', - '-m', - commitMessage, - branch - ]); - - return { - success: true, - message: mergeResult, - commitSha: '' - }; + const git = createGit(workspacePath); + const mergeResult = await git.raw(['merge', '--no-ff', '-m', commitMessage, branch]); + return { success: true, message: mergeResult, commitSha: '' }; } catch (error: any) { if (error.message?.includes('conflict')) { const conflicts = await detectConflicts(workspacePath); - return { - success: false, - conflicts, - error: 'Merge conflicts detected' - }; + return { success: false, conflicts, error: 'Merge conflicts detected' }; } throw new GitAgentSyncError(`Merge failed: ${error.message}`); } } - async function detectConflicts(workspacePath: string): Promise { const git = createGit(workspacePath); const status = await git.status(); - const conflicts: ConflictInfo[] = []; - for (const file of status.conflicted) { const content = await fs.readFile(path.join(workspacePath, file), 'utf-8'); - conflicts.push({ - file, - content - }); + conflicts.push({ file, content }); } - return conflicts; } - export async function hasUncommittedChanges(workspacePath: string): Promise { const git = createGit(workspacePath); const status = await git.status(); - return status.modified.length > 0 || - status.created.length > 0 || - status.deleted.length > 0; + return status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0; } - export async function getLastCommitInfo(workspacePath: string): Promise<{ hash: string; message: string }> { const git = createGit(workspacePath); const log = await git.log({ maxCount: 1 }); - - if (log.latest) { - return { - hash: log.latest.hash, - message: log.latest.message || '' - }; - } - - return { - hash: '', - message: '' - }; + if (log.latest) return { hash: log.latest.hash, message: log.latest.message || '' }; + return { hash: '', message: '' }; } - -export function extractAgentName(branch: string): string { - return branch.replace(/^agent-/, ''); -} - +export function extractAgentName(branch: string): string { return branch.replace(/^agent-/, ''); } export async function stashChanges(workspacePath: string, message: string): Promise { const git = createGit(workspacePath); await git.stash(['push', '-m', message]); } - -export async function applyStash(workspacePath: string, stashRef: string): Promise { - const git = createGit(workspacePath); - await git.stash(['apply', stashRef]); -} - -export async function getStashList(workspacePath: string): Promise<{ hash: string; message: string }[]> { - const git = createGit(workspacePath); - const stashList = await git.stash(['list']); - - const stashes: { hash: string; message: string }[] = []; - const lines = stashList.split('\n').filter(Boolean); - - for (const line of lines) { - const match = line.match(/^(\w+@.+): (.+)$/); - if (match) { - stashes.push({ - hash: match[1], - message: match[2] - }); - } - } - - return stashes; -} - export async function exportChangesAsPatch(workspacePath: string, outputPath: string): Promise { const git = createGit(workspacePath); const diff = await git.diff(['--no-color', '--unified=3']); - await fs.writeFile(outputPath, diff, 'utf-8'); -} +} \ No newline at end of file