gennady 0.1.0 → 0.2.0
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 +16 -6
- package/package.json +2 -5
- package/src/commit-gen/commit-gen.js +88 -146
- 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
|
@@ -4,6 +4,7 @@ import { dirname, join } from 'path';
|
|
|
4
4
|
import { CommitGen } from '../src/commit-gen/commit-gen.js';
|
|
5
5
|
import { parseArgs } from '../src/utils/parse-args.js';
|
|
6
6
|
import { style } from '../src/utils/style.js';
|
|
7
|
+
import { getSysLang } from '../src/utils/language.js';
|
|
7
8
|
|
|
8
9
|
const PROMPTS_DIR = join(
|
|
9
10
|
typeof __dirname !== 'string' ? dirname(fileURLToPath(import.meta.url)) : __dirname,
|
|
@@ -12,22 +13,31 @@ const PROMPTS_DIR = join(
|
|
|
12
13
|
|
|
13
14
|
const params = parseArgs(process.argv, {
|
|
14
15
|
mode: ['mode', 'm'],
|
|
16
|
+
oneline: ['short', 'one', 'o'],
|
|
15
17
|
reviewerModel: ['model'],
|
|
16
18
|
targetBranch: ['branch', 'b'],
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
const commit = new CommitGen({
|
|
20
22
|
...params,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
basePromptTemplate: readFileSync(join(PROMPTS_DIR, 'base-prompt.md')).toString(),
|
|
24
|
+
formatOnelinePromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-oneline-prompt.md')).toString(),
|
|
25
|
+
formatDetailedPromptTemplate: readFileSync(join(PROMPTS_DIR, 'format-detailed-prompt.md')).toString(),
|
|
26
|
+
translatePromptTemplate: readFileSync(join(PROMPTS_DIR, 'translate-prompt.md')).toString(),
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
console.info(`🤖`, style.whiteBright.bold(`GENNADY`), `(${style.cyan(commit.reviewerModel)} → ${style.yellow(commit.mode)})`, `🗯️`);
|
|
27
30
|
console.info(style.gray(`-`.repeat(30)));
|
|
28
31
|
|
|
29
32
|
const msg = await commit.generate();
|
|
33
|
+
if (msg) {
|
|
34
|
+
console.info(`-`.repeat(40), '\n');
|
|
35
|
+
console.info(style.whiteBright(msg), '\n');
|
|
36
|
+
console.info(`^`.repeat(40), '\n');
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.info(
|
|
38
|
+
const lang = getSysLang();
|
|
39
|
+
if (lang !== 'en') {
|
|
40
|
+
console.info(await commit.translate(msg, lang), '\n');
|
|
41
|
+
console.info(`^`.repeat(40), '\n');
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gennady",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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,99 @@ 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
88
|
this.init.mode === 'auto'
|
|
166
89
|
? this.init.targetBranch
|
|
167
90
|
? 'detailed'
|
|
168
|
-
: commitCount > 1 &&
|
|
91
|
+
: commitCount > 1 && codeChanged.length < 5
|
|
169
92
|
? 'oneline'
|
|
170
93
|
: 'detailed'
|
|
171
94
|
: this.init.mode;
|
|
172
95
|
|
|
173
|
-
this.logger.info(
|
|
96
|
+
this.logger.info(`- Mode: ${style.bold.magentaBright(mode)}`);
|
|
97
|
+
this.logger.info(`- Languages: ${style.yellow(languages)}`);
|
|
98
|
+
this.logger.info(`- Tokens: ${style.bold.cyanBright(tokens)} ${style.gray(`(max per file: ${maxTokens})`)}`);
|
|
174
99
|
|
|
175
|
-
|
|
176
|
-
this.logger.
|
|
177
|
-
|
|
100
|
+
if (maxTokens > maxInputTokens) {
|
|
101
|
+
this.logger.error(`TODO: Diff is too large, skipping commit message generation.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
178
104
|
|
|
179
|
-
const
|
|
105
|
+
const batches = codeChanged.reduce((acc, file) => {
|
|
106
|
+
if (!acc[0] || acc[0].tokens + file.tokens > maxInputTokens) {
|
|
107
|
+
if (acc[0]) {
|
|
108
|
+
acc[0].languages = [...new Set(acc[0].languages)];
|
|
109
|
+
}
|
|
180
110
|
|
|
181
|
-
|
|
111
|
+
acc.unshift({tokens: 0, diff: '', languages: []});
|
|
112
|
+
}
|
|
182
113
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
114
|
+
acc[0].tokens += file.tokens;
|
|
115
|
+
acc[0].diff += `File: ${file.filename}\n${file.diff.hunks.flatMap(h => h.changes).join('\n')}\n\n`;
|
|
116
|
+
acc[0].languages.push(file.programmingLanguage);
|
|
186
117
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let finalMessage = '';
|
|
118
|
+
return acc;
|
|
119
|
+
}, []);
|
|
190
120
|
|
|
191
|
-
this.logger.info(
|
|
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
|
-
|
|
121
|
+
this.logger.info(`- Queue: ${style.bold.cyan(batches.length)}`);
|
|
122
|
+
this.logger.info(`-`.repeat(40));
|
|
123
|
+
|
|
124
|
+
const startGenTime = performance.now();
|
|
125
|
+
const results = await Promise.all(batches.map(async (batch) => {
|
|
126
|
+
console.info(`- Task:`, batch.tokens, batch.languages);
|
|
127
|
+
|
|
128
|
+
const prompt = this.init.basePromptTemplate
|
|
129
|
+
.replaceAll('{languages}', batch.languages.join('/'))
|
|
130
|
+
.replaceAll('{input}', batch.diff);
|
|
131
|
+
|
|
132
|
+
const msg = await this.generateCommitMessage(prompt);
|
|
133
|
+
|
|
134
|
+
return msg
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
this.logger.info(`-`.repeat(30));
|
|
138
|
+
this.logger.info(`- Generation time: ${style.blueBright(((performance.now() - startGenTime) / 1000).toFixed(2))}s`);
|
|
139
|
+
|
|
140
|
+
const startFormatTime = performance.now();
|
|
141
|
+
const formatted = await this.toFormat(
|
|
142
|
+
mode === 'detailed' && !this.init.oneline ? this.init.formatDetailedPromptTemplate : this.init.formatOnelinePromptTemplate,
|
|
143
|
+
results.join('\n\n'),
|
|
144
|
+
);
|
|
145
|
+
this.logger.info(`- Formatting time: ${style.blueBright(((performance.now() - startFormatTime) / 1000).toFixed(2))}s`);
|
|
146
|
+
|
|
147
|
+
return formatted.trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async toFormat(format, text) {
|
|
151
|
+
const result = await this.fetchPrompt(format.replaceAll('{input}', text));
|
|
152
|
+
return result.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*$)/g, '').replace(`\n-`, `\n\n-`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async translate(input, lang) {
|
|
156
|
+
const result = await this.fetchPrompt(this.init.translatePromptTemplate
|
|
157
|
+
.replaceAll('{input}', input)
|
|
158
|
+
.replaceAll('{lang}', lang));
|
|
217
159
|
|
|
218
|
-
return
|
|
160
|
+
return result.replace(/(^[\s\S]*<message>|<\/message>[\s\S]*)/g, '');
|
|
219
161
|
}
|
|
220
162
|
}
|
|
@@ -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>`.
|