From aae2360620a99bca11617ebffdb143128b493418 Mon Sep 17 00:00:00 2001 From: waleedcodes <110061477+waleedcodes@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:19:20 +0500 Subject: [PATCH 1/3] feat(git): initialize GitService class and shell runner --- src/git/gitService.ts | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/git/gitService.ts diff --git a/src/git/gitService.ts b/src/git/gitService.ts new file mode 100644 index 0000000..54354d1 --- /dev/null +++ b/src/git/gitService.ts @@ -0,0 +1,61 @@ +import { exec } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { promisify } from 'node:util'; +import { DiffParser } from './diffParser'; +import { GitDiffResult, GitFileStatus } from '../types'; + +const execAsync = promisify(exec); + +export interface GitServiceOptions { + cwd?: string; + maxBuffer?: number; +} + +export class GitService { + private cwd: string; + private maxBuffer: number; + + constructor(options: GitServiceOptions = {}) { + this.cwd = options.cwd || process.cwd(); + this.maxBuffer = options.maxBuffer || 10 * 1024 * 1024; // 10MB + } + + public async getRepoRoot(): Promise { + try { + const { stdout } = await execAsync('git rev-parse --show-toplevel', { + cwd: this.cwd, + maxBuffer: this.maxBuffer + }); + return stdout.trim(); + } catch { + return this.cwd; + } + } + + public async getCurrentBranch(repoRoot?: string): Promise { + const cwd = repoRoot || this.cwd; + try { + const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { + cwd, + maxBuffer: this.maxBuffer + }); + return stdout.trim(); + } catch { + return 'HEAD'; + } + } + + public async getStatus(repoRoot?: string): Promise { + const cwd = repoRoot || this.cwd; + try { + const { stdout } = await execAsync('git status --porcelain -uall', { + cwd, + maxBuffer: this.maxBuffer + }); + + const results: GitFileStatus[] = []; + const lines = stdout.split('\n'); + + for (const rawLine of lines) { + if (!rawLine || rawLine.trim().length === 0) continue; From af01357627f5e522c181a4ee43330f1dccc628be Mon Sep 17 00:00:00 2001 From: waleedcodes <110061477+waleedcodes@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:19:20 +0500 Subject: [PATCH 2/3] feat(git): implement branch discovery and porcelain status parser --- src/git/gitService.ts | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 54354d1..4522b73 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -59,3 +59,73 @@ export class GitService { for (const rawLine of lines) { if (!rawLine || rawLine.trim().length === 0) continue; + const indexStatus = rawLine[0]; + const workTreeStatus = rawLine[1]; + const filePath = rawLine.substring(3).trim(); + + let status: GitFileStatus['status'] = 'modified'; + if (indexStatus === '?' || workTreeStatus === '?') { + status = 'untracked'; + } else if (indexStatus === 'A' || workTreeStatus === 'A') { + status = 'added'; + } else if (indexStatus === 'D' || workTreeStatus === 'D') { + status = 'deleted'; + } else if (indexStatus === 'R' || workTreeStatus === 'R') { + status = 'renamed'; + } + + results.push({ path: filePath, status }); + } + + return results; + } catch { + return []; + } + } + + public async getDiff(options?: { + staged?: boolean; + baseRef?: string; + repoRoot?: string; + }): Promise { + const repoRoot = options?.repoRoot || (await this.getRepoRoot()); + const parts: string[] = ['git diff']; + + if (options?.staged) { + parts.push('--cached'); + } else if (options?.baseRef) { + parts.push(options.baseRef); + } else { + // By default compare against HEAD to include both staged and unstaged tracked changes + // Check if repository has any commits yet + try { + await execAsync('git rev-parse HEAD', { cwd: repoRoot }); + parts.push('HEAD'); + } catch { + // Empty repository, diff against empty tree + parts.push('4b825dc642cb6eb9a060e54bf8d69288fbee4904'); + } + } + + let diffOutput = ''; + try { + const { stdout } = await execAsync(parts.join(' '), { + cwd: repoRoot, + maxBuffer: this.maxBuffer + }); + diffOutput = stdout; + } catch (err: unknown) { + // Fallback to simple git diff if HEAD ref fails + try { + const { stdout } = await execAsync('git diff', { + cwd: repoRoot, + maxBuffer: this.maxBuffer + }); + diffOutput = stdout; + } catch { + diffOutput = ''; + } + } + + const parsed = DiffParser.parse(diffOutput, repoRoot); + From 0222d86de61e3a885ec4f5c35516d85db172d22f Mon Sep 17 00:00:00 2001 From: waleedcodes <110061477+waleedcodes@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:19:20 +0500 Subject: [PATCH 3/3] feat(git): handle untracked files with synthetic diff generation --- src/git/gitService.ts | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 4522b73..f7bbdc3 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -129,3 +129,47 @@ export class GitService { const parsed = DiffParser.parse(diffOutput, repoRoot); + // Also check for untracked files and synthesize file diffs so new AI-created files are fully verified! + const statuses = await this.getStatus(repoRoot); + const untracked = statuses.filter((s) => s.status === 'untracked'); + + for (const item of untracked) { + const fullPath = path.resolve(repoRoot, item.path); + try { + const stat = await fs.promises.stat(fullPath); + if (stat.isFile()) { + const content = await fs.promises.readFile(fullPath, 'utf8'); + const fileLines = content.split('\n'); + const addedLines = fileLines.map((_, idx) => idx + 1); + + parsed.files.push({ + oldPath: item.path, + newPath: item.path, + isNew: true, + isDeleted: false, + isRenamed: false, + hunks: [ + { + oldStart: 0, + oldLines: 0, + newStart: 1, + newLines: fileLines.length, + header: `@@ -0,0 +1,${fileLines.length} @@`, + lines: fileLines.map((l) => `+${l}`) + } + ], + addedLines, + deletedLines: [], + modifiedLineRanges: [{ start: 1, end: fileLines.length }] + }); + parsed.totalFilesChanged++; + parsed.totalInsertions += fileLines.length; + } + } catch { + // Skip unreadable or directory items + } + } + + return parsed; + } +}