gennady 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/gennady.js +18 -6
- package/package.json +2 -5
- package/src/commit-gen/commit-gen.js +96 -152
- package/src/git/git-cmd.js +32 -0
- package/src/git/git-diff.js +146 -0
- package/src/prompts/base-prompt.md +27 -0
- package/src/prompts/format-detailed-prompt.md +16 -0
- package/src/prompts/format-oneline-prompt.md +11 -0
- package/src/prompts/translate-prompt.md +11 -0
- package/src/utils/language.js +39 -0
- package/src/utils/tokens.js +4 -0
- package/src/prompts/composer-prompt.md +0 -7
- package/src/prompts/detailed-prompt.md +0 -32
- package/src/prompts/oneline-prompt.md +0 -22
package/cli/gennady.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
1
3
|
import { readFileSync } from 'fs';
|
|
2
4
|
import { fileURLToPath } from 'url';
|
|
3
5
|
import { dirname, join } from 'path';
|
|
4
6
|
import { CommitGen } from '../src/commit-gen/commit-gen.js';
|
|
5
7
|
import { parseArgs } from '../src/utils/parse-args.js';
|
|
6
8
|
import { style } from '../src/utils/style.js';
|
|
9
|
+
import { getSysLang } from '../src/utils/language.js';
|
|
7
10
|
|
|
8
11
|
const PROMPTS_DIR = join(
|
|
9
12
|
typeof __dirname !== 'string' ? dirname(fileURLToPath(import.meta.url)) : __dirname,
|
|
@@ -12,22 +15,31 @@ const PROMPTS_DIR = join(
|
|
|
12
15
|
|
|
13
16
|
const params = parseArgs(process.argv, {
|
|
14
17
|
mode: ['mode', 'm'],
|
|
18
|
+
oneline: ['short', 'one', 'o'],
|
|
15
19
|
reviewerModel: ['model'],
|
|
16
20
|
targetBranch: ['branch', 'b'],
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
const commit = new CommitGen({
|
|
20
24
|
...params,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
basePromptTemplate: readFileSync(join(PROMPTS_DIR, 'base-prompt.md')).toString(),
|
|
26
|
+
formatOnelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-oneline-prompt.md')).toString(),
|
|
27
|
+
formatDetailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-detailed-prompt.md')).toString(),
|
|
28
|
+
translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
|
|
24
29
|
});
|
|
25
30
|
|
|
26
31
|
console.info(`🤖`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.reviewerModel)} → ${style.yellow(commit.mode)})`, `🗯️`);
|
|
27
32
|
console.info(style.gray(`-`.repeat(30)));
|
|
28
33
|
|
|
29
34
|
const msg = await commit.generate();
|
|
35
|
+
if (msg) {
|
|
36
|
+
console.info(`-`.repeat(40), '\n');
|
|
37
|
+
console.info(style.whiteBright(msg), '\n');
|
|
38
|
+
console.info(`^`.repeat(40), '\n');
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.info(
|
|
40
|
+
const lang = getSysLang();
|
|
41
|
+
if (lang !== 'en') {
|
|
42
|
+
console.info(await commit.translate(msg, lang), '\n');
|
|
43
|
+
console.info(`^`.repeat(40), '\n');
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gennady",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"author": "Konstantin Lebedev <ibnrubaxa@gmail.com>",
|
|
5
5
|
"description": "Gennady — Generate Next-level Automated Description Intelligence",
|
|
6
6
|
"keywords": [
|
|
@@ -13,10 +13,7 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": "https://github.com/rubaxa/commit-gen",
|
|
15
15
|
"type": "module",
|
|
16
|
-
"bin":
|
|
17
|
-
"gennady": "./cli/gennady.js",
|
|
18
|
-
"gennadi": "./cli/gennady.js"
|
|
19
|
-
},
|
|
16
|
+
"bin": "./cli/gennady.js",
|
|
20
17
|
"main": "index.js",
|
|
21
18
|
"scripts": {
|
|
22
19
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getGitCommitCount, getGitDiff } from '../git/git-cmd.js';
|
|
2
|
+
import { parseGitDiff } from '../git/git-diff.js';
|
|
2
3
|
import { style } from '../utils/style.js';
|
|
3
4
|
|
|
4
5
|
export class CommitGen {
|
|
5
6
|
constructor(init) {
|
|
6
7
|
this.init = {
|
|
7
8
|
mode: 'auto',
|
|
9
|
+
oneline: false,
|
|
8
10
|
reviewerModel: 'llama3:8b',
|
|
9
11
|
targetBranch: undefined,
|
|
10
|
-
onelinePromptTemplate: 'Oneline commit: {diff}',
|
|
11
|
-
detailedPromptTemplate: 'Detailed commit: {diff}',
|
|
12
|
-
composerPromptTemplate: 'Compose final commit message from parts: {messages}',
|
|
13
12
|
|
|
14
|
-
maxInputTokens: init.maxInputTokens ||
|
|
13
|
+
maxInputTokens: init.maxInputTokens || 4000,
|
|
15
14
|
ollamaUrl: init.ollamaUrl || 'http://127.0.0.1:11434/api/generate',
|
|
16
15
|
logger: console,
|
|
17
16
|
|
|
17
|
+
basePromptTemplate: undefined,
|
|
18
|
+
formatOnelinePromptTemplate: undefined,
|
|
19
|
+
formatDetailedPromptTemplate: undefined,
|
|
20
|
+
translatePromptTemplate: undefined,
|
|
21
|
+
|
|
18
22
|
...init,
|
|
19
23
|
};
|
|
20
24
|
}
|
|
@@ -35,112 +39,22 @@ export class CommitGen {
|
|
|
35
39
|
return this.init.targetBranch;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
execSync(cmd) {
|
|
39
|
-
try {
|
|
40
|
-
return nodeExecSync(cmd, { encoding: 'utf-8'});
|
|
41
|
-
} catch (e) {
|
|
42
|
-
return '';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
getCommitCount() {
|
|
47
|
-
const output = this.execSync('git rev-list --count HEAD 2>/dev/null');
|
|
48
|
-
return parseInt(output, 10) || 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getDiff() {
|
|
52
|
-
if (this.init.targetBranch) {
|
|
53
|
-
return this.execSync(`git diff ${this.init.targetBranch}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return this.execSync('git diff --cached');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
parseDiff(diffStr) {
|
|
60
|
-
const lines = diffStr.split('\n');
|
|
61
|
-
const files = [];
|
|
62
|
-
let currentFile = null;
|
|
63
|
-
|
|
64
|
-
lines.forEach((line) => {
|
|
65
|
-
if (line.startsWith('diff --git')) {
|
|
66
|
-
if (currentFile) files.push(currentFile);
|
|
67
|
-
const parts = line.split(' ');
|
|
68
|
-
const bFile = parts[3] ? parts[3].replace('b/', '') : parts[2].replace('a/', '');
|
|
69
|
-
currentFile = { filename: bFile, diff: [line] };
|
|
70
|
-
} else if (currentFile) {
|
|
71
|
-
currentFile.diff.push(line);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
if (currentFile) files.push(currentFile);
|
|
76
|
-
|
|
77
|
-
return files;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
categorizeFiles(files) {
|
|
81
|
-
const lockFiles = [];
|
|
82
|
-
const dotFiles = [];
|
|
83
|
-
const docFiles = [];
|
|
84
|
-
const renamedFiles = [];
|
|
85
|
-
const deletedFiles = [];
|
|
86
|
-
const changedFiles = [];
|
|
87
|
-
|
|
88
|
-
files.forEach((file) => {
|
|
89
|
-
const isRenamed = file.diff.some(
|
|
90
|
-
(line) => line.includes('rename from') || line.includes('rename to')
|
|
91
|
-
);
|
|
92
|
-
const isDeleted = file.diff.some((line) => line.includes('deleted file mode'));
|
|
93
|
-
|
|
94
|
-
if (/\.md$/.test(file.filename)) {
|
|
95
|
-
docFiles.push(file);
|
|
96
|
-
} else if (/^\./.test(file.filename)) {
|
|
97
|
-
dotFiles.push(file);
|
|
98
|
-
} else if (/(\.lock|-lock\.json)$/.test(file.filename)) {
|
|
99
|
-
lockFiles.push(file);
|
|
100
|
-
} else if (isRenamed) {
|
|
101
|
-
renamedFiles.push(file);
|
|
102
|
-
} else if (isDeleted) {
|
|
103
|
-
deletedFiles.push(file);
|
|
104
|
-
} else {
|
|
105
|
-
changedFiles.push(file);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return { dotFiles, docFiles, lockFiles, renamedFiles, deletedFiles, changedFiles };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
42
|
generateDiffText(files) {
|
|
113
43
|
return files
|
|
114
44
|
.map((file) => `File: ${file.filename}\n${file.diff.join('\n')}`)
|
|
115
45
|
.join('\n\n');
|
|
116
46
|
}
|
|
117
47
|
|
|
118
|
-
|
|
119
|
-
return text.split(/\s+/).length;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
splitDiffByTokens(diffText, template, maxTokens) {
|
|
123
|
-
const templateTokens = this.tokenCount(template.replace('{diff}', ''));
|
|
124
|
-
const availableTokens = maxTokens - templateTokens;
|
|
125
|
-
const words = diffText.split(/\s+/);
|
|
126
|
-
const chunks = [];
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < words.length; i += availableTokens) {
|
|
129
|
-
chunks.push(words.slice(i, i + availableTokens).join(' '));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return chunks;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async generateFromOllama(prompt) {
|
|
48
|
+
async fetchPrompt(prompt, context) {
|
|
136
49
|
try {
|
|
137
50
|
const req = await fetch(this.init.ollamaUrl, {
|
|
138
51
|
method: 'POST',
|
|
139
52
|
headers: {'Content-Type': 'application/json'},
|
|
140
53
|
body: JSON.stringify({
|
|
141
|
-
prompt,
|
|
142
54
|
model: this.init.reviewerModel,
|
|
143
55
|
stream: false,
|
|
56
|
+
prompt,
|
|
57
|
+
context,
|
|
144
58
|
}),
|
|
145
59
|
});
|
|
146
60
|
const data = await req.json();
|
|
@@ -150,71 +64,101 @@ export class CommitGen {
|
|
|
150
64
|
return '';
|
|
151
65
|
}
|
|
152
66
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return msg.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*)/g, '').replace(/\n\s*- /, '\n\n- ')
|
|
67
|
+
async generateCommitMessage(prompt) {
|
|
68
|
+
const text = await this.fetchPrompt(prompt);
|
|
69
|
+
return text;
|
|
157
70
|
}
|
|
158
71
|
|
|
159
72
|
async generate() {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
73
|
+
const {maxInputTokens} = this.init
|
|
74
|
+
const commitCount = getGitCommitCount();
|
|
75
|
+
const diffStr = getGitDiff(this.init.targetBranch);
|
|
76
|
+
const parsedDiff = parseGitDiff(diffStr).sort((a, b) => a.tokens - b.tokens);
|
|
77
|
+
const codeChanged = parsedDiff.filter(f => !f.isDeleted && !f.isRenamed && f.programmingLanguage);
|
|
78
|
+
|
|
79
|
+
if (codeChanged.length === 0) {
|
|
80
|
+
this.logger.warn(`No changes detected, skipping commit message generation.`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tokens = codeChanged.reduce((sum, file) => sum + file.tokens, 0);
|
|
85
|
+
const maxTokens = codeChanged[codeChanged.length - 1].tokens;
|
|
86
|
+
const languages = [...new Set(codeChanged.map(f => f.programmingLanguage).filter(Boolean))].join(', ');
|
|
164
87
|
const mode =
|
|
165
|
-
this.init.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
88
|
+
this.init.oneline
|
|
89
|
+
? 'oneline'
|
|
90
|
+
: this.init.mode === 'auto'
|
|
91
|
+
? this.init.targetBranch
|
|
92
|
+
? 'detailed'
|
|
93
|
+
: commitCount > 1 && codeChanged.length < 5
|
|
94
|
+
? 'oneline'
|
|
95
|
+
: 'detailed'
|
|
96
|
+
: this.init.mode;
|
|
97
|
+
|
|
98
|
+
this.logger.info(`- Mode: ${style.bold.magentaBright(mode)}`);
|
|
99
|
+
this.logger.info(`- Languages: ${style.yellow(languages)}`);
|
|
100
|
+
this.logger.info(`- Tokens: ${style.bold.cyanBright(tokens)} ${style.gray(`(max per file: ${maxTokens})`)}`);
|
|
101
|
+
|
|
102
|
+
if (maxTokens > maxInputTokens) {
|
|
103
|
+
this.logger.error(`TODO: Diff is too large, skipping commit message generation.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
172
106
|
|
|
173
|
-
|
|
107
|
+
const batches = codeChanged.reduce((acc, file) => {
|
|
108
|
+
if (!acc[0] || acc[0].tokens + file.tokens > maxInputTokens) {
|
|
109
|
+
if (acc[0]) {
|
|
110
|
+
acc[0].languages = [...new Set(acc[0].languages)];
|
|
111
|
+
}
|
|
174
112
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
113
|
+
acc.unshift({tokens: 0, diff: '', languages: []});
|
|
114
|
+
}
|
|
178
115
|
|
|
179
|
-
|
|
116
|
+
acc[0].tokens += file.tokens;
|
|
117
|
+
acc[0].diff += `File: ${file.filename}\n${file.diff.hunks.flatMap(h => h.changes).join('\n')}\n\n`;
|
|
118
|
+
acc[0].languages.push(file.programmingLanguage);
|
|
180
119
|
|
|
181
|
-
|
|
120
|
+
return acc;
|
|
121
|
+
}, []);
|
|
182
122
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
: this.init.detailedPromptTemplate).trim();
|
|
123
|
+
this.logger.info(`- Queue: ${style.bold.cyan(batches.length)}`);
|
|
124
|
+
this.logger.info(`-`.repeat(40));
|
|
186
125
|
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
126
|
+
const startGenTime = performance.now();
|
|
127
|
+
const results = await Promise.all(batches.map(async (batch) => {
|
|
128
|
+
console.info(`- Task:`, batch.tokens, batch.languages);
|
|
190
129
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
130
|
+
const prompt = this.init.basePromptTemplate
|
|
131
|
+
.replaceAll('{languages}', batch.languages.join('/'))
|
|
132
|
+
.replaceAll('{input}', batch.diff);
|
|
133
|
+
|
|
134
|
+
const msg = await this.generateCommitMessage(prompt);
|
|
135
|
+
|
|
136
|
+
return msg
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
this.logger.info(`-`.repeat(30));
|
|
140
|
+
this.logger.info(`- Generation time: ${style.blueBright(((performance.now() - startGenTime) / 1000).toFixed(2))}s`);
|
|
141
|
+
|
|
142
|
+
const startFormatTime = performance.now();
|
|
143
|
+
const formatted = await this.toFormat(
|
|
144
|
+
mode === 'detailed' ? this.init.formatDetailedPromptTemplate : this.init.formatOnelinePromptTemplate,
|
|
145
|
+
results.join('\n\n'),
|
|
146
|
+
);
|
|
147
|
+
this.logger.info(`- Formatting time: ${style.blueBright(((performance.now() - startFormatTime) / 1000).toFixed(2))}s`);
|
|
148
|
+
|
|
149
|
+
return formatted.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async toFormat(format, text) {
|
|
153
|
+
const result = await this.fetchPrompt(format.replaceAll('{input}', text));
|
|
154
|
+
return result.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*$)/g, '').replace(`\n-`, `\n\n-`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async translate(input, lang) {
|
|
158
|
+
const result = await this.fetchPrompt(this.init.translatePromptTemplate
|
|
159
|
+
.replaceAll('{input}', input)
|
|
160
|
+
.replaceAll('{lang}', lang));
|
|
217
161
|
|
|
218
|
-
return
|
|
162
|
+
return result.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*)/g, '');
|
|
219
163
|
}
|
|
220
164
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execSync as nodeExecSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const execSync = (cmd) => {
|
|
4
|
+
try {
|
|
5
|
+
return nodeExecSync(cmd, { encoding: 'utf-8'});
|
|
6
|
+
} catch (e) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const detectBaseBranch = () => {
|
|
12
|
+
const branchesOutput = execSync('git branch --list 2>/dev/null');
|
|
13
|
+
const match = branchesOutput?.match(/\s*\*?\s*(master|main)$/m);
|
|
14
|
+
return match?.[1] || 'master';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getGitCommitCount = () => {
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync(`git rev-list --count HEAD ^${detectBaseBranch()} 2>/dev/null`);
|
|
20
|
+
return parseInt(output, 10) || 0;
|
|
21
|
+
} catch {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getGitDiff = (targetBranch) => {
|
|
27
|
+
if (targetBranch) {
|
|
28
|
+
return execSync(`git diff ${targetBranch}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return execSync('git diff --cached');
|
|
32
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getProgrammingLanguage } from "../utils/language.js";
|
|
2
|
+
import { countTokens } from "../utils/tokens.js";
|
|
3
|
+
|
|
4
|
+
const getCategory = (filename, metadata) => {
|
|
5
|
+
if (metadata && metadata.extra && Array.isArray(metadata.extra)) {
|
|
6
|
+
for (let line of metadata.extra) {
|
|
7
|
+
if (/^Binary files? /i.test(line) || /GIT binary patch/i.test(line)) {
|
|
8
|
+
return 'bin';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const categories = {
|
|
14
|
+
bin: /^(exe|dll|so|bin)\b$/i,
|
|
15
|
+
lock: /^(package-lock\.json|yarn\.lock|npm-shrinkwrap\.json|composer\.lock|podfile\.lock|go\.sum|gemfile\.lock)$/i,
|
|
16
|
+
json: /\.json$/i,
|
|
17
|
+
doc: /\.(md|markdown|txt|rst)$/i,
|
|
18
|
+
cfg: /^(\..+|.*\.(yml|yaml|rc|ini|conf))$/i,
|
|
19
|
+
img: /\.(png|jpe?g|gif|svg|bmp|tiff|ico)$/i,
|
|
20
|
+
css: /\.(css|less|scss|sass|styl)$/i,
|
|
21
|
+
html: /\.(html?)$/i,
|
|
22
|
+
code: /\.(js|jsx|ts|tsx|java|py|c|cpp|cs|rb|php|go|swift|m|mm|kt)$/i,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const category = Object.entries(categories).find(
|
|
26
|
+
([, regexp]) => regexp.test(filename)
|
|
27
|
+
)?.[0] || 'other';
|
|
28
|
+
|
|
29
|
+
return category;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const parseGitDiff = (diffText) => {
|
|
33
|
+
const lines = diffText.split('\n');
|
|
34
|
+
const result = [];
|
|
35
|
+
let currentFile = null;
|
|
36
|
+
let currentHunk = null;
|
|
37
|
+
|
|
38
|
+
lines.forEach((line) => {
|
|
39
|
+
if (line.startsWith('diff --git')) {
|
|
40
|
+
if (currentFile) {
|
|
41
|
+
currentFile.diff.tokens = currentFile.diff.hunks.reduce((sum, hunk) => sum + hunk.tokens, 0);
|
|
42
|
+
currentFile.tokens = currentFile.diff.tokens;
|
|
43
|
+
const extMatch = currentFile.filename.match(/\.([^.]+)$/);
|
|
44
|
+
currentFile.ext = extMatch ? extMatch[1].toLowerCase() : '';
|
|
45
|
+
currentFile.category = getCategory(currentFile.filename, currentFile.metadata);
|
|
46
|
+
currentFile.programmingLanguage = getProgrammingLanguage(currentFile.ext);
|
|
47
|
+
result.push(currentFile);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const fileMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
51
|
+
const oldFilename = fileMatch ? fileMatch[1] : null;
|
|
52
|
+
const filename = fileMatch ? fileMatch[2] : null;
|
|
53
|
+
|
|
54
|
+
currentFile = {
|
|
55
|
+
isNew: false,
|
|
56
|
+
isDeleted: false,
|
|
57
|
+
isRenamed: false,
|
|
58
|
+
filename,
|
|
59
|
+
oldFilename,
|
|
60
|
+
newFileMode: null,
|
|
61
|
+
deletedFileMode: null,
|
|
62
|
+
diff: {
|
|
63
|
+
tokens: 0,
|
|
64
|
+
hunks: []
|
|
65
|
+
},
|
|
66
|
+
tokens: 0,
|
|
67
|
+
metadata: {},
|
|
68
|
+
ext: '',
|
|
69
|
+
category: 'other',
|
|
70
|
+
programmingLanguage: undefined
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
currentHunk = null;
|
|
74
|
+
} else if (currentFile && line.startsWith('new file mode')) {
|
|
75
|
+
const parts = line.split(' ');
|
|
76
|
+
|
|
77
|
+
currentFile.isNew = true;
|
|
78
|
+
currentFile.newFileMode = parts[3] || null;
|
|
79
|
+
currentFile.metadata.newFileMode = currentFile.newFileMode;
|
|
80
|
+
} else if (currentFile && line.startsWith('deleted file mode')) {
|
|
81
|
+
const parts = line.split(' ');
|
|
82
|
+
|
|
83
|
+
currentFile.isDeleted = true;
|
|
84
|
+
currentFile.deletedFileMode = parts[3] || null;
|
|
85
|
+
currentFile.metadata.deletedFileMode = currentFile.deletedFileMode;
|
|
86
|
+
} else if (currentFile && line.startsWith('old mode')) {
|
|
87
|
+
const parts = line.split(' ');
|
|
88
|
+
currentFile.metadata.oldMode = parts[2] || null;
|
|
89
|
+
} else if (currentFile && line.startsWith('new mode')) {
|
|
90
|
+
const parts = line.split(' ');
|
|
91
|
+
currentFile.metadata.newMode = parts[2] || null;
|
|
92
|
+
} else if (currentFile && line.startsWith('similarity index')) {
|
|
93
|
+
const parts = line.split(' ');
|
|
94
|
+
currentFile.metadata.similarityIndex = parts[2] || null;
|
|
95
|
+
} else if (currentFile && line.startsWith('rename from')) {
|
|
96
|
+
const from = line.substring('rename from'.length).trim();
|
|
97
|
+
|
|
98
|
+
currentFile.isRenamed = true;
|
|
99
|
+
currentFile.oldFilename = from;
|
|
100
|
+
currentFile.metadata.renameFrom = from;
|
|
101
|
+
} else if (currentFile && line.startsWith('rename to')) {
|
|
102
|
+
const to = line.substring('rename to'.length).trim();
|
|
103
|
+
|
|
104
|
+
currentFile.isRenamed = true;
|
|
105
|
+
currentFile.filename = to;
|
|
106
|
+
currentFile.metadata.renameTo = to;
|
|
107
|
+
} else if (currentFile && line.startsWith('index')) {
|
|
108
|
+
currentFile.metadata.index = line.substring('index'.length).trim();
|
|
109
|
+
} else if (currentFile && (line.startsWith('--- ') || line.startsWith('+++ '))) {
|
|
110
|
+
if (line.startsWith('--- ')) {
|
|
111
|
+
currentFile.metadata.oldFileMarker = line;
|
|
112
|
+
} else {
|
|
113
|
+
currentFile.metadata.newFileMarker = line;
|
|
114
|
+
}
|
|
115
|
+
} else if (currentFile && line.startsWith('@@')) {
|
|
116
|
+
currentHunk = {
|
|
117
|
+
header: line,
|
|
118
|
+
changes: [line],
|
|
119
|
+
tokens: countTokens(line)
|
|
120
|
+
};
|
|
121
|
+
currentFile.diff.hunks.push(currentHunk);
|
|
122
|
+
} else if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))) {
|
|
123
|
+
currentHunk.changes.push(line);
|
|
124
|
+
currentHunk.tokens += countTokens(line);
|
|
125
|
+
} else if (currentFile) {
|
|
126
|
+
if (!currentFile.metadata.extra) {
|
|
127
|
+
currentFile.metadata.extra = [];
|
|
128
|
+
}
|
|
129
|
+
currentFile.metadata.extra.push(line);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (currentFile) {
|
|
134
|
+
const extMatch = currentFile.filename.match(/\.([^.]+)$/);
|
|
135
|
+
|
|
136
|
+
currentFile.diff.tokens = currentFile.diff.hunks.reduce((sum, hunk) => sum + hunk.tokens, 0);
|
|
137
|
+
currentFile.tokens = currentFile.diff.tokens;
|
|
138
|
+
currentFile.ext = extMatch ? extMatch[1].toLowerCase() : '';
|
|
139
|
+
currentFile.category = getCategory(currentFile.filename, currentFile.metadata);
|
|
140
|
+
currentFile.programmingLanguage = getProgrammingLanguage(currentFile.ext);
|
|
141
|
+
|
|
142
|
+
result.push(currentFile);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
You are {languages} expert tasked with writing a git commit subject and description based on code changes.
|
|
2
|
+
|
|
3
|
+
**Subject:**
|
|
4
|
+
- Clearly summarize the key changes in one concise yet informative sentence.
|
|
5
|
+
- Do not describe changes for each file.
|
|
6
|
+
- Do not mention stylistic changes or fixed typos.
|
|
7
|
+
- The subject must provide enough context to understand the commit at a glance.
|
|
8
|
+
- End with an emoji instead of a period.
|
|
9
|
+
|
|
10
|
+
**Description:**
|
|
11
|
+
- Expand on the changes by listing key modifications as an unordered list.
|
|
12
|
+
- Do not describe changes for each file.
|
|
13
|
+
- Do not mention stylistic changes or fixed typos.
|
|
14
|
+
- Group related changes into single points.
|
|
15
|
+
- Each list item **must** be less than 140 characters and should not end with a period.
|
|
16
|
+
|
|
17
|
+
**Output Format (any deviation from this format is incorrect):**
|
|
18
|
+
<message>subject emoji
|
|
19
|
+
- description item 1
|
|
20
|
+
- description item 2</message>
|
|
21
|
+
|
|
22
|
+
**Extremely important:**
|
|
23
|
+
- The output must contain only commit message inside `<message>` and `</message>`.
|
|
24
|
+
- Do not add unnecessary words and markup, strictly follow the output format.
|
|
25
|
+
|
|
26
|
+
**Input:**
|
|
27
|
+
{input}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are a {languages} expert tasked with writing a git commit subject and description based on code changes.
|
|
2
|
+
|
|
3
|
+
### **Correct Output Format (no deviations allowed):**
|
|
4
|
+
<message>
|
|
5
|
+
[Commit subject ending with an emoji]
|
|
6
|
+
- [Description item 1]
|
|
7
|
+
- [Description item 2]
|
|
8
|
+
- [Description item N]
|
|
9
|
+
</message>
|
|
10
|
+
|
|
11
|
+
### **Fix this incorrect commit message:**
|
|
12
|
+
{input}
|
|
13
|
+
|
|
14
|
+
**Extremely important:**
|
|
15
|
+
- The output must contain only commit message inside `<message>` and `</message>`.
|
|
16
|
+
- Do not add unnecessary words and markup, strictly follow the output format.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
You are a {languages} expert tasked with writing a git commit subject and description based on code changes.
|
|
2
|
+
|
|
3
|
+
### **Correct Output Format (no deviations allowed):**
|
|
4
|
+
<message>Oneline commit message without loss of meaning and important changes and ending with an emoji</message>
|
|
5
|
+
|
|
6
|
+
### **Fix this incorrect commit message:**
|
|
7
|
+
{input}
|
|
8
|
+
|
|
9
|
+
**Extremely important:**
|
|
10
|
+
- The output must contain only commit message inside `<message>` and `</message>`.
|
|
11
|
+
- Do not add unnecessary words and markup, strictly follow the output format.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
### Translate text inside <message> and </message> tags from English to **{lang}**:
|
|
2
|
+
|
|
3
|
+
### **Output:**
|
|
4
|
+
<message>Translation result</message>
|
|
5
|
+
|
|
6
|
+
### **Extremely important:**
|
|
7
|
+
- The translation must be wrapped inside <message> and </message> tags.
|
|
8
|
+
- Do not add unnecessary words and markup, strictly follow the output format.
|
|
9
|
+
|
|
10
|
+
### **Input:**
|
|
11
|
+
<message>{input}</message>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execSync as nodeExecSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const languages = {
|
|
4
|
+
js: 'JavaScript',
|
|
5
|
+
jsx: 'JavaScript',
|
|
6
|
+
ts: 'TypeScript',
|
|
7
|
+
tsx: 'TypeScript',
|
|
8
|
+
java: 'Java',
|
|
9
|
+
py: 'Python',
|
|
10
|
+
c: 'C',
|
|
11
|
+
cpp: 'C++',
|
|
12
|
+
cs: 'C#',
|
|
13
|
+
rb: 'Ruby',
|
|
14
|
+
php: 'PHP',
|
|
15
|
+
go: 'Go',
|
|
16
|
+
swift: 'Swift',
|
|
17
|
+
m: 'Objective-C',
|
|
18
|
+
mm: 'Objective-C++',
|
|
19
|
+
kt: 'Kotlin',
|
|
20
|
+
html: 'HTML',
|
|
21
|
+
css: 'CSS',
|
|
22
|
+
less: 'Less',
|
|
23
|
+
scss: 'SCSS',
|
|
24
|
+
sass: 'Sass'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getProgrammingLanguage = (ext) => {
|
|
28
|
+
return languages[ext] || undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getSysLang = () => {
|
|
32
|
+
try {
|
|
33
|
+
const values = nodeExecSync("osascript -e 'user locale of (get system info)'").toString().trim().toLowerCase().split('_');
|
|
34
|
+
const lang = values.filter(v => v !== 'en' && v !== 'us');
|
|
35
|
+
return lang[0] || 'en';
|
|
36
|
+
} catch {
|
|
37
|
+
return 'en';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
You are a JavaScript/TypeScript expert tasked with writing a commit message for git based on the provided code changes. Follow these rules carefully:
|
|
2
|
-
|
|
3
|
-
**Subject:**
|
|
4
|
-
- Clearly summarize the key changes in one concise yet informative sentence.
|
|
5
|
-
- Use **present tense** and **imperative mood** (e.g., "Fix bug", "Add feature").
|
|
6
|
-
- Do **not** describe changes for each file.
|
|
7
|
-
- Do **not** mention stylistic changes or fixed typos.
|
|
8
|
-
- The subject **must** provide enough context to understand the commit at a glance.
|
|
9
|
-
- End with an **emoji** instead of a period.
|
|
10
|
-
|
|
11
|
-
**Description:**
|
|
12
|
-
- Expand on the changes by listing key modifications as an unordered list.
|
|
13
|
-
- Do **not** describe changes for each file.
|
|
14
|
-
- Do **not** mention stylistic changes or fixed typos.
|
|
15
|
-
- Group related changes into single points.
|
|
16
|
-
- Each list item **must** be less than 140 characters and should not end with a period.
|
|
17
|
-
|
|
18
|
-
**Output Format:**
|
|
19
|
-
<message>subject emoji
|
|
20
|
-
- description item 1
|
|
21
|
-
- description item 2</message>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
**Important:**
|
|
25
|
-
- The description **must** be an unordered list.
|
|
26
|
-
- Do **not** include introductory phrases like "Here's the commit message".
|
|
27
|
-
- The output must contain **only** the `<message>` tags and the commit message inside.
|
|
28
|
-
- The output **must** be wrapped inside `<message>` and `</message>` tags.
|
|
29
|
-
- Any deviation from this format is incorrect.
|
|
30
|
-
|
|
31
|
-
**Input:**
|
|
32
|
-
{diff}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
You are a JavaScript/TypeScript expert tasked with writing a commit message for git based on the provided code changes. Follow these rules:
|
|
2
|
-
|
|
3
|
-
**Online message:**
|
|
4
|
-
- Summarize changes as one sentence of key changes (e.g., broken code, new features).
|
|
5
|
-
- Use present tense, imperative mood.
|
|
6
|
-
- Do not describe changes for each file.
|
|
7
|
-
- Do not mention stylistic changes or fixed typos.
|
|
8
|
-
- Group related changes into a single point.
|
|
9
|
-
|
|
10
|
-
**Output Format:**
|
|
11
|
-
<message>your commit message here</message>
|
|
12
|
-
|
|
13
|
-
**Input:**
|
|
14
|
-
{diff}
|
|
15
|
-
|
|
16
|
-
**Important:**
|
|
17
|
-
- The commit message must be wrapped inside `<message>` and `</message>` tags.
|
|
18
|
-
- The message must be a **single line** with no line breaks, lists, bullet points, or additional formatting.
|
|
19
|
-
- Do not include any additional text, explanations, or commentary.
|
|
20
|
-
- Do not include introductory phrases like "Here's the commit message".
|
|
21
|
-
- The output must contain **only** the `<message>` tags and the commit message inside.
|
|
22
|
-
- **Lists, multiple lines, or extra formatting are strictly forbidden.** Only a single sentence inside `<message>`.
|