You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
4.7 KiB
171 lines
4.7 KiB
/** |
|
* Commit operations module |
|
* Handles commit history and diff operations |
|
*/ |
|
|
|
import simpleGit, { type SimpleGit } from 'simple-git'; |
|
import logger from '../../logger.js'; |
|
import { sanitizeError } from '../../../utils/security.js'; |
|
import { validateRepoName, validateNpub } from './path-validator.js'; |
|
import type { Commit, Diff } from '../file-manager.js'; |
|
|
|
export interface CommitHistoryOptions { |
|
npub: string; |
|
repoName: string; |
|
branch?: string; |
|
limit?: number; |
|
path?: string; |
|
repoPath: string; |
|
} |
|
|
|
export interface DiffOptions { |
|
npub: string; |
|
repoName: string; |
|
fromRef: string; |
|
toRef?: string; |
|
filePath?: string; |
|
repoPath: string; |
|
} |
|
|
|
/** |
|
* Get commit history |
|
*/ |
|
export async function getCommitHistory(options: CommitHistoryOptions): Promise<Commit[]> { |
|
const { npub, repoName, branch = 'main', limit = 50, path, repoPath } = options; |
|
|
|
// Validate inputs |
|
const npubValidation = validateNpub(npub); |
|
if (!npubValidation.valid) { |
|
throw new Error(`Invalid npub: ${npubValidation.error}`); |
|
} |
|
const repoValidation = validateRepoName(repoName); |
|
if (!repoValidation.valid) { |
|
throw new Error(`Invalid repository name: ${repoValidation.error}`); |
|
} |
|
|
|
const git: SimpleGit = simpleGit(repoPath); |
|
|
|
try { |
|
logger.operation('Getting commit history', { npub, repoName, branch, limit, path }); |
|
|
|
const logOptions: { |
|
maxCount: number; |
|
from: string; |
|
file?: string; |
|
} = { |
|
maxCount: limit, |
|
from: branch |
|
}; |
|
|
|
if (path) { |
|
logOptions.file = path; |
|
} |
|
|
|
const log = await git.log(logOptions); |
|
|
|
const commits = log.all.map(commit => ({ |
|
hash: commit.hash, |
|
message: commit.message, |
|
author: `${commit.author_name} <${commit.author_email}>`, |
|
date: commit.date, |
|
files: commit.diff?.files?.map((f: { file: string }) => f.file) || [] |
|
})); |
|
|
|
logger.operation('Commit history retrieved', { npub, repoName, count: commits.length }); |
|
return commits; |
|
} catch (error) { |
|
logger.error({ error, repoPath, branch, limit }, 'Error getting commit history'); |
|
throw new Error(`Failed to get commit history: ${error instanceof Error ? error.message : String(error)}`); |
|
} |
|
} |
|
|
|
/** |
|
* Get diff between two commits or for a file |
|
*/ |
|
export async function getDiff(options: DiffOptions): Promise<Diff[]> { |
|
const { npub, repoName, fromRef, toRef = 'HEAD', filePath, repoPath } = options; |
|
|
|
// Validate inputs |
|
const npubValidation = validateNpub(npub); |
|
if (!npubValidation.valid) { |
|
throw new Error(`Invalid npub: ${npubValidation.error}`); |
|
} |
|
const repoValidation = validateRepoName(repoName); |
|
if (!repoValidation.valid) { |
|
throw new Error(`Invalid repository name: ${repoValidation.error}`); |
|
} |
|
|
|
const git: SimpleGit = simpleGit(repoPath); |
|
|
|
try { |
|
logger.operation('Getting diff', { npub, repoName, fromRef, toRef, filePath }); |
|
|
|
const diffOptions: string[] = [fromRef, toRef]; |
|
if (filePath) { |
|
diffOptions.push('--', filePath); |
|
} |
|
|
|
const [diff, stats] = await Promise.all([ |
|
git.diff(diffOptions), |
|
git.diffSummary(diffOptions) |
|
]); |
|
|
|
// Parse diff output |
|
const files: Diff[] = []; |
|
const diffLines = diff.split('\n'); |
|
let currentFile = ''; |
|
let currentDiff = ''; |
|
let inFileHeader = false; |
|
|
|
for (const line of diffLines) { |
|
if (line.startsWith('diff --git')) { |
|
if (currentFile) { |
|
files.push({ |
|
file: currentFile, |
|
additions: 0, |
|
deletions: 0, |
|
diff: currentDiff |
|
}); |
|
} |
|
const match = line.match(/diff --git a\/(.+?) b\/(.+?)$/); |
|
if (match) { |
|
currentFile = match[2]; |
|
currentDiff = line + '\n'; |
|
inFileHeader = true; |
|
} |
|
} else { |
|
currentDiff += line + '\n'; |
|
if (line.startsWith('@@')) { |
|
inFileHeader = false; |
|
} |
|
} |
|
} |
|
|
|
if (currentFile) { |
|
files.push({ |
|
file: currentFile, |
|
additions: 0, |
|
deletions: 0, |
|
diff: currentDiff |
|
}); |
|
} |
|
|
|
// Add stats from diffSummary |
|
if (stats.files && files.length > 0) { |
|
for (const statFile of stats.files) { |
|
const file = files.find(f => f.file === statFile.file); |
|
if (file && 'insertions' in statFile && 'deletions' in statFile) { |
|
file.additions = statFile.insertions; |
|
file.deletions = statFile.deletions; |
|
} |
|
} |
|
} |
|
|
|
logger.operation('Diff retrieved', { npub, repoName, fileCount: files.length }); |
|
return files; |
|
} catch (error) { |
|
const sanitizedError = sanitizeError(error); |
|
logger.error({ error: sanitizedError, repoPath, fromRef, toRef }, 'Error getting diff'); |
|
throw new Error(`Failed to get diff: ${sanitizedError}`); |
|
} |
|
}
|
|
|