231 lines
8.9 KiB
TypeScript
231 lines
8.9 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { AiDoc } from '../classes.aidoc.js';
|
|
import { ProjectContext } from './projectcontext.js';
|
|
import { DiffProcessor } from '../context/diff-processor.js';
|
|
|
|
export interface INextCommitObject {
|
|
recommendedNextVersionLevel: 'fix' | 'feat' | 'BREAKING CHANGE'; // the recommended next version level of the project
|
|
recommendedNextVersionScope: string; // the recommended scope name of the next version, like "core" or "cli", or specific class names.
|
|
recommendedNextVersionMessage: string; // the commit message. Don't put fix() feat() or BREAKING CHANGE in the message. Please just the message itself.
|
|
recommendedNextVersionDetails: string[]; // detailed bullet points for the changelog
|
|
recommendedNextVersion: string; // the recommended next version of the project, x.x.x
|
|
changelog?: string; // the changelog for the next version
|
|
}
|
|
|
|
export class Commit {
|
|
private aiDocsRef: AiDoc;
|
|
private projectDir: string;
|
|
|
|
constructor(aiDocsRef: AiDoc, projectDirArg: string) {
|
|
this.aiDocsRef = aiDocsRef;
|
|
this.projectDir = projectDirArg;
|
|
}
|
|
|
|
public async buildNextCommitObject(): Promise<INextCommitObject> {
|
|
const smartgitInstance = new plugins.smartgit.Smartgit();
|
|
await smartgitInstance.init();
|
|
const gitRepo = await plugins.smartgit.GitRepo.fromOpeningRepoDir(
|
|
smartgitInstance,
|
|
this.projectDir
|
|
);
|
|
|
|
// Define comprehensive exclusion patterns
|
|
// smartgit@3.3.0+ supports glob patterns natively
|
|
const excludePatterns = [
|
|
// Lock files
|
|
'pnpm-lock.yaml',
|
|
'package-lock.json',
|
|
'npm-shrinkwrap.json',
|
|
'yarn.lock',
|
|
'deno.lock',
|
|
'bun.lockb',
|
|
|
|
// Build artifacts (main culprit for large diffs!)
|
|
'dist/**',
|
|
'dist_*/**', // dist_ts, dist_web, etc.
|
|
'build/**',
|
|
'.next/**',
|
|
'out/**',
|
|
'public/dist/**',
|
|
|
|
// Compiled/bundled files
|
|
'**/*.js.map',
|
|
'**/*.d.ts.map',
|
|
'**/*.min.js',
|
|
'**/*.bundle.js',
|
|
'**/*.chunk.js',
|
|
|
|
// IDE/Editor directories
|
|
'.claude/**',
|
|
'.cursor/**',
|
|
'.vscode/**',
|
|
'.idea/**',
|
|
'**/*.swp',
|
|
'**/*.swo',
|
|
|
|
// Logs and caches
|
|
'.nogit/**',
|
|
'**/*.log',
|
|
'.cache/**',
|
|
'.rpt2_cache/**',
|
|
'coverage/**',
|
|
'.nyc_output/**',
|
|
];
|
|
|
|
// Pass glob patterns directly to smartgit - it handles matching internally
|
|
const diffStringArray = await gitRepo.getUncommittedDiff(excludePatterns);
|
|
|
|
// Process diffs intelligently using DiffProcessor
|
|
let processedDiffString: string;
|
|
|
|
if (diffStringArray.length > 0) {
|
|
// Diagnostic logging for raw diff statistics
|
|
const totalChars = diffStringArray.join('\n\n').length;
|
|
const estimatedTokens = Math.ceil(totalChars / 4);
|
|
|
|
console.log(`📊 Raw git diff statistics:`);
|
|
console.log(` Files changed: ${diffStringArray.length}`);
|
|
console.log(` Total characters: ${totalChars.toLocaleString()}`);
|
|
console.log(` Estimated tokens: ${estimatedTokens.toLocaleString()}`);
|
|
console.log(` Exclusion patterns: ${excludePatterns.length}`);
|
|
|
|
// Use DiffProcessor to intelligently handle large diffs
|
|
const diffProcessor = new DiffProcessor({
|
|
maxDiffTokens: 100000, // Reserve 100k tokens for diffs
|
|
smallFileLines: 50, // Include files <= 50 lines fully
|
|
mediumFileLines: 200, // Summarize files <= 200 lines
|
|
sampleHeadLines: 20, // Show first 20 lines
|
|
sampleTailLines: 20, // Show last 20 lines
|
|
});
|
|
|
|
const processedDiff = diffProcessor.processDiffs(diffStringArray);
|
|
processedDiffString = diffProcessor.formatForContext(processedDiff);
|
|
|
|
console.log(`📝 Processed diff statistics:`);
|
|
console.log(` Full diffs: ${processedDiff.fullDiffs.length} files`);
|
|
console.log(` Summarized: ${processedDiff.summarizedDiffs.length} files`);
|
|
console.log(` Metadata only: ${processedDiff.metadataOnly.length} files`);
|
|
console.log(` Final tokens: ${processedDiff.totalTokens.toLocaleString()}`);
|
|
|
|
if (estimatedTokens > 50000) {
|
|
console.log(`✅ DiffProcessor reduced token usage: ${estimatedTokens.toLocaleString()} → ${processedDiff.totalTokens.toLocaleString()}`);
|
|
}
|
|
} else {
|
|
processedDiffString = 'No changes.';
|
|
}
|
|
|
|
// Use the new TaskContextFactory for optimized context
|
|
const taskContextFactory = new (await import('../context/index.js')).TaskContextFactory(
|
|
this.projectDir,
|
|
this.aiDocsRef.openaiInstance
|
|
);
|
|
await taskContextFactory.initialize();
|
|
|
|
// Generate context specifically for commit task
|
|
const contextResult = await taskContextFactory.createContextForCommit(processedDiffString);
|
|
|
|
// Get the optimized context string
|
|
let contextString = contextResult.context;
|
|
|
|
// Log token usage statistics
|
|
console.log(`Token usage - Context: ${contextResult.tokenCount}, Files: ${contextResult.includedFiles.length + contextResult.trimmedFiles.length}, Savings: ${contextResult.tokenSavings}`);
|
|
|
|
// Check for token overflow against model limits
|
|
const MODEL_TOKEN_LIMIT = 200000; // o4-mini
|
|
if (contextResult.tokenCount > MODEL_TOKEN_LIMIT * 0.9) {
|
|
console.log(`⚠️ Warning: Context size (${contextResult.tokenCount} tokens) is close to or exceeds model limit (${MODEL_TOKEN_LIMIT} tokens).`);
|
|
console.log(`The model may not be able to process all information effectively.`);
|
|
}
|
|
|
|
let result = await this.aiDocsRef.openaiInstance.chat({
|
|
systemMessage: `
|
|
You create a commit message for a git commit.
|
|
The commit message should be based on the files in the project.
|
|
You should not include any licensing information.
|
|
You should not include any personal information.
|
|
|
|
Important: Answer only in valid JSON.
|
|
|
|
Your answer should be parseable with JSON.parse() without modifying anything.
|
|
|
|
Here is the structure of the JSON you should return:
|
|
|
|
interface {
|
|
recommendedNextVersionLevel: 'fix' | 'feat' | 'BREAKING CHANGE'; // the recommended next version level of the project
|
|
recommendedNextVersionScope: string; // the recommended scope name of the next version, like "core" or "cli", or specific class names.
|
|
recommendedNextVersionMessage: string; // the commit message. Don't put fix() feat() or BREAKING CHANGE in the message. Please just the message itself.
|
|
recommendedNextVersionDetails: string[]; // detailed bullet points for the changelog
|
|
recommendedNextVersion: string; // the recommended next version of the project, x.x.x
|
|
}
|
|
|
|
For the recommendedNextVersionDetails, please only add a detail entries to the array if it has an obvious value to the reader.
|
|
|
|
You are being given the files of the project. You should use them to create the commit message.
|
|
Also you are given a diff.
|
|
Never mention CLAUDE code, or codex.
|
|
`,
|
|
messageHistory: [],
|
|
userMessage: contextString,
|
|
});
|
|
|
|
// console.log(result.message);
|
|
const resultObject: INextCommitObject = JSON.parse(
|
|
result.message.replace('```json', '').replace('```', '')
|
|
);
|
|
|
|
const previousChangelogPath = plugins.path.join(this.projectDir, 'changelog.md');
|
|
let previousChangelog: plugins.smartfile.SmartFile;
|
|
if (await plugins.smartfile.fs.fileExists(previousChangelogPath)) {
|
|
previousChangelog = await plugins.smartfile.SmartFile.fromFilePath(previousChangelogPath);
|
|
}
|
|
|
|
if (!previousChangelog) {
|
|
// lets build the changelog based on that
|
|
const commitMessages = await gitRepo.getAllCommitMessages();
|
|
console.log(JSON.stringify(commitMessages, null, 2));
|
|
let result2 = await this.aiDocsRef.openaiInstance.chat({
|
|
messageHistory: [],
|
|
systemMessage: `
|
|
You are building a changelog.md file for the project.
|
|
Omit commits and versions that lack relevant changes, but make sure to mention them as a range with a summarizing message instead.
|
|
|
|
A changelog entry should look like this:
|
|
|
|
## yyyy-mm-dd - x.x.x - scope here
|
|
main descriptiom here
|
|
|
|
- detailed bullet points follow
|
|
|
|
You are given:
|
|
* the commit messages of the project
|
|
|
|
Only return the changelog file, so it can be written directly to changelog.md`,
|
|
userMessage: `
|
|
Here are the commit messages:
|
|
|
|
${JSON.stringify(commitMessages, null, 2)}
|
|
`,
|
|
});
|
|
|
|
previousChangelog = await plugins.smartfile.SmartFile.fromString(
|
|
previousChangelogPath,
|
|
result2.message.replaceAll('```markdown', '').replaceAll('```', ''),
|
|
'utf8'
|
|
);
|
|
}
|
|
|
|
let oldChangelog = previousChangelog.contents.toString().replace('# Changelog\n\n', '');
|
|
if (oldChangelog.startsWith('\n')) {
|
|
oldChangelog = oldChangelog.replace('\n', '');
|
|
}
|
|
let newDateString = new plugins.smarttime.ExtendedDate().exportToHyphedSortableDate();
|
|
let newChangelog = `# Changelog\n\n${`## ${newDateString} - {{nextVersion}} - {{nextVersionScope}}
|
|
{{nextVersionMessage}}
|
|
|
|
{{nextVersionDetails}}`}\n\n${oldChangelog}`;
|
|
resultObject.changelog = newChangelog;
|
|
|
|
return resultObject;
|
|
}
|
|
}
|