hikma-engine
Version:
Code Knowledge Graph Indexer - A sophisticated TypeScript-based indexer that transforms Git repositories into multi-dimensional knowledge stores for AI agents
391 lines (390 loc) • 15.4 kB
JavaScript
"use strict";
/**
* @file Analyzes Git repository history to extract commit information and track file evolution.
* Creates CommitNodes and PullRequestNodes with their relationships to files and other commits.
* Supports incremental analysis by processing only new commits since the last indexed state.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitAnalyzer = void 0;
const simple_git_1 = require("simple-git");
const path = __importStar(require("path"));
const logger_1 = require("../utils/logger");
const connection_1 = require("../persistence/db/connection");
const error_handling_1 = require("../utils/error-handling");
/**
* Analyzes Git repository history and extracts commit-related information.
*/
class GitAnalyzer {
/**
* @param {string} projectRoot - The absolute path to the root of the project.
* @param {ConfigManager} config - Configuration manager instance.
*/
constructor(projectRoot, config) {
this.logger = (0, logger_1.getLogger)('GitAnalyzer');
this.nodes = [];
this.edges = [];
this.projectRoot = projectRoot;
this.config = config;
this.git = (0, simple_git_1.simpleGit)(projectRoot);
const dbConfig = this.config.getDatabaseConfig();
this.sqliteClient = new connection_1.SQLiteClient(dbConfig.sqlite.path);
}
/**
* Returns the list of extracted nodes.
* @returns {(CommitNode | PullRequestNode)[]} An array of nodes.
*/
getNodes() {
return this.nodes;
}
/**
* Returns the list of extracted edges.
* @returns {Edge[]} An array of edges.
*/
getEdges() {
return this.edges;
}
/**
* Gets the current HEAD commit hash.
* @returns {Promise<string | null>} The current commit hash or null if not found.
*/
async getCurrentCommitHash() {
try {
const log = await this.git.log(['-1']);
return log.latest?.hash || null;
}
catch (error) {
this.logger.warn('Failed to get current commit hash', { error: (0, error_handling_1.getErrorMessage)(error) });
return null;
}
}
/**
* Gets the last indexed commit hash from the database.
* @returns {Promise<string | null>} The last indexed commit hash or null if not found.
*/
async getLastIndexedCommit() {
try {
await this.sqliteClient.connect();
const result = this.sqliteClient.getLastIndexedCommit();
await this.sqliteClient.disconnect();
return result;
}
catch (error) {
this.logger.warn('Failed to get last indexed commit', { error: (0, error_handling_1.getErrorMessage)(error) });
return null;
}
}
/**
* Sets the last indexed commit hash in the database.
* @param {string} commitHash - The commit hash to store.
* @returns {Promise<void>}
*/
async setLastIndexedCommit(commitHash) {
try {
await this.sqliteClient.connect();
await this.sqliteClient.setLastIndexedCommit(commitHash);
await this.sqliteClient.disconnect();
this.logger.debug('Updated last indexed commit', { commitHash });
}
catch (error) {
this.logger.error('Failed to set last indexed commit', { error: (0, error_handling_1.getErrorMessage)(error) });
throw error;
}
}
/**
* Gets the list of files that have changed between two commits.
* @param {string} fromCommit - The starting commit hash.
* @param {string} toCommit - The ending commit hash.
* @returns {Promise<string[]>} Array of changed file paths.
*/
async getChangedFiles(fromCommit, toCommit) {
try {
const diff = await this.git.diff([`${fromCommit}..${toCommit}`, '--name-only']);
const changedFiles = diff
.split('\n')
.filter(line => line.trim() !== '')
.map(file => path.resolve(this.projectRoot, file));
this.logger.debug('Found changed files', {
fromCommit,
toCommit,
count: changedFiles.length
});
return changedFiles;
}
catch (error) {
this.logger.error('Failed to get changed files', {
error: (0, error_handling_1.getErrorMessage)(error),
fromCommit,
toCommit
});
throw error;
}
}
/**
* Creates a unique ID for a commit node.
* @param {string} hash - The commit hash.
* @returns {string} A unique identifier.
*/
createCommitNodeId(hash) {
return `commit:${hash}`;
}
/**
* Creates a unique ID for a pull request node.
* @param {string} prId - The pull request ID.
* @returns {string} A unique identifier.
*/
createPullRequestNodeId(prId) {
return `pr:${prId}`;
}
/**
* Extracts commit information and creates CommitNodes.
* @param {string | null} sinceCommit - Optional commit hash to start from (for incremental analysis).
* @returns {Promise<void>}
*/
async extractCommits(sinceCommit) {
try {
const logOptions = ['--all'];
if (sinceCommit) {
logOptions.push(`${sinceCommit}..HEAD`);
this.logger.info('Extracting commits since last indexed commit', { sinceCommit });
}
else {
this.logger.info('Extracting all commits (full analysis)');
}
const log = await this.git.log(logOptions);
this.logger.info(`Found ${log.all.length} commits to process`);
for (const commit of log.all) {
const commitNode = {
id: this.createCommitNodeId(commit.hash),
type: 'CommitNode',
properties: {
hash: commit.hash,
author: commit.author_name,
date: commit.date,
message: commit.message,
diffSummary: await this.getCommitDiffSummary(commit.hash),
},
};
this.nodes.push(commitNode);
}
}
catch (error) {
this.logger.error('Failed to extract commits', { error: (0, error_handling_1.getErrorMessage)(error) });
throw error;
}
}
/**
* Gets a summary of changes in a commit.
* @param {string} commitHash - The commit hash.
* @returns {Promise<string>} A summary of the changes.
*/
async getCommitDiffSummary(commitHash) {
try {
const diffStat = await this.git.show([commitHash, '--stat', '--format=']);
return diffStat.trim();
}
catch (error) {
this.logger.warn(`Failed to get diff summary for commit ${commitHash}`, { error: (0, error_handling_1.getErrorMessage)(error) });
return 'Unable to generate diff summary';
}
}
/**
* Creates edges between commits and the files they modified.
* @param {FileNode[]} fileNodes - Array of file nodes to link with commits.
* @returns {Promise<void>}
*/
async createCommitFileEdges(fileNodes) {
const operation = this.logger.operation('Creating commit-file edges');
try {
for (const commitNode of this.nodes.filter(n => n.type === 'CommitNode')) {
try {
// Get files modified in this commit
const diff = await this.git.show([commitNode.properties.hash, '--name-only', '--format=']);
const modifiedFiles = diff
.split('\n')
.filter(line => line.trim() !== '')
.map(file => path.resolve(this.projectRoot, file));
// Create MODIFIED edges
for (const modifiedFile of modifiedFiles) {
const fileNode = fileNodes.find(f => path.resolve(this.projectRoot, f.properties.filePath) === modifiedFile);
if (fileNode) {
this.edges.push({
source: commitNode.id,
target: fileNode.id,
type: 'MODIFIED',
});
// Also create EVOLVED_BY edge (reverse relationship)
this.edges.push({
source: fileNode.id,
target: commitNode.id,
type: 'EVOLVED_BY',
});
}
}
}
catch (error) {
this.logger.warn(`Failed to process commit ${commitNode.properties.hash}`, { error: (0, error_handling_1.getErrorMessage)(error) });
}
}
operation();
}
catch (error) {
this.logger.error('Failed to create commit-file edges', { error: (0, error_handling_1.getErrorMessage)(error) });
operation();
throw error;
}
}
/**
* Creates mock pull request nodes (placeholder for future GitHub/GitLab integration).
* @returns {Promise<void>}
*/
async createMockPullRequests() {
// TODO: Integrate with GitHub/GitLab API to fetch actual pull requests
this.logger.debug('Creating mock pull request nodes (placeholder implementation)');
// Create a few mock PRs for demonstration
const mockPRs = [
{
prId: 'mock-pr-1',
title: 'Add new feature X',
author: 'developer1',
createdAt: new Date().toISOString(),
url: 'https://github.com/example/repo/pull/1',
body: 'This PR adds feature X to improve functionality.',
},
{
prId: 'mock-pr-2',
title: 'Fix bug in component Y',
author: 'developer2',
createdAt: new Date().toISOString(),
url: 'https://github.com/example/repo/pull/2',
body: 'This PR fixes a critical bug in component Y.',
},
];
for (const mockPR of mockPRs) {
const prNode = {
id: this.createPullRequestNodeId(mockPR.prId),
type: 'PullRequestNode',
properties: mockPR,
};
this.nodes.push(prNode);
// Create INCLUDES_COMMIT edges (mock - link to recent commits)
const recentCommits = this.nodes
.filter(n => n.type === 'CommitNode')
.slice(0, 2);
for (const commit of recentCommits) {
this.edges.push({
source: prNode.id,
target: commit.id,
type: 'INCLUDES_COMMIT',
});
}
}
}
/**
* Analyzes the Git repository and extracts commit and pull request information.
* @param {FileNode[]} fileNodes - Array of file nodes to link with commits.
* @param {string | null} lastIndexedCommit - The last indexed commit hash for incremental analysis.
* @returns {Promise<void>}
*/
async analyzeRepo(fileNodes, lastIndexedCommit = null) {
const operation = this.logger.operation('Git repository analysis');
try {
this.logger.info('Starting Git repository analysis', {
projectRoot: this.projectRoot,
fileCount: fileNodes.length,
incremental: !!lastIndexedCommit,
});
// Reset state
this.nodes = [];
this.edges = [];
// Check if we're in a Git repository
const isRepo = await this.git.checkIsRepo();
if (!isRepo) {
this.logger.warn('Not a Git repository, skipping Git analysis');
operation();
return;
}
// Extract commits
await this.extractCommits(lastIndexedCommit);
// Create commit-file relationships
await this.createCommitFileEdges(fileNodes);
// Create mock pull requests (placeholder)
await this.createMockPullRequests();
this.logger.info('Git analysis completed', {
totalNodes: this.nodes.length,
totalEdges: this.edges.length,
nodeTypes: this.getNodeTypeStats(),
});
operation();
}
catch (error) {
this.logger.error('Git analysis failed', { error: (0, error_handling_1.getErrorMessage)(error) });
operation();
throw error;
}
}
/**
* Gets statistics about the analyzed nodes by type.
* @returns {Record<string, number>} Node type statistics.
*/
getNodeTypeStats() {
const stats = {};
for (const node of this.nodes) {
stats[node.type] = (stats[node.type] || 0) + 1;
}
return stats;
}
/**
* Gets repository statistics.
* @returns {Promise<{totalCommits: number, authors: string[], latestCommit: string | null}>}
*/
async getRepoStats() {
try {
const log = await this.git.log();
const authors = [...new Set(log.all.map(commit => commit.author_name))];
const latestCommit = log.latest?.hash || null;
return {
totalCommits: log.total,
authors,
latestCommit,
};
}
catch (error) {
this.logger.error('Failed to get repository stats', { error: (0, error_handling_1.getErrorMessage)(error) });
throw error;
}
}
}
exports.GitAnalyzer = GitAnalyzer;