@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
311 lines • 9.01 kB
JavaScript
/**
* Git Context Service for AI-Trail Pattern Correlation
*
* Detects git context from the current working directory for
* correlating execution traces with commits.
*
* @package @ace-sdk/cli
* @since v2.2.0
* @see https://github.com/ce-dot-net/ace-sdk/issues/16
*/
import { execSync } from 'child_process';
/**
* Service for detecting git context from the current working directory.
*
* Provides graceful fallback when not in a git repository.
* All git commands use child_process.execSync for consistency
* with existing patterns in the codebase.
*/
export class GitContextService {
cwd;
cachedContext = null;
cacheTimestamp = 0;
cacheTtlMs;
/**
* Create a new GitContextService
*
* @param cwd Working directory (defaults to process.cwd())
* @param cacheTtlMs Cache TTL in milliseconds (defaults to 60000 = 1 minute)
*/
constructor(cwd, cacheTtlMs = 60000) {
this.cwd = cwd || process.cwd();
this.cacheTtlMs = cacheTtlMs;
}
/**
* Check if current directory is a git repository
*/
isGitRepository() {
try {
execSync('git rev-parse --git-dir', {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
});
return true;
}
catch {
return false;
}
}
/**
* Get current HEAD commit hash
*/
getCurrentHead() {
try {
return execSync('git rev-parse HEAD', {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
}
catch {
return null;
}
}
/**
* Get current branch name
*/
getBranch() {
try {
return execSync('git rev-parse --abbrev-ref HEAD', {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
}
catch {
return null;
}
}
/**
* Get commit message for a given commit (first line)
*/
getCommitMessage(commitHash) {
try {
const ref = commitHash || 'HEAD';
return execSync(`git log -1 --format=%s ${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
}
catch {
return null;
}
}
/**
* Get commit timestamp in ISO 8601 format
*/
getCommitTimestamp(commitHash) {
try {
const ref = commitHash || 'HEAD';
return execSync(`git log -1 --format=%aI ${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
}
catch {
return null;
}
}
/**
* Get commit author name
*/
getCommitAuthor(commitHash) {
try {
const ref = commitHash || 'HEAD';
return execSync(`git log -1 --format=%an ${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
}
catch {
return null;
}
}
/**
* Get commit author email
*/
getCommitAuthorEmail(commitHash) {
try {
const ref = commitHash || 'HEAD';
return execSync(`git log -1 --format=%ae ${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
}
catch {
return null;
}
}
/**
* Get files changed in a commit
*/
getChangedFiles(commitHash) {
try {
const ref = commitHash || 'HEAD';
const output = execSync(`git diff-tree --no-commit-id --name-only -r ${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
return output ? output.split('\n').filter(Boolean) : [];
}
catch {
return [];
}
}
/**
* Get parent commit hashes
*/
getParentCommits(commitHash) {
try {
const ref = commitHash || 'HEAD';
const output = execSync(`git log -1 --format=%P ${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
return output ? output.split(' ').filter(Boolean) : [];
}
catch {
return [];
}
}
/**
* Get insertions and deletions for a commit
*/
getCommitStats(commitHash) {
try {
const ref = commitHash || 'HEAD';
const output = execSync(`git diff --stat --numstat ${ref}~1..${ref}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
let insertions = 0;
let deletions = 0;
// Parse numstat output: each line is "insertions deletions filename"
for (const line of output.split('\n')) {
const parts = line.split('\t');
if (parts.length >= 2) {
const ins = parseInt(parts[0], 10);
const del = parseInt(parts[1], 10);
if (!isNaN(ins))
insertions += ins;
if (!isNaN(del))
deletions += del;
}
}
return { insertions, deletions };
}
catch {
return null;
}
}
/**
* Detect full git context from current working directory.
*
* Returns null if not in a git repository.
* Caches result for cacheTtlMs to avoid repeated git calls.
*
* @param commitHash Optional specific commit (defaults to HEAD)
* @param includePrivate Include author email (default: false for privacy)
*/
detectContext(commitHash, includePrivate = false) {
// Check cache
const now = Date.now();
if (this.cachedContext && (now - this.cacheTimestamp) < this.cacheTtlMs) {
return this.cachedContext;
}
// Check if git repo
if (!this.isGitRepository()) {
return null;
}
// Get basic info
const hash = commitHash || this.getCurrentHead();
const branch = this.getBranch();
if (!hash || !branch) {
return null;
}
// Build context
const context = {
commit_hash: hash,
branch,
files_changed: this.getChangedFiles(hash),
commit_message: this.getCommitMessage(hash) || undefined,
timestamp: this.getCommitTimestamp(hash) || undefined,
author: this.getCommitAuthor(hash) || undefined,
parent_commits: this.getParentCommits(hash)
};
// Include email only if explicitly requested
if (includePrivate) {
context.author_email = this.getCommitAuthorEmail(hash) || undefined;
}
// Get stats (may fail for initial commit)
const stats = this.getCommitStats(hash);
if (stats) {
context.insertions = stats.insertions;
context.deletions = stats.deletions;
}
// Cache result
this.cachedContext = context;
this.cacheTimestamp = now;
return context;
}
/**
* Get commits made since a specific time.
* Useful for finding commits made during a session.
*
* @param since Date to search from
* @param limit Maximum commits to return (default: 10)
*/
getCommitsSince(since, limit = 10) {
if (!this.isGitRepository()) {
return [];
}
try {
const sinceIso = since.toISOString();
const output = execSync(`git log --since="${sinceIso}" --format=%H -n ${limit}`, {
cwd: this.cwd,
stdio: 'pipe',
encoding: 'utf8'
}).trim();
if (!output) {
return [];
}
const hashes = output.split('\n').filter(Boolean);
return hashes
.map(hash => this.detectContext(hash))
.filter((ctx) => ctx !== null);
}
catch {
return [];
}
}
/**
* Clear the cached context
*/
clearCache() {
this.cachedContext = null;
this.cacheTimestamp = 0;
}
}
/**
* Create a GitContextService for the current directory
*/
export function createGitContextService(cwd) {
return new GitContextService(cwd);
}
/**
* Convenience function to detect git context from cwd.
* Returns null if not in a git repository.
*/
export function detectGitContext(cwd) {
const service = new GitContextService(cwd);
return service.detectContext();
}
//# sourceMappingURL=git-context.js.map