git-creeper 1.0.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/LICENSE +21 -0
- package/README.md +134 -0
- package/bin/cli.js +174 -0
- package/package.json +51 -0
- package/src/config.js +54 -0
- package/src/i18n.js +51 -0
- package/src/index.js +1235 -0
- package/src/locales/en.js +197 -0
- package/src/locales/tr.js +155 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
const simpleGit = require('simple-git');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { t } = require('./i18n');
|
|
4
|
+
|
|
5
|
+
const git = simpleGit();
|
|
6
|
+
|
|
7
|
+
async function timeline(options) {
|
|
8
|
+
const days = parseInt(options.days);
|
|
9
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
10
|
+
const title = `${t('timeline.title')} (${t('timeline.last')} ${days} ${t('stats.days')})`;
|
|
11
|
+
const titlePadding = Math.floor((60 - title.length) / 2);
|
|
12
|
+
|
|
13
|
+
console.log(chalk.bold.cyan('\n╔════════════════════════════════════════════════════════════╗'));
|
|
14
|
+
console.log(chalk.bold.cyan(`║${' '.repeat(titlePadding)}${title}${' '.repeat(60 - title.length - titlePadding)}║`));
|
|
15
|
+
console.log(chalk.bold.cyan('╚════════════════════════════════════════════════════════════╝\n'));
|
|
16
|
+
|
|
17
|
+
const log = await git.log({
|
|
18
|
+
'--since': since.toISOString()
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const commits = log.all;
|
|
22
|
+
|
|
23
|
+
if (commits.length === 0) {
|
|
24
|
+
console.log(chalk.gray('No commits found in this time range.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
commits.forEach((commit, index) => {
|
|
29
|
+
const date = new Date(commit.date);
|
|
30
|
+
const timeAgo = getTimeAgo(date);
|
|
31
|
+
const type = commit.message.startsWith('feat') ? 'FEAT' :
|
|
32
|
+
commit.message.startsWith('fix') ? 'FIX' :
|
|
33
|
+
commit.message.startsWith('docs') ? 'DOCS' : 'OTHER';
|
|
34
|
+
|
|
35
|
+
const typeColor = type === 'FEAT' ? chalk.green :
|
|
36
|
+
type === 'FIX' ? chalk.red :
|
|
37
|
+
type === 'DOCS' ? chalk.blue : chalk.gray;
|
|
38
|
+
|
|
39
|
+
console.log(`${typeColor('[' + type.padEnd(5) + ']')} ${chalk.bold(commit.message)}`);
|
|
40
|
+
console.log(` ${chalk.gray('Hash:')} ${chalk.white(commit.hash.substring(0, 7))}`);
|
|
41
|
+
console.log(` ${chalk.gray('Author:')} ${chalk.cyan(commit.author_name)}`);
|
|
42
|
+
console.log(` ${chalk.gray('Time:')} ${chalk.yellow(timeAgo)}`);
|
|
43
|
+
|
|
44
|
+
if (index < commits.length - 1) {
|
|
45
|
+
console.log(chalk.gray(' │'));
|
|
46
|
+
} else {
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTimeAgo(date) {
|
|
53
|
+
const seconds = Math.floor((new Date() - date) / 1000);
|
|
54
|
+
const intervals = {
|
|
55
|
+
year: 31536000,
|
|
56
|
+
month: 2592000,
|
|
57
|
+
week: 604800,
|
|
58
|
+
day: 86400,
|
|
59
|
+
hour: 3600,
|
|
60
|
+
minute: 60
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
64
|
+
const interval = Math.floor(seconds / secondsInUnit);
|
|
65
|
+
if (interval >= 1) {
|
|
66
|
+
return `${interval} ${unit}${interval > 1 ? 's' : ''} ago`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 'just now';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function hotspots(options) {
|
|
73
|
+
const limit = parseInt(options.limit);
|
|
74
|
+
const title = `${t('hotspots.title')} (${t('hotspots.top')} ${limit})`;
|
|
75
|
+
const titlePadding = Math.floor((60 - title.length) / 2);
|
|
76
|
+
|
|
77
|
+
console.log(chalk.bold.red('\n╔════════════════════════════════════════════════════════════╗'));
|
|
78
|
+
console.log(chalk.bold.red(`║${' '.repeat(titlePadding)}${title}${' '.repeat(60 - title.length - titlePadding)}║`));
|
|
79
|
+
console.log(chalk.bold.red('╚════════════════════════════════════════════════════════════╝\n'));
|
|
80
|
+
|
|
81
|
+
const log = await git.raw(['log', '--name-only', '--pretty=format:']);
|
|
82
|
+
const files = log.split('\n').filter(f => f.trim());
|
|
83
|
+
|
|
84
|
+
const frequency = {};
|
|
85
|
+
files.forEach(file => {
|
|
86
|
+
frequency[file] = (frequency[file] || 0) + 1;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const sorted = Object.entries(frequency)
|
|
90
|
+
.sort((a, b) => b[1] - a[1])
|
|
91
|
+
.slice(0, limit);
|
|
92
|
+
|
|
93
|
+
const maxCount = sorted[0]?.[1] || 1;
|
|
94
|
+
|
|
95
|
+
console.log(chalk.bold('RANK FILE CHANGES IMPACT'));
|
|
96
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
97
|
+
|
|
98
|
+
sorted.forEach(([file, count], i) => {
|
|
99
|
+
const barLength = Math.floor((count / maxCount) * 30);
|
|
100
|
+
const bar = '█'.repeat(barLength);
|
|
101
|
+
const percentage = ((count / maxCount) * 100).toFixed(0);
|
|
102
|
+
const rank = (i + 1).toString().padStart(4);
|
|
103
|
+
const fileShort = file.length > 35 ? '...' + file.slice(-32) : file.padEnd(35);
|
|
104
|
+
const countStr = count.toString().padStart(7);
|
|
105
|
+
|
|
106
|
+
console.log(`${chalk.yellow(rank)} ${chalk.cyan(fileShort)} ${chalk.white(countStr)} ${chalk.red(bar)} ${chalk.gray(percentage + '%')}`);
|
|
107
|
+
});
|
|
108
|
+
console.log();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function changelog(options) {
|
|
112
|
+
console.log(chalk.bold.green('\n╔════════════════════════════════════════════════════════════╗'));
|
|
113
|
+
console.log(chalk.bold.green('║ 📝 Smart Changelog ║'));
|
|
114
|
+
console.log(chalk.bold.green('╚════════════════════════════════════════════════════════════╝\n'));
|
|
115
|
+
|
|
116
|
+
const range = options.from ? `${options.from}..${options.to}` : options.to;
|
|
117
|
+
const log = await git.log([range]);
|
|
118
|
+
|
|
119
|
+
const categories = {
|
|
120
|
+
feat: [],
|
|
121
|
+
fix: [],
|
|
122
|
+
docs: [],
|
|
123
|
+
other: []
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
log.all.forEach(commit => {
|
|
127
|
+
const msg = commit.message;
|
|
128
|
+
if (msg.startsWith('feat')) categories.feat.push(msg);
|
|
129
|
+
else if (msg.startsWith('fix')) categories.fix.push(msg);
|
|
130
|
+
else if (msg.startsWith('docs')) categories.docs.push(msg);
|
|
131
|
+
else categories.other.push(msg);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (categories.feat.length) {
|
|
135
|
+
console.log(chalk.bold.green('┌─ ✨ Features'));
|
|
136
|
+
categories.feat.forEach((m, i) => {
|
|
137
|
+
const prefix = i === categories.feat.length - 1 ? '└─' : '├─';
|
|
138
|
+
console.log(chalk.green(`${prefix} ${m.replace('feat: ', '').replace('feat:', '')}`));
|
|
139
|
+
});
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (categories.fix.length) {
|
|
144
|
+
console.log(chalk.bold.red('┌─ 🐛 Bug Fixes'));
|
|
145
|
+
categories.fix.forEach((m, i) => {
|
|
146
|
+
const prefix = i === categories.fix.length - 1 ? '└─' : '├─';
|
|
147
|
+
console.log(chalk.red(`${prefix} ${m.replace('fix: ', '').replace('fix:', '')}`));
|
|
148
|
+
});
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (categories.docs.length) {
|
|
153
|
+
console.log(chalk.bold.blue('┌─ 📚 Documentation'));
|
|
154
|
+
categories.docs.forEach((m, i) => {
|
|
155
|
+
const prefix = i === categories.docs.length - 1 ? '└─' : '├─';
|
|
156
|
+
console.log(chalk.blue(`${prefix} ${m.replace('docs: ', '').replace('docs:', '')}`));
|
|
157
|
+
});
|
|
158
|
+
console.log();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (categories.other.length) {
|
|
162
|
+
console.log(chalk.bold.gray('┌─ 🔧 Other Changes'));
|
|
163
|
+
categories.other.forEach((m, i) => {
|
|
164
|
+
const prefix = i === categories.other.length - 1 ? '└─' : '├─';
|
|
165
|
+
console.log(chalk.gray(`${prefix} ${m}`));
|
|
166
|
+
});
|
|
167
|
+
console.log();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function blameAnalysis(file) {
|
|
172
|
+
console.log(chalk.bold.magenta(`\n🔍 Blame Analysis: ${file}\n`));
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const blame = await git.raw(['blame', '--line-porcelain', file]);
|
|
176
|
+
const lines = blame.split('\n');
|
|
177
|
+
|
|
178
|
+
const authors = {};
|
|
179
|
+
let currentAuthor = null;
|
|
180
|
+
|
|
181
|
+
lines.forEach(line => {
|
|
182
|
+
if (line.startsWith('author ')) {
|
|
183
|
+
currentAuthor = line.substring(7);
|
|
184
|
+
authors[currentAuthor] = (authors[currentAuthor] || 0) + 1;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const sorted = Object.entries(authors).sort((a, b) => b[1] - a[1]);
|
|
189
|
+
const total = sorted.reduce((sum, [, count]) => sum + count, 0);
|
|
190
|
+
|
|
191
|
+
console.log(chalk.bold('Contributors:\n'));
|
|
192
|
+
sorted.forEach(([author, lines]) => {
|
|
193
|
+
const percentage = ((lines / total) * 100).toFixed(1);
|
|
194
|
+
const bar = '█'.repeat(Math.floor(percentage / 2));
|
|
195
|
+
console.log(`${chalk.cyan(author)}`);
|
|
196
|
+
console.log(` ${chalk.green(bar)} ${lines} lines (${percentage}%)\n`);
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
function showHelp() {
|
|
206
|
+
const chalk = require('chalk');
|
|
207
|
+
|
|
208
|
+
console.log(chalk.bold.magenta('\n╔════════════════════════════════════════════════════════════╗'));
|
|
209
|
+
console.log(chalk.bold.magenta('║ �️ GIT CREEPER v1.0.0 ║'));
|
|
210
|
+
console.log(chalk.bold.magenta('║ Creep on your Git History with Smart Insights ║'));
|
|
211
|
+
console.log(chalk.bold.magenta('╚════════════════════════════════════════════════════════════╝\n'));
|
|
212
|
+
|
|
213
|
+
console.log(chalk.bold('🚀 CORE COMMANDS:\n'));
|
|
214
|
+
|
|
215
|
+
console.log(chalk.cyan(' timeline') + chalk.gray(' View project evolution over time'));
|
|
216
|
+
console.log(chalk.gray(' git-creeper timeline --days 60\n'));
|
|
217
|
+
|
|
218
|
+
console.log(chalk.cyan(' stats') + chalk.gray(' Comprehensive repository statistics'));
|
|
219
|
+
console.log(chalk.gray(' git-creeper stats\n'));
|
|
220
|
+
|
|
221
|
+
console.log(chalk.cyan(' hotspots') + chalk.gray(' Find frequently changed files'));
|
|
222
|
+
console.log(chalk.gray(' git-creeper hotspots --limit 20\n'));
|
|
223
|
+
|
|
224
|
+
console.log(chalk.cyan(' contributors') + chalk.gray(' Analyze team member contributions'));
|
|
225
|
+
console.log(chalk.gray(' git-creeper contributors\n'));
|
|
226
|
+
|
|
227
|
+
console.log(chalk.bold('🔍 ANALYSIS TOOLS:\n'));
|
|
228
|
+
|
|
229
|
+
console.log(chalk.cyan(' changelog') + chalk.gray(' Generate organized changelog'));
|
|
230
|
+
console.log(chalk.gray(' git-creeper changelog --from v1.0.0 --to v2.0.0\n'));
|
|
231
|
+
|
|
232
|
+
console.log(chalk.cyan(' compare') + chalk.gray(' Compare branches or commits'));
|
|
233
|
+
console.log(chalk.gray(' git-creeper compare --from main --to develop\n'));
|
|
234
|
+
|
|
235
|
+
console.log(chalk.cyan(' search') + chalk.gray(' Search commits with filters'));
|
|
236
|
+
console.log(chalk.gray(' git-creeper search "bug" --author "John"\n'));
|
|
237
|
+
|
|
238
|
+
console.log(chalk.cyan(' blame-smart') + chalk.gray(' Analyze file contributors'));
|
|
239
|
+
console.log(chalk.gray(' git-creeper blame-smart src/index.js\n'));
|
|
240
|
+
|
|
241
|
+
console.log(chalk.bold('🤖 SMART FEATURES:\n'));
|
|
242
|
+
|
|
243
|
+
console.log(chalk.cyan(' smart-commit') + chalk.gray(' Get smart commit message suggestions'));
|
|
244
|
+
console.log(chalk.gray(' git-creeper smart-commit\n'));
|
|
245
|
+
|
|
246
|
+
console.log(chalk.cyan(' review') + chalk.gray(' Pattern-based code review'));
|
|
247
|
+
console.log(chalk.gray(' git-creeper review\n'));
|
|
248
|
+
|
|
249
|
+
console.log(chalk.cyan(' bug-risk') + chalk.gray(' Predict files with high bug risk'));
|
|
250
|
+
console.log(chalk.gray(' git-creeper bug-risk\n'));
|
|
251
|
+
|
|
252
|
+
console.log(chalk.cyan(' refactor') + chalk.gray(' Get code quality suggestions'));
|
|
253
|
+
console.log(chalk.gray(' git-creeper refactor\n'));
|
|
254
|
+
|
|
255
|
+
console.log(chalk.bold('📊 INSIGHTS & REPORTS:\n'));
|
|
256
|
+
|
|
257
|
+
console.log(chalk.cyan(' insights') + chalk.gray(' Get smart recommendations'));
|
|
258
|
+
console.log(chalk.gray(' git-creeper insights\n'));
|
|
259
|
+
|
|
260
|
+
console.log(chalk.cyan(' visualize') + chalk.gray(' Show ASCII visualizations'));
|
|
261
|
+
console.log(chalk.gray(' git-creeper visualize --type heatmap\n'));
|
|
262
|
+
|
|
263
|
+
console.log(chalk.cyan(' export') + chalk.gray(' Export data to file'));
|
|
264
|
+
console.log(chalk.gray(' git-creeper export --format markdown\n'));
|
|
265
|
+
|
|
266
|
+
console.log(chalk.cyan(' lang') + chalk.gray(' Set default language (en, tr)'));
|
|
267
|
+
console.log(chalk.gray(' git-creeper lang tr\n'));
|
|
268
|
+
|
|
269
|
+
console.log(chalk.bold('⚙️ OPTIONS:\n'));
|
|
270
|
+
console.log(chalk.gray(' -v, --version Display version number'));
|
|
271
|
+
console.log(chalk.gray(' -h, --help Show command help\n'));
|
|
272
|
+
|
|
273
|
+
console.log(chalk.bold('💡 TIPS:\n'));
|
|
274
|
+
console.log(chalk.gray(' • Use conventional commits (feat:, fix:, docs:) for better results'));
|
|
275
|
+
console.log(chalk.gray(' • Run "git-creeper insights" regularly for code quality recommendations'));
|
|
276
|
+
console.log(chalk.gray(' • Try smart features: smart-commit, review, bug-risk, refactor'));
|
|
277
|
+
console.log(chalk.gray(' • Export reports for team meetings and stakeholder updates'));
|
|
278
|
+
console.log(chalk.gray(' • Set your preferred language once with "git-creeper lang tr"\n'));
|
|
279
|
+
|
|
280
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
281
|
+
console.log(chalk.dim('Made with ❤️ by @levantedev | MIT License'));
|
|
282
|
+
console.log(chalk.dim('npm: npmjs.com/package/git-creeper'));
|
|
283
|
+
console.log(chalk.dim('GitHub: github.com/levantedev/git-creeper\n'));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
module.exports = { timeline, hotspots, changelog, blameAnalysis, showHelp };
|
|
289
|
+
|
|
290
|
+
async function stats() {
|
|
291
|
+
const title = t('stats.title');
|
|
292
|
+
const titlePadding = Math.floor((60 - title.length) / 2);
|
|
293
|
+
|
|
294
|
+
console.log(chalk.bold.blue('\n╔════════════════════════════════════════════════════════════╗'));
|
|
295
|
+
console.log(chalk.bold.blue(`║${' '.repeat(titlePadding)}${title}${' '.repeat(60 - title.length - titlePadding)}║`));
|
|
296
|
+
console.log(chalk.bold.blue('╚════════════════════════════════════════════════════════════╝\n'));
|
|
297
|
+
|
|
298
|
+
const log = await git.log();
|
|
299
|
+
const commits = log.all;
|
|
300
|
+
|
|
301
|
+
if (commits.length === 0) {
|
|
302
|
+
console.log(chalk.gray(t('timeline.noCommits')));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Date range
|
|
307
|
+
const firstCommit = new Date(commits[commits.length - 1].date);
|
|
308
|
+
const lastCommit = new Date(commits[0].date);
|
|
309
|
+
const daysDiff = Math.floor((lastCommit - firstCommit) / (1000 * 60 * 60 * 24));
|
|
310
|
+
|
|
311
|
+
// Average message length
|
|
312
|
+
const avgMessageLength = commits.reduce((sum, c) => sum + c.message.length, 0) / commits.length;
|
|
313
|
+
|
|
314
|
+
// Contributors
|
|
315
|
+
const contributors = {};
|
|
316
|
+
commits.forEach(c => {
|
|
317
|
+
contributors[c.author_name] = (contributors[c.author_name] || 0) + 1;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Commit types
|
|
321
|
+
const types = { feat: 0, fix: 0, docs: 0, other: 0 };
|
|
322
|
+
commits.forEach(c => {
|
|
323
|
+
if (c.message.startsWith('feat')) types.feat++;
|
|
324
|
+
else if (c.message.startsWith('fix')) types.fix++;
|
|
325
|
+
else if (c.message.startsWith('docs')) types.docs++;
|
|
326
|
+
else types.other++;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Activity by day and hour
|
|
330
|
+
const days = {};
|
|
331
|
+
const hours = {};
|
|
332
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
333
|
+
|
|
334
|
+
commits.forEach(c => {
|
|
335
|
+
const date = new Date(c.date);
|
|
336
|
+
const day = dayNames[date.getDay()];
|
|
337
|
+
const hour = date.getHours();
|
|
338
|
+
days[day] = (days[day] || 0) + 1;
|
|
339
|
+
hours[hour] = (hours[hour] || 0) + 1;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const mostActiveDay = Object.entries(days).sort((a, b) => b[1] - a[1])[0];
|
|
343
|
+
const peakHour = Object.entries(hours).sort((a, b) => b[1] - a[1])[0];
|
|
344
|
+
const topContributor = Object.entries(contributors).sort((a, b) => b[1] - a[1])[0];
|
|
345
|
+
|
|
346
|
+
// Display in clean table format
|
|
347
|
+
console.log(chalk.bold(t('stats.overview')));
|
|
348
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
349
|
+
console.log(` ${t('stats.totalCommits').padEnd(25)} ${chalk.cyan(commits.length.toString().padStart(10))}`);
|
|
350
|
+
console.log(` ${t('stats.repoAge').padEnd(25)} ${chalk.cyan((daysDiff + ' ' + t('stats.days')).padStart(10))}`);
|
|
351
|
+
console.log(` ${t('stats.commitsPerDay').padEnd(25)} ${chalk.cyan((commits.length / Math.max(daysDiff, 1)).toFixed(2).padStart(10))}`);
|
|
352
|
+
console.log(` ${t('stats.avgMessageLength').padEnd(25)} ${chalk.cyan((avgMessageLength.toFixed(0) + ' ' + t('stats.chars')).padStart(10))}`);
|
|
353
|
+
console.log();
|
|
354
|
+
|
|
355
|
+
console.log(chalk.bold(t('stats.contributors')));
|
|
356
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
357
|
+
console.log(` ${t('stats.totalContributors').padEnd(25)} ${chalk.cyan(Object.keys(contributors).length.toString().padStart(10))}`);
|
|
358
|
+
console.log(` ${t('stats.mostProductive').padEnd(25)} ${chalk.cyan(topContributor[0].padStart(10))}`);
|
|
359
|
+
console.log(` ${t('stats.theirCommits').padEnd(25)} ${chalk.cyan(topContributor[1].toString().padStart(10))}`);
|
|
360
|
+
console.log();
|
|
361
|
+
|
|
362
|
+
console.log(chalk.bold(t('stats.commitTypes')));
|
|
363
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
364
|
+
console.log(` ${t('stats.features').padEnd(25)} ${chalk.green(types.feat.toString().padStart(10))}`);
|
|
365
|
+
console.log(` ${t('stats.bugFixes').padEnd(25)} ${chalk.red(types.fix.toString().padStart(10))}`);
|
|
366
|
+
console.log(` ${t('stats.documentation').padEnd(25)} ${chalk.blue(types.docs.toString().padStart(10))}`);
|
|
367
|
+
console.log(` ${t('stats.other').padEnd(25)} ${chalk.gray(types.other.toString().padStart(10))}`);
|
|
368
|
+
console.log();
|
|
369
|
+
|
|
370
|
+
console.log(chalk.bold(t('stats.activityPatterns')));
|
|
371
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
372
|
+
console.log(` ${t('stats.mostActiveDay').padEnd(25)} ${chalk.yellow(mostActiveDay[0].padStart(10))} (${mostActiveDay[1]} ${t('stats.commits')})`);
|
|
373
|
+
console.log(` ${t('stats.peakHour').padEnd(25)} ${chalk.yellow((peakHour[0] + ':00').padStart(10))} (${peakHour[1]} ${t('stats.commits')})`);
|
|
374
|
+
console.log();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function contributors() {
|
|
378
|
+
const title = t('contributors.title');
|
|
379
|
+
const titlePadding = Math.floor((60 - title.length) / 2);
|
|
380
|
+
|
|
381
|
+
console.log(chalk.bold.magenta('\n╔════════════════════════════════════════════════════════════╗'));
|
|
382
|
+
console.log(chalk.bold.magenta(`║${' '.repeat(titlePadding)}${title}${' '.repeat(60 - title.length - titlePadding)}║`));
|
|
383
|
+
console.log(chalk.bold.magenta('╚════════════════════════════════════════════════════════════╝\n'));
|
|
384
|
+
|
|
385
|
+
const log = await git.log();
|
|
386
|
+
const commits = log.all;
|
|
387
|
+
|
|
388
|
+
const contributorData = {};
|
|
389
|
+
|
|
390
|
+
// Get commit data
|
|
391
|
+
commits.forEach(c => {
|
|
392
|
+
if (!contributorData[c.author_name]) {
|
|
393
|
+
contributorData[c.author_name] = {
|
|
394
|
+
commits: 0,
|
|
395
|
+
firstCommit: c.date,
|
|
396
|
+
lastCommit: c.date,
|
|
397
|
+
insertions: 0,
|
|
398
|
+
deletions: 0
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
contributorData[c.author_name].commits++;
|
|
402
|
+
contributorData[c.author_name].lastCommit = c.date;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const sorted = Object.entries(contributorData).sort((a, b) => b[1].commits - a[1].commits);
|
|
406
|
+
const totalCommits = commits.length;
|
|
407
|
+
|
|
408
|
+
console.log(chalk.bold('RANK CONTRIBUTOR COMMITS SHARE LAST ACTIVE'));
|
|
409
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
410
|
+
|
|
411
|
+
sorted.forEach(([name, data], i) => {
|
|
412
|
+
const percentage = ((data.commits / totalCommits) * 100).toFixed(1);
|
|
413
|
+
const barLength = Math.floor((data.commits / sorted[0][1].commits) * 20);
|
|
414
|
+
const bar = '█'.repeat(barLength);
|
|
415
|
+
const rank = (i + 1).toString().padStart(4);
|
|
416
|
+
const nameShort = name.length > 20 ? name.substring(0, 17) + '...' : name.padEnd(20);
|
|
417
|
+
const commitStr = data.commits.toString().padStart(7);
|
|
418
|
+
const shareStr = (percentage + '%').padStart(7);
|
|
419
|
+
const lastActive = getTimeAgo(new Date(data.lastCommit));
|
|
420
|
+
|
|
421
|
+
console.log(`${chalk.yellow(rank)} ${chalk.cyan(nameShort)} ${chalk.white(commitStr)} ${chalk.magenta(bar)}${shareStr.padStart(10 - barLength)} ${chalk.gray(lastActive)}`);
|
|
422
|
+
});
|
|
423
|
+
console.log();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function compare(options) {
|
|
427
|
+
const from = options.from || 'HEAD~10';
|
|
428
|
+
const to = options.to || 'HEAD';
|
|
429
|
+
|
|
430
|
+
console.log(chalk.bold.yellow('\n╔════════════════════════════════════════════════════════════╗'));
|
|
431
|
+
console.log(chalk.bold.yellow(`║ 🔄 Compare: ${from} → ${to}`.padEnd(61) + '║'));
|
|
432
|
+
console.log(chalk.bold.yellow('╚════════════════════════════════════════════════════════════╝\n'));
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const diff = await git.diffSummary([from, to]);
|
|
436
|
+
const log = await git.log([`${from}..${to}`]);
|
|
437
|
+
|
|
438
|
+
console.log(chalk.bold('📊 Summary:'));
|
|
439
|
+
console.log(` Files Changed: ${chalk.cyan(diff.files.length)}`);
|
|
440
|
+
console.log(` Insertions: ${chalk.green('+' + diff.insertions)}`);
|
|
441
|
+
console.log(` Deletions: ${chalk.red('-' + diff.deletions)}`);
|
|
442
|
+
console.log(` Commits: ${chalk.yellow(log.all.length)}\n`);
|
|
443
|
+
|
|
444
|
+
// Who did what
|
|
445
|
+
console.log(chalk.bold('👥 Contributors:\n'));
|
|
446
|
+
const authorChanges = {};
|
|
447
|
+
log.all.forEach(commit => {
|
|
448
|
+
if (!authorChanges[commit.author_name]) {
|
|
449
|
+
authorChanges[commit.author_name] = 0;
|
|
450
|
+
}
|
|
451
|
+
authorChanges[commit.author_name]++;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
Object.entries(authorChanges).sort((a, b) => b[1] - a[1]).forEach(([author, count]) => {
|
|
455
|
+
console.log(` ${chalk.cyan(author)}: ${chalk.yellow(count + ' commits')}`);
|
|
456
|
+
});
|
|
457
|
+
console.log();
|
|
458
|
+
|
|
459
|
+
console.log(chalk.bold('📁 Changed Files:\n'));
|
|
460
|
+
|
|
461
|
+
diff.files.slice(0, 15).forEach(file => {
|
|
462
|
+
const changes = file.insertions + file.deletions;
|
|
463
|
+
const barLength = Math.min(Math.floor(changes / 2), 30);
|
|
464
|
+
const bar = '█'.repeat(barLength);
|
|
465
|
+
|
|
466
|
+
console.log(chalk.cyan(file.file));
|
|
467
|
+
console.log(` ${chalk.green('+' + file.insertions)} ${chalk.red('-' + file.deletions)} ${chalk.gray(bar)}\n`);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (diff.files.length > 15) {
|
|
471
|
+
console.log(chalk.gray(` ... and ${diff.files.length - 15} more files\n`));
|
|
472
|
+
}
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function search(query, options) {
|
|
479
|
+
console.log(chalk.bold.green('\n╔════════════════════════════════════════════════════════════╗'));
|
|
480
|
+
console.log(chalk.bold.green(`║ 🔍 Search: "${query}"`.padEnd(61) + '║'));
|
|
481
|
+
console.log(chalk.bold.green('╚════════════════════════════════════════════════════════════╝\n'));
|
|
482
|
+
|
|
483
|
+
const logOptions = ['--all', '--grep=' + query, '-i'];
|
|
484
|
+
if (options.author) logOptions.push('--author=' + options.author);
|
|
485
|
+
if (options.since) logOptions.push('--since=' + options.since);
|
|
486
|
+
|
|
487
|
+
const log = await git.log(logOptions);
|
|
488
|
+
const commits = log.all;
|
|
489
|
+
|
|
490
|
+
if (commits.length === 0) {
|
|
491
|
+
console.log(chalk.gray('No commits found matching your search.\n'));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
console.log(chalk.bold(`Found ${chalk.cyan(commits.length)} commits:\n`));
|
|
496
|
+
|
|
497
|
+
commits.slice(0, 20).forEach(commit => {
|
|
498
|
+
const icon = commit.message.startsWith('feat') ? '✨' :
|
|
499
|
+
commit.message.startsWith('fix') ? '🐛' :
|
|
500
|
+
commit.message.startsWith('docs') ? '📚' : '🔧';
|
|
501
|
+
|
|
502
|
+
console.log(`${icon} ${chalk.bold(commit.message)}`);
|
|
503
|
+
console.log(` ${chalk.gray(commit.hash.substring(0, 7))} by ${chalk.cyan(commit.author_name)} - ${chalk.yellow(getTimeAgo(new Date(commit.date)))}`);
|
|
504
|
+
|
|
505
|
+
// Show files changed in this commit
|
|
506
|
+
console.log(chalk.gray(` Files: ${commit.diff?.files?.length || 'N/A'}\n`));
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (commits.length > 20) {
|
|
510
|
+
console.log(chalk.gray(`... and ${commits.length - 20} more results\n`));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Search in specific file if needed
|
|
514
|
+
console.log(chalk.dim('💡 Tip: Use --author "name" or --since "2 weeks ago" to filter results\n'));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function insights() {
|
|
518
|
+
console.log(chalk.bold.magenta('\n╔════════════════════════════════════════════════════════════╗'));
|
|
519
|
+
console.log(chalk.bold.magenta('║ 💡 Smart Insights ║'));
|
|
520
|
+
console.log(chalk.bold.magenta('╚════════════════════════════════════════════════════════════╝\n'));
|
|
521
|
+
|
|
522
|
+
const log = await git.log();
|
|
523
|
+
const commits = log.all;
|
|
524
|
+
|
|
525
|
+
const insights = [];
|
|
526
|
+
|
|
527
|
+
// Check recent activity
|
|
528
|
+
const lastCommit = new Date(commits[0].date);
|
|
529
|
+
const daysSinceLastCommit = Math.floor((new Date() - lastCommit) / (1000 * 60 * 60 * 24));
|
|
530
|
+
|
|
531
|
+
if (daysSinceLastCommit > 7) {
|
|
532
|
+
insights.push({
|
|
533
|
+
type: 'warning',
|
|
534
|
+
icon: '⚠️',
|
|
535
|
+
message: `No commits in the last ${daysSinceLastCommit} days. Project might be inactive.`
|
|
536
|
+
});
|
|
537
|
+
} else if (daysSinceLastCommit === 0) {
|
|
538
|
+
insights.push({
|
|
539
|
+
type: 'success',
|
|
540
|
+
icon: '✅',
|
|
541
|
+
message: 'Active development! Commits made today.'
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Check hotspots
|
|
546
|
+
const fileLog = await git.raw(['log', '--name-only', '--pretty=format:']);
|
|
547
|
+
const files = fileLog.split('\n').filter(f => f.trim());
|
|
548
|
+
const frequency = {};
|
|
549
|
+
files.forEach(file => {
|
|
550
|
+
frequency[file] = (frequency[file] || 0) + 1;
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const hotFiles = Object.entries(frequency).filter(([, count]) => count > 10);
|
|
554
|
+
if (hotFiles.length > 0) {
|
|
555
|
+
insights.push({
|
|
556
|
+
type: 'info',
|
|
557
|
+
icon: '🔥',
|
|
558
|
+
message: `${hotFiles.length} files changed more than 10 times. Consider refactoring.`
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Check commit message quality
|
|
563
|
+
const shortMessages = commits.filter(c => c.message.length < 10).length;
|
|
564
|
+
if (shortMessages > commits.length * 0.3) {
|
|
565
|
+
insights.push({
|
|
566
|
+
type: 'warning',
|
|
567
|
+
icon: '📝',
|
|
568
|
+
message: `${((shortMessages / commits.length) * 100).toFixed(0)}% of commits have short messages. Use descriptive commit messages.`
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Check conventional commits
|
|
573
|
+
const conventionalCommits = commits.filter(c =>
|
|
574
|
+
c.message.startsWith('feat') ||
|
|
575
|
+
c.message.startsWith('fix') ||
|
|
576
|
+
c.message.startsWith('docs')
|
|
577
|
+
).length;
|
|
578
|
+
|
|
579
|
+
if (conventionalCommits < commits.length * 0.5) {
|
|
580
|
+
insights.push({
|
|
581
|
+
type: 'info',
|
|
582
|
+
icon: '💡',
|
|
583
|
+
message: 'Consider using conventional commits (feat:, fix:, docs:) for better changelogs.'
|
|
584
|
+
});
|
|
585
|
+
} else {
|
|
586
|
+
insights.push({
|
|
587
|
+
type: 'success',
|
|
588
|
+
icon: '🎉',
|
|
589
|
+
message: `${((conventionalCommits / commits.length) * 100).toFixed(0)}% of commits follow conventional format. Great job!`
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Display insights
|
|
594
|
+
insights.forEach(insight => {
|
|
595
|
+
const color = insight.type === 'warning' ? chalk.yellow :
|
|
596
|
+
insight.type === 'success' ? chalk.green :
|
|
597
|
+
chalk.blue;
|
|
598
|
+
|
|
599
|
+
console.log(`${insight.icon} ${color(insight.message)}\n`);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
if (insights.length === 0) {
|
|
603
|
+
console.log(chalk.green('✨ Everything looks good! Keep up the great work.\n'));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
module.exports = {
|
|
608
|
+
timeline,
|
|
609
|
+
hotspots,
|
|
610
|
+
changelog,
|
|
611
|
+
blameAnalysis,
|
|
612
|
+
showHelp,
|
|
613
|
+
stats,
|
|
614
|
+
contributors,
|
|
615
|
+
compare,
|
|
616
|
+
search,
|
|
617
|
+
insights,
|
|
618
|
+
exportData,
|
|
619
|
+
visualize,
|
|
620
|
+
setLang
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
async function setLang(locale) {
|
|
624
|
+
const chalk = require('chalk');
|
|
625
|
+
const { setLanguage } = require('./config');
|
|
626
|
+
const { setLocale } = require('./i18n');
|
|
627
|
+
|
|
628
|
+
const validLocales = ['en', 'tr', 'auto'];
|
|
629
|
+
|
|
630
|
+
if (!validLocales.includes(locale)) {
|
|
631
|
+
console.log(chalk.red(`\n✗ Invalid language: ${locale}`));
|
|
632
|
+
console.log(chalk.gray(` Valid options: ${validLocales.join(', ')}\n`));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (setLanguage(locale)) {
|
|
637
|
+
if (locale === 'auto') {
|
|
638
|
+
console.log(chalk.green('\n✓ Language set to auto-detect'));
|
|
639
|
+
} else {
|
|
640
|
+
setLocale(locale);
|
|
641
|
+
console.log(chalk.green(`\n✓ Language set to: ${locale}`));
|
|
642
|
+
}
|
|
643
|
+
console.log(chalk.gray(` Config saved to: ~/.git-creeper/config.json\n`));
|
|
644
|
+
} else {
|
|
645
|
+
console.log(chalk.red('\n✗ Failed to save language preference\n'));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function exportData(options) {
|
|
650
|
+
const format = options.format || 'json';
|
|
651
|
+
const output = options.output || 'git-creeper-report';
|
|
652
|
+
|
|
653
|
+
console.log(chalk.bold.cyan(`\n📦 Exporting data to ${format.toUpperCase()} format...\n`));
|
|
654
|
+
|
|
655
|
+
const log = await git.log();
|
|
656
|
+
const commits = log.all;
|
|
657
|
+
|
|
658
|
+
// Gather all data
|
|
659
|
+
const data = {
|
|
660
|
+
summary: {
|
|
661
|
+
totalCommits: commits.length,
|
|
662
|
+
firstCommit: commits[commits.length - 1]?.date,
|
|
663
|
+
lastCommit: commits[0]?.date,
|
|
664
|
+
contributors: {}
|
|
665
|
+
},
|
|
666
|
+
commits: commits.slice(0, 100).map(c => ({
|
|
667
|
+
hash: c.hash,
|
|
668
|
+
message: c.message,
|
|
669
|
+
author: c.author_name,
|
|
670
|
+
date: c.date
|
|
671
|
+
}))
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
commits.forEach(c => {
|
|
675
|
+
data.summary.contributors[c.author_name] = (data.summary.contributors[c.author_name] || 0) + 1;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const fs = require('fs');
|
|
679
|
+
|
|
680
|
+
if (format === 'json') {
|
|
681
|
+
const filename = `${output}.json`;
|
|
682
|
+
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
|
|
683
|
+
console.log(chalk.green(`✅ Exported to ${filename}\n`));
|
|
684
|
+
} else if (format === 'markdown') {
|
|
685
|
+
const filename = `${output}.md`;
|
|
686
|
+
let md = `# Git-Creeper Report\n\n`;
|
|
687
|
+
md += `## Summary\n\n`;
|
|
688
|
+
md += `- Total Commits: ${data.summary.totalCommits}\n`;
|
|
689
|
+
md += `- Contributors: ${Object.keys(data.summary.contributors).length}\n\n`;
|
|
690
|
+
md += `## Top Contributors\n\n`;
|
|
691
|
+
Object.entries(data.summary.contributors)
|
|
692
|
+
.sort((a, b) => b[1] - a[1])
|
|
693
|
+
.slice(0, 10)
|
|
694
|
+
.forEach(([name, count]) => {
|
|
695
|
+
md += `- ${name}: ${count} commits\n`;
|
|
696
|
+
});
|
|
697
|
+
md += `\n## Recent Commits\n\n`;
|
|
698
|
+
data.commits.slice(0, 20).forEach(c => {
|
|
699
|
+
md += `- **${c.message}** (${c.hash.substring(0, 7)}) by ${c.author}\n`;
|
|
700
|
+
});
|
|
701
|
+
fs.writeFileSync(filename, md);
|
|
702
|
+
console.log(chalk.green(`✅ Exported to ${filename}\n`));
|
|
703
|
+
} else if (format === 'html') {
|
|
704
|
+
const filename = `${output}.html`;
|
|
705
|
+
let html = `<!DOCTYPE html>
|
|
706
|
+
<html>
|
|
707
|
+
<head>
|
|
708
|
+
<title>Git Story Report</title>
|
|
709
|
+
<style>
|
|
710
|
+
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
|
711
|
+
h1 { color: #333; }
|
|
712
|
+
.summary { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
|
713
|
+
.commit { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; }
|
|
714
|
+
.hash { color: #666; font-family: monospace; }
|
|
715
|
+
.author { color: #0066cc; }
|
|
716
|
+
</style>
|
|
717
|
+
</head>
|
|
718
|
+
<body>
|
|
719
|
+
<h1>📖 Git Story Report</h1>
|
|
720
|
+
<div class="summary">
|
|
721
|
+
<h2>Summary</h2>
|
|
722
|
+
<p>Total Commits: <strong>${data.summary.totalCommits}</strong></p>
|
|
723
|
+
<p>Contributors: <strong>${Object.keys(data.summary.contributors).length}</strong></p>
|
|
724
|
+
</div>
|
|
725
|
+
<h2>Recent Commits</h2>`;
|
|
726
|
+
data.commits.slice(0, 20).forEach(c => {
|
|
727
|
+
html += `
|
|
728
|
+
<div class="commit">
|
|
729
|
+
<strong>${c.message}</strong><br>
|
|
730
|
+
<span class="hash">${c.hash.substring(0, 7)}</span> by <span class="author">${c.author}</span>
|
|
731
|
+
</div>`;
|
|
732
|
+
});
|
|
733
|
+
html += `
|
|
734
|
+
</body>
|
|
735
|
+
</html>`;
|
|
736
|
+
fs.writeFileSync(filename, html);
|
|
737
|
+
console.log(chalk.green(`✅ Exported to ${filename}\n`));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function visualize(options) {
|
|
742
|
+
const type = options.type || 'frequency';
|
|
743
|
+
|
|
744
|
+
console.log(chalk.bold.magenta('\n╔════════════════════════════════════════════════════════════╗'));
|
|
745
|
+
console.log(chalk.bold.magenta(`║ 📊 Visualization: ${type}`.padEnd(61) + '║'));
|
|
746
|
+
console.log(chalk.bold.magenta('╚════════════════════════════════════════════════════════════╝\n'));
|
|
747
|
+
|
|
748
|
+
const log = await git.log();
|
|
749
|
+
const commits = log.all;
|
|
750
|
+
|
|
751
|
+
if (type === 'frequency') {
|
|
752
|
+
// Commit frequency over last 30 days
|
|
753
|
+
console.log(chalk.bold('📈 Commit Frequency (Last 30 Days)\n'));
|
|
754
|
+
|
|
755
|
+
const days = 30;
|
|
756
|
+
const frequency = Array(days).fill(0);
|
|
757
|
+
const now = new Date();
|
|
758
|
+
|
|
759
|
+
commits.forEach(c => {
|
|
760
|
+
const commitDate = new Date(c.date);
|
|
761
|
+
const daysAgo = Math.floor((now - commitDate) / (1000 * 60 * 60 * 24));
|
|
762
|
+
if (daysAgo < days) {
|
|
763
|
+
frequency[days - 1 - daysAgo]++;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const maxFreq = Math.max(...frequency, 1);
|
|
768
|
+
|
|
769
|
+
frequency.forEach((count, i) => {
|
|
770
|
+
const daysAgo = days - i;
|
|
771
|
+
const barLength = Math.floor((count / maxFreq) * 40);
|
|
772
|
+
const bar = '█'.repeat(barLength);
|
|
773
|
+
const color = count === 0 ? chalk.gray : count > maxFreq * 0.7 ? chalk.green : chalk.yellow;
|
|
774
|
+
console.log(`${String(daysAgo).padStart(2)}d ago: ${color(bar)} ${count}`);
|
|
775
|
+
});
|
|
776
|
+
console.log();
|
|
777
|
+
|
|
778
|
+
} else if (type === 'heatmap') {
|
|
779
|
+
// Contributor activity heatmap
|
|
780
|
+
console.log(chalk.bold('🔥 Contributor Activity Heatmap (Last 7 Days)\n'));
|
|
781
|
+
|
|
782
|
+
const contributors = {};
|
|
783
|
+
const days = 7;
|
|
784
|
+
const now = new Date();
|
|
785
|
+
|
|
786
|
+
commits.forEach(c => {
|
|
787
|
+
const commitDate = new Date(c.date);
|
|
788
|
+
const daysAgo = Math.floor((now - commitDate) / (1000 * 60 * 60 * 24));
|
|
789
|
+
if (daysAgo < days) {
|
|
790
|
+
if (!contributors[c.author_name]) {
|
|
791
|
+
contributors[c.author_name] = Array(days).fill(0);
|
|
792
|
+
}
|
|
793
|
+
contributors[c.author_name][days - 1 - daysAgo]++;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
798
|
+
console.log(' ' + dayLabels.map(d => d.padEnd(4)).join(''));
|
|
799
|
+
|
|
800
|
+
Object.entries(contributors).forEach(([name, activity]) => {
|
|
801
|
+
const nameShort = name.substring(0, 8).padEnd(8);
|
|
802
|
+
const heatmap = activity.map(count => {
|
|
803
|
+
if (count === 0) return chalk.gray('░░░░');
|
|
804
|
+
if (count <= 2) return chalk.yellow('▒▒▒▒');
|
|
805
|
+
if (count <= 5) return chalk.yellow('▓▓▓▓');
|
|
806
|
+
return chalk.red('████');
|
|
807
|
+
}).join('');
|
|
808
|
+
console.log(`${nameShort} ${heatmap}`);
|
|
809
|
+
});
|
|
810
|
+
console.log();
|
|
811
|
+
|
|
812
|
+
} else if (type === 'trends') {
|
|
813
|
+
// File change trends
|
|
814
|
+
console.log(chalk.bold('📁 File Change Trends (Top 10 Files)\n'));
|
|
815
|
+
|
|
816
|
+
const fileLog = await git.raw(['log', '--name-only', '--pretty=format:']);
|
|
817
|
+
const files = fileLog.split('\n').filter(f => f.trim());
|
|
818
|
+
const frequency = {};
|
|
819
|
+
|
|
820
|
+
files.forEach(file => {
|
|
821
|
+
frequency[file] = (frequency[file] || 0) + 1;
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const sorted = Object.entries(frequency).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
825
|
+
const maxCount = sorted[0]?.[1] || 1;
|
|
826
|
+
|
|
827
|
+
sorted.forEach(([file, count]) => {
|
|
828
|
+
const barLength = Math.floor((count / maxCount) * 40);
|
|
829
|
+
const bar = '█'.repeat(barLength);
|
|
830
|
+
const fileShort = file.length > 30 ? '...' + file.slice(-27) : file.padEnd(30);
|
|
831
|
+
console.log(`${chalk.cyan(fileShort)} ${chalk.red(bar)} ${count}`);
|
|
832
|
+
});
|
|
833
|
+
console.log();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
console.log(chalk.dim('💡 Try: --type frequency, --type heatmap, or --type trends\n'));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// AI-Powered Features
|
|
840
|
+
async function smartCommitMessage() {
|
|
841
|
+
const { t } = require('./i18n');
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
const git = simpleGit();
|
|
845
|
+
const status = await git.status();
|
|
846
|
+
|
|
847
|
+
if (status.files.length === 0) {
|
|
848
|
+
console.log(chalk.yellow(t('ai.noChanges')));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
console.log(chalk.blue.bold(t('ai.smartCommitTitle')));
|
|
853
|
+
console.log('─'.repeat(60));
|
|
854
|
+
|
|
855
|
+
// Analyze changed files
|
|
856
|
+
const changedFiles = status.files;
|
|
857
|
+
const suggestions = await analyzeChangesForCommit(changedFiles);
|
|
858
|
+
|
|
859
|
+
console.log(chalk.green('📝 ' + t('ai.suggestedMessages') + ':'));
|
|
860
|
+
suggestions.forEach((msg, index) => {
|
|
861
|
+
console.log(chalk.cyan(`${index + 1}. ${msg}`));
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
console.log('\n' + chalk.gray(t('ai.commitTip')));
|
|
865
|
+
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error(chalk.red(t('error') + ':'), error.message);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
async function analyzeChangesForCommit(files) {
|
|
873
|
+
const suggestions = [];
|
|
874
|
+
|
|
875
|
+
// Analyze file patterns
|
|
876
|
+
const hasNewFiles = files.some(f => f.index === 'A');
|
|
877
|
+
const hasDeletedFiles = files.some(f => f.working_dir === 'D');
|
|
878
|
+
const hasModifiedFiles = files.some(f => f.working_dir === 'M');
|
|
879
|
+
|
|
880
|
+
// Check file types
|
|
881
|
+
const jsFiles = files.filter(f => f.path.endsWith('.js') || f.path.endsWith('.ts'));
|
|
882
|
+
const cssFiles = files.filter(f => f.path.endsWith('.css') || f.path.endsWith('.scss'));
|
|
883
|
+
const configFiles = files.filter(f => f.path.includes('config') || f.path.includes('package.json'));
|
|
884
|
+
const testFiles = files.filter(f => f.path.includes('test') || f.path.includes('spec'));
|
|
885
|
+
|
|
886
|
+
// Generate smart suggestions
|
|
887
|
+
if (testFiles.length > 0) {
|
|
888
|
+
suggestions.push('test: add unit tests for new features');
|
|
889
|
+
suggestions.push('test: update test cases');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (configFiles.length > 0) {
|
|
893
|
+
suggestions.push('config: update project configuration');
|
|
894
|
+
suggestions.push('chore: update dependencies');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (hasNewFiles && jsFiles.length > 0) {
|
|
898
|
+
suggestions.push('feat: add new component/module');
|
|
899
|
+
suggestions.push('feat: implement new functionality');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (hasModifiedFiles && jsFiles.length > 0) {
|
|
903
|
+
suggestions.push('fix: resolve bug in core functionality');
|
|
904
|
+
suggestions.push('refactor: improve code structure');
|
|
905
|
+
suggestions.push('perf: optimize performance');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (cssFiles.length > 0) {
|
|
909
|
+
suggestions.push('style: update UI components');
|
|
910
|
+
suggestions.push('style: improve responsive design');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (hasDeletedFiles) {
|
|
914
|
+
suggestions.push('refactor: remove unused code');
|
|
915
|
+
suggestions.push('cleanup: remove deprecated files');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Default suggestions
|
|
919
|
+
if (suggestions.length === 0) {
|
|
920
|
+
suggestions.push('feat: add new feature');
|
|
921
|
+
suggestions.push('fix: resolve issue');
|
|
922
|
+
suggestions.push('docs: update documentation');
|
|
923
|
+
suggestions.push('refactor: improve code quality');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return suggestions.slice(0, 5); // Return top 5 suggestions
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function codeReviewAssistant() {
|
|
930
|
+
const { t } = require('./i18n');
|
|
931
|
+
|
|
932
|
+
try {
|
|
933
|
+
const git = simpleGit();
|
|
934
|
+
const diff = await git.diff(['--cached']);
|
|
935
|
+
|
|
936
|
+
if (!diff) {
|
|
937
|
+
console.log(chalk.yellow(t('noStagedChanges')));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
console.log(chalk.blue.bold(t('codeReviewTitle')));
|
|
942
|
+
console.log('─'.repeat(60));
|
|
943
|
+
|
|
944
|
+
const issues = await analyzeCodeForReview(diff);
|
|
945
|
+
|
|
946
|
+
if (issues.length === 0) {
|
|
947
|
+
console.log(chalk.green('✅ ' + t('noIssuesFound')));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
console.log(chalk.yellow('⚠️ ' + t('foundIssues') + ':'));
|
|
952
|
+
issues.forEach((issue, index) => {
|
|
953
|
+
console.log(chalk.red(`${index + 1}. ${issue.type}: ${issue.message}`));
|
|
954
|
+
if (issue.suggestion) {
|
|
955
|
+
console.log(chalk.gray(` 💡 ${issue.suggestion}`));
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
} catch (error) {
|
|
960
|
+
console.error(chalk.red(t('error') + ':'), error.message);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function analyzeCodeForReview(diff) {
|
|
965
|
+
const issues = [];
|
|
966
|
+
const lines = diff.split('\n');
|
|
967
|
+
|
|
968
|
+
lines.forEach((line, index) => {
|
|
969
|
+
if (line.startsWith('+')) {
|
|
970
|
+
const code = line.substring(1).trim();
|
|
971
|
+
|
|
972
|
+
// Check for common issues
|
|
973
|
+
if (code.includes('console.log')) {
|
|
974
|
+
issues.push({
|
|
975
|
+
type: 'Debug Code',
|
|
976
|
+
message: 'Console.log statement found',
|
|
977
|
+
suggestion: 'Remove debug statements before commit'
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (code.includes('TODO') || code.includes('FIXME')) {
|
|
982
|
+
issues.push({
|
|
983
|
+
type: 'TODO/FIXME',
|
|
984
|
+
message: 'Unresolved TODO or FIXME comment',
|
|
985
|
+
suggestion: 'Complete the task or create an issue'
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (code.length > 120) {
|
|
990
|
+
issues.push({
|
|
991
|
+
type: 'Long Line',
|
|
992
|
+
message: 'Line exceeds 120 characters',
|
|
993
|
+
suggestion: 'Break long lines for better readability'
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (code.includes('var ')) {
|
|
998
|
+
issues.push({
|
|
999
|
+
type: 'Deprecated Syntax',
|
|
1000
|
+
message: 'Use of var instead of let/const',
|
|
1001
|
+
suggestion: 'Use let or const for better scoping'
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (code.includes('==') && !code.includes('===')) {
|
|
1006
|
+
issues.push({
|
|
1007
|
+
type: 'Loose Equality',
|
|
1008
|
+
message: 'Use of == instead of ===',
|
|
1009
|
+
suggestion: 'Use strict equality (===) for type safety'
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
return issues;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function bugPrediction() {
|
|
1019
|
+
const { t } = require('./i18n');
|
|
1020
|
+
|
|
1021
|
+
try {
|
|
1022
|
+
const git = simpleGit();
|
|
1023
|
+
const logs = await git.log(['--since=3 months ago', '--pretty=format:%H|%s|%an|%ad']);
|
|
1024
|
+
|
|
1025
|
+
console.log(chalk.blue.bold(t('bugPredictionTitle')));
|
|
1026
|
+
console.log('─'.repeat(60));
|
|
1027
|
+
|
|
1028
|
+
const riskAnalysis = await analyzeBugRisk(logs.all);
|
|
1029
|
+
|
|
1030
|
+
console.log(chalk.red('🚨 ' + t('highRiskFiles') + ':'));
|
|
1031
|
+
riskAnalysis.highRisk.forEach(file => {
|
|
1032
|
+
console.log(chalk.red(` • ${file.path} (Risk: ${file.score}%)`));
|
|
1033
|
+
console.log(chalk.gray(` ${file.reason}`));
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
console.log('\n' + chalk.yellow('⚠️ ' + t('mediumRiskFiles') + ':'));
|
|
1037
|
+
riskAnalysis.mediumRisk.forEach(file => {
|
|
1038
|
+
console.log(chalk.yellow(` • ${file.path} (Risk: ${file.score}%)`));
|
|
1039
|
+
console.log(chalk.gray(` ${file.reason}`));
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
console.error(chalk.red(t('error') + ':'), error.message);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function analyzeBugRisk(commits) {
|
|
1048
|
+
const fileStats = {};
|
|
1049
|
+
const bugKeywords = ['fix', 'bug', 'error', 'issue', 'problem', 'crash'];
|
|
1050
|
+
|
|
1051
|
+
// Analyze commit patterns
|
|
1052
|
+
for (const commit of commits) {
|
|
1053
|
+
const message = commit.message.toLowerCase();
|
|
1054
|
+
const isBugFix = bugKeywords.some(keyword => message.includes(keyword));
|
|
1055
|
+
|
|
1056
|
+
if (isBugFix) {
|
|
1057
|
+
try {
|
|
1058
|
+
const git = simpleGit();
|
|
1059
|
+
const files = await git.show(['--name-only', commit.hash]);
|
|
1060
|
+
const changedFiles = files.split('\n').filter(f => f.trim());
|
|
1061
|
+
|
|
1062
|
+
changedFiles.forEach(file => {
|
|
1063
|
+
if (!fileStats[file]) {
|
|
1064
|
+
fileStats[file] = { bugFixes: 0, totalChanges: 0 };
|
|
1065
|
+
}
|
|
1066
|
+
fileStats[file].bugFixes++;
|
|
1067
|
+
fileStats[file].totalChanges++;
|
|
1068
|
+
});
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
// Skip if commit not accessible
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Calculate risk scores
|
|
1076
|
+
const riskFiles = Object.entries(fileStats)
|
|
1077
|
+
.map(([path, stats]) => ({
|
|
1078
|
+
path,
|
|
1079
|
+
score: Math.round((stats.bugFixes / stats.totalChanges) * 100),
|
|
1080
|
+
reason: `${stats.bugFixes} bug fixes out of ${stats.totalChanges} changes`
|
|
1081
|
+
}))
|
|
1082
|
+
.sort((a, b) => b.score - a.score);
|
|
1083
|
+
|
|
1084
|
+
return {
|
|
1085
|
+
highRisk: riskFiles.filter(f => f.score >= 50),
|
|
1086
|
+
mediumRisk: riskFiles.filter(f => f.score >= 25 && f.score < 50)
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function refactorSuggestions() {
|
|
1091
|
+
const { t } = require('./i18n');
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
console.log(chalk.blue.bold(t('refactorTitle')));
|
|
1095
|
+
console.log('─'.repeat(60));
|
|
1096
|
+
|
|
1097
|
+
const suggestions = await analyzeCodeQuality();
|
|
1098
|
+
|
|
1099
|
+
if (suggestions.length === 0) {
|
|
1100
|
+
console.log(chalk.green('✅ ' + t('codeQualityGood')));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
console.log(chalk.yellow('💡 ' + t('refactorSuggestions') + ':'));
|
|
1105
|
+
suggestions.forEach((suggestion, index) => {
|
|
1106
|
+
console.log(chalk.cyan(`${index + 1}. ${suggestion.title}`));
|
|
1107
|
+
console.log(chalk.gray(` 📁 ${suggestion.file}`));
|
|
1108
|
+
console.log(chalk.gray(` 📝 ${suggestion.description}`));
|
|
1109
|
+
console.log(chalk.gray(` 🎯 ${suggestion.benefit}`));
|
|
1110
|
+
console.log('');
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
console.error(chalk.red(t('error') + ':'), error.message);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async function analyzeCodeQuality() {
|
|
1119
|
+
const suggestions = [];
|
|
1120
|
+
const fs = require('fs');
|
|
1121
|
+
const path = require('path');
|
|
1122
|
+
|
|
1123
|
+
// Find JavaScript/TypeScript files
|
|
1124
|
+
const findFiles = (dir, fileList = []) => {
|
|
1125
|
+
const files = fs.readdirSync(dir);
|
|
1126
|
+
|
|
1127
|
+
files.forEach(file => {
|
|
1128
|
+
const filePath = path.join(dir, file);
|
|
1129
|
+
const stat = fs.statSync(filePath);
|
|
1130
|
+
|
|
1131
|
+
if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
|
|
1132
|
+
findFiles(filePath, fileList);
|
|
1133
|
+
} else if (file.endsWith('.js') || file.endsWith('.ts')) {
|
|
1134
|
+
fileList.push(filePath);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
return fileList;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
try {
|
|
1142
|
+
const files = findFiles('.');
|
|
1143
|
+
|
|
1144
|
+
for (const file of files) {
|
|
1145
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
1146
|
+
const lines = content.split('\n');
|
|
1147
|
+
|
|
1148
|
+
// Check file size
|
|
1149
|
+
if (lines.length > 300) {
|
|
1150
|
+
suggestions.push({
|
|
1151
|
+
title: 'Large File Detected',
|
|
1152
|
+
file: file,
|
|
1153
|
+
description: `File has ${lines.length} lines, consider splitting into smaller modules`,
|
|
1154
|
+
benefit: 'Improves maintainability and readability'
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Check for long functions
|
|
1159
|
+
let functionLines = 0;
|
|
1160
|
+
let inFunction = false;
|
|
1161
|
+
|
|
1162
|
+
lines.forEach(line => {
|
|
1163
|
+
if (line.includes('function') || line.includes('=>')) {
|
|
1164
|
+
inFunction = true;
|
|
1165
|
+
functionLines = 0;
|
|
1166
|
+
}
|
|
1167
|
+
if (inFunction) functionLines++;
|
|
1168
|
+
if (line.includes('}') && inFunction && functionLines > 50) {
|
|
1169
|
+
suggestions.push({
|
|
1170
|
+
title: 'Long Function Detected',
|
|
1171
|
+
file: file,
|
|
1172
|
+
description: `Function has ${functionLines} lines, consider breaking it down`,
|
|
1173
|
+
benefit: 'Improves testability and code reuse'
|
|
1174
|
+
});
|
|
1175
|
+
inFunction = false;
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
// Check for code duplication patterns
|
|
1180
|
+
const duplicateLines = findDuplicateLines(lines);
|
|
1181
|
+
if (duplicateLines.length > 0) {
|
|
1182
|
+
suggestions.push({
|
|
1183
|
+
title: 'Code Duplication Found',
|
|
1184
|
+
file: file,
|
|
1185
|
+
description: `${duplicateLines.length} duplicate code blocks detected`,
|
|
1186
|
+
benefit: 'Reduces maintenance overhead and bugs'
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
// Skip files that can't be read
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return suggestions.slice(0, 10); // Return top 10 suggestions
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function findDuplicateLines(lines) {
|
|
1198
|
+
const lineCount = {};
|
|
1199
|
+
const duplicates = [];
|
|
1200
|
+
|
|
1201
|
+
lines.forEach(line => {
|
|
1202
|
+
const trimmed = line.trim();
|
|
1203
|
+
if (trimmed.length > 10 && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
|
1204
|
+
lineCount[trimmed] = (lineCount[trimmed] || 0) + 1;
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
Object.entries(lineCount).forEach(([line, count]) => {
|
|
1209
|
+
if (count > 1) {
|
|
1210
|
+
duplicates.push(line);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
return duplicates;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
module.exports = {
|
|
1218
|
+
timeline,
|
|
1219
|
+
hotspots,
|
|
1220
|
+
changelog,
|
|
1221
|
+
blameAnalysis,
|
|
1222
|
+
showHelp,
|
|
1223
|
+
stats,
|
|
1224
|
+
contributors,
|
|
1225
|
+
compare,
|
|
1226
|
+
search,
|
|
1227
|
+
insights,
|
|
1228
|
+
exportData,
|
|
1229
|
+
visualize,
|
|
1230
|
+
setLang,
|
|
1231
|
+
smartCommitMessage,
|
|
1232
|
+
codeReviewAssistant,
|
|
1233
|
+
bugPrediction,
|
|
1234
|
+
refactorSuggestions
|
|
1235
|
+
};
|