/** * Git PR Context Gatherer * * Extracts PR context from git branches for validation */ import { execSync } from 'child_process'; import { PRContext, FileChange } from './types'; import { JiraClient } from '@nihal1983/context-gatherer'; export interface GitPRContextOptions { repoPath: string; branch: string; baseBranch?: string; jiraClient?: JiraClient; } export class GitPRContextGatherer { private repoPath: string; private branch: string; private baseBranch: string; private jiraClient?: JiraClient; constructor(options: GitPRContextOptions) { this.repoPath = options.repoPath; this.branch = options.branch; this.baseBranch = options.baseBranch || 'master'; this.jiraClient = options.jiraClient; } /** * Gather full PR context from git branch */ async gatherContext(): Promise { // Get commit message from branch const commitMessage = this.getCommitMessage(); const { title, description } = this.parseCommitMessage(commitMessage); // Extract ticket ID from commit message or branch name let ticketId = this.extractTicketId(commitMessage); if (!ticketId) { ticketId = this.extractTicketId(this.branch); } let ticket = null; if (ticketId && this.jiraClient) { try { ticket = await this.jiraClient.getTicket(ticketId); } catch (error: any) { console.warn(`Could not fetch Jira ticket ${ticketId}:`, error.message); } } // Get changed files const files = this.getChangedFiles(); return { pr: { number: 1, // Simulated PR number title, description, branch: this.branch, base_branch: this.baseBranch }, ticket, files }; } /** * Get commit message from branch HEAD */ private getCommitMessage(): string { try { const message = execSync( `git log -1 --pretty=format:"%s%n%n%b"`, { cwd: this.repoPath, encoding: 'utf-8' } ); return message.trim(); } catch (error: any) { throw new Error(`Failed to get commit message: ${error.message}`); } } /** * Parse commit message into title and description */ private parseCommitMessage(message: string): { title: string; description: string } { const lines = message.split('\n'); const title = lines[0] || ''; const description = lines.slice(1).join('\n').trim(); return { title, description }; } /** * Extract ticket ID from commit message */ private extractTicketId(message: string): string | null { // Look for patterns like PB-123, PROJ-456, etc. const match = message.match(/([A-Z]+-\d+)/); return match ? match[1]! : null; } /** * Get list of changed files with content */ private getChangedFiles(): FileChange[] { const files: FileChange[] = []; try { // Get list of changed files const changedFilesOutput = execSync( `git diff --name-status ${this.baseBranch}...${this.branch}`, { cwd: this.repoPath, encoding: 'utf-8' } ); const lines = changedFilesOutput.trim().split('\n').filter(l => l); for (const line of lines) { const [status, filePath] = line.split('\t'); if (!filePath) continue; let fileStatus: FileChange['status']; switch (status) { case 'A': fileStatus = 'added'; break; case 'M': fileStatus = 'modified'; break; case 'D': fileStatus = 'deleted'; break; case 'R': fileStatus = 'renamed'; break; default: fileStatus = 'modified'; } // Get file content (if not deleted) let content: string | undefined; if (fileStatus !== 'deleted') { try { // Use git show to get content from the branch content = execSync( `git show ${this.branch}:"${filePath}"`, { cwd: this.repoPath, encoding: 'utf-8' } ); } catch (error: any) { console.warn(`Could not read file ${filePath} from branch ${this.branch}:`, error.message); } } // Get additions/deletions count const { additions, deletions } = this.getFileStats(filePath); files.push({ path: filePath, status: fileStatus, additions, deletions, content }); } return files; } catch (error: any) { throw new Error(`Failed to get changed files: ${error.message}`); } } /** * Get file statistics (additions/deletions) */ private getFileStats(filePath: string): { additions: number; deletions: number } { try { const stats = execSync( `git diff --numstat ${this.baseBranch}...${this.branch} -- "${filePath}"`, { cwd: this.repoPath, encoding: 'utf-8' } ); const match = stats.trim().match(/^(\d+)\s+(\d+)/); if (match) { return { additions: parseInt(match[1]!, 10), deletions: parseInt(match[2]!, 10) }; } return { additions: 0, deletions: 0 }; } catch (error) { return { additions: 0, deletions: 0 }; } } /** * Get full diff for context */ getDiff(): string { try { return execSync( `git diff ${this.baseBranch}...${this.branch}`, { cwd: this.repoPath, encoding: 'utf-8' } ); } catch (error) { return ''; } } }