mc-gitpulse 1.0.2 → 1.0.5
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/README.md +6 -1
- package/analyzer.js +370 -0
- package/cli.js +55 -9
- package/package.json +3 -3
- package/public/index.html +73 -19
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
## Screenshot
|
|
52
52
|
|
|
53
|
-

|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
## 📦 Installation
|
|
@@ -80,6 +80,11 @@ gitpulse -g /path/to/repo
|
|
|
80
80
|
# Specify git repository path
|
|
81
81
|
gitpulse --git-folder /path/to/repo
|
|
82
82
|
|
|
83
|
+
# or URL for the git repo
|
|
84
|
+
|
|
85
|
+
gitpulse -g git-repo-url
|
|
86
|
+
|
|
87
|
+
|
|
83
88
|
# Specify custom port
|
|
84
89
|
gitpulse --port 8080 --git-folder /path/to/repo
|
|
85
90
|
|
package/analyzer.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
const simpleGit = require('simple-git');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
class GitAnalyzer {
|
|
6
|
+
constructor(repoPath) {
|
|
7
|
+
this.repoPath = repoPath;
|
|
8
|
+
this.git = simpleGit(repoPath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze() {
|
|
12
|
+
const [
|
|
13
|
+
branches,
|
|
14
|
+
tags,
|
|
15
|
+
remotes,
|
|
16
|
+
status,
|
|
17
|
+
log,
|
|
18
|
+
contributors,
|
|
19
|
+
repoSize,
|
|
20
|
+
garbageInfo
|
|
21
|
+
] = await Promise.all([
|
|
22
|
+
this.getBranches(),
|
|
23
|
+
this.getTags(),
|
|
24
|
+
this.getRemotes(),
|
|
25
|
+
this.getStatus(),
|
|
26
|
+
this.getLog(),
|
|
27
|
+
this.getContributors(),
|
|
28
|
+
this.getRepoSize(),
|
|
29
|
+
this.analyzeGarbage()
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const stats = this.calculateStats(log);
|
|
33
|
+
const health = this.assessRepoHealth(log, garbageInfo);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
branches,
|
|
37
|
+
tags,
|
|
38
|
+
remotes,
|
|
39
|
+
status,
|
|
40
|
+
commitCount: log.total,
|
|
41
|
+
recentCommits: log.all.slice(0, 50),
|
|
42
|
+
contributors,
|
|
43
|
+
stats,
|
|
44
|
+
repoSize,
|
|
45
|
+
garbageInfo,
|
|
46
|
+
health,
|
|
47
|
+
path: this.repoPath
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getBranches() {
|
|
52
|
+
const summary = await this.git.branch(['-a', '-v', '--sort=-committerdate']);
|
|
53
|
+
const current = summary.current;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
current,
|
|
57
|
+
all: Object.entries(summary.branches).map(([name, info]) => ({
|
|
58
|
+
name,
|
|
59
|
+
current: name === current,
|
|
60
|
+
commit: info.commit,
|
|
61
|
+
label: info.label,
|
|
62
|
+
remote: name.startsWith('remotes/')
|
|
63
|
+
}))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getTags() {
|
|
68
|
+
const tags = await this.git.tags();
|
|
69
|
+
return tags.all;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getRemotes() {
|
|
73
|
+
const remotes = await this.git.getRemotes(true);
|
|
74
|
+
return remotes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getStatus() {
|
|
78
|
+
const status = await this.git.status();
|
|
79
|
+
return {
|
|
80
|
+
modified: status.modified,
|
|
81
|
+
created: status.created,
|
|
82
|
+
deleted: status.deleted,
|
|
83
|
+
renamed: status.renamed,
|
|
84
|
+
staged: status.staged,
|
|
85
|
+
conflicted: status.conflicted,
|
|
86
|
+
ahead: status.ahead,
|
|
87
|
+
behind: status.behind,
|
|
88
|
+
tracking: status.tracking,
|
|
89
|
+
isClean: status.isClean()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getLog() {
|
|
94
|
+
const log = await this.git.log({
|
|
95
|
+
'--all': null,
|
|
96
|
+
'--max-count': 1000,
|
|
97
|
+
'--no-merges': null
|
|
98
|
+
});
|
|
99
|
+
return log;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getContributors() {
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.git.raw([
|
|
105
|
+
'shortlog',
|
|
106
|
+
'-sn',
|
|
107
|
+
'--all',
|
|
108
|
+
'--no-merges'
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
.trim()
|
|
113
|
+
.split('\n')
|
|
114
|
+
.map(line => {
|
|
115
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
116
|
+
if (match) {
|
|
117
|
+
return {
|
|
118
|
+
commits: parseInt(match[1]),
|
|
119
|
+
name: match[2]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
})
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
calculateStats(log) {
|
|
131
|
+
const commits = log.all;
|
|
132
|
+
const now = new Date();
|
|
133
|
+
const dayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
|
134
|
+
const weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
|
135
|
+
const monthAgo = new Date(now - 30 * 24 * 60 * 60 * 1000);
|
|
136
|
+
|
|
137
|
+
const commitsLastDay = commits.filter(c => new Date(c.date) > dayAgo).length;
|
|
138
|
+
const commitsLastWeek = commits.filter(c => new Date(c.date) > weekAgo).length;
|
|
139
|
+
const commitsLastMonth = commits.filter(c => new Date(c.date) > monthAgo).length;
|
|
140
|
+
|
|
141
|
+
// Calculate commits per day of week
|
|
142
|
+
const dayOfWeek = [0, 0, 0, 0, 0, 0, 0];
|
|
143
|
+
const hourOfDay = new Array(24).fill(0);
|
|
144
|
+
|
|
145
|
+
commits.forEach(commit => {
|
|
146
|
+
const date = new Date(commit.date);
|
|
147
|
+
dayOfWeek[date.getDay()]++;
|
|
148
|
+
hourOfDay[date.getHours()]++;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
total: log.total,
|
|
153
|
+
commitsLastDay,
|
|
154
|
+
commitsLastWeek,
|
|
155
|
+
commitsLastMonth,
|
|
156
|
+
dayOfWeek,
|
|
157
|
+
hourOfDay,
|
|
158
|
+
avgCommitsPerDay: (commitsLastMonth / 30).toFixed(2)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getRepoSize() {
|
|
163
|
+
try {
|
|
164
|
+
const gitDir = path.join(this.repoPath, '.git');
|
|
165
|
+
const size = await this.getDirectorySize(gitDir);
|
|
166
|
+
return {
|
|
167
|
+
bytes: size,
|
|
168
|
+
mb: (size / 1024 / 1024).toFixed(2),
|
|
169
|
+
gb: (size / 1024 / 1024 / 1024).toFixed(3)
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { bytes: 0, mb: '0', gb: '0' };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getDirectorySize(dirPath) {
|
|
177
|
+
let totalSize = 0;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
181
|
+
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
const itemPath = path.join(dirPath, item.name);
|
|
184
|
+
|
|
185
|
+
if (item.isDirectory()) {
|
|
186
|
+
totalSize += await this.getDirectorySize(itemPath);
|
|
187
|
+
} else {
|
|
188
|
+
const stats = await fs.stat(itemPath);
|
|
189
|
+
totalSize += stats.size;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Ignore errors for inaccessible files
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return totalSize;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async analyzeGarbage() {
|
|
200
|
+
try {
|
|
201
|
+
// Get count-objects info
|
|
202
|
+
const countObjects = await this.git.raw(['count-objects', '-v']);
|
|
203
|
+
const lines = countObjects.split('\n');
|
|
204
|
+
const info = {};
|
|
205
|
+
|
|
206
|
+
lines.forEach(line => {
|
|
207
|
+
const [key, value] = line.split(':').map(s => s.trim());
|
|
208
|
+
if (key && value) {
|
|
209
|
+
info[key] = value;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const looseObjects = parseInt(info['count'] || 0);
|
|
214
|
+
const looseSize = parseFloat(info['size'] || 0);
|
|
215
|
+
const packCount = parseInt(info['packs'] || 0);
|
|
216
|
+
const packSize = parseFloat(info['size-pack'] || 0);
|
|
217
|
+
|
|
218
|
+
// Recommend GC if there are many loose objects
|
|
219
|
+
const shouldGC = looseObjects > 1000 || looseSize > 10;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
looseObjects,
|
|
223
|
+
looseSize,
|
|
224
|
+
packCount,
|
|
225
|
+
packSize,
|
|
226
|
+
shouldGC,
|
|
227
|
+
recommendation: shouldGC
|
|
228
|
+
? 'Run `git gc` to optimize repository performance'
|
|
229
|
+
: 'Repository is well optimized'
|
|
230
|
+
};
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return {
|
|
233
|
+
looseObjects: 0,
|
|
234
|
+
looseSize: 0,
|
|
235
|
+
packCount: 0,
|
|
236
|
+
packSize: 0,
|
|
237
|
+
shouldGC: false,
|
|
238
|
+
recommendation: 'Unable to analyze'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
assessRepoHealth(log, garbageInfo) {
|
|
244
|
+
const scores = [];
|
|
245
|
+
const issues = [];
|
|
246
|
+
const suggestions = [];
|
|
247
|
+
|
|
248
|
+
// Check commit frequency
|
|
249
|
+
const commits = log.all;
|
|
250
|
+
const now = new Date();
|
|
251
|
+
const weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
|
252
|
+
const recentCommits = commits.filter(c => new Date(c.date) > weekAgo).length;
|
|
253
|
+
|
|
254
|
+
if (recentCommits > 0) {
|
|
255
|
+
scores.push(100);
|
|
256
|
+
} else {
|
|
257
|
+
scores.push(50);
|
|
258
|
+
issues.push('No commits in the last week');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check garbage collection
|
|
262
|
+
if (garbageInfo.shouldGC) {
|
|
263
|
+
scores.push(60);
|
|
264
|
+
issues.push('Repository needs garbage collection');
|
|
265
|
+
suggestions.push('Run: git gc --aggressive');
|
|
266
|
+
} else {
|
|
267
|
+
scores.push(100);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check for large pack files
|
|
271
|
+
if (garbageInfo.packSize > 100) {
|
|
272
|
+
issues.push('Large pack files detected');
|
|
273
|
+
suggestions.push('Consider using git-lfs for large files');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
277
|
+
|
|
278
|
+
let status = 'excellent';
|
|
279
|
+
if (avgScore < 90) status = 'good';
|
|
280
|
+
if (avgScore < 70) status = 'needs attention';
|
|
281
|
+
if (avgScore < 50) status = 'poor';
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
score: Math.round(avgScore),
|
|
285
|
+
status,
|
|
286
|
+
issues,
|
|
287
|
+
suggestions
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async getFileTree() {
|
|
292
|
+
try {
|
|
293
|
+
const result = await this.git.raw(['ls-tree', '-r', '--name-only', 'HEAD']);
|
|
294
|
+
const files = result.trim().split('\n').filter(Boolean);
|
|
295
|
+
|
|
296
|
+
// Build tree structure
|
|
297
|
+
const tree = {};
|
|
298
|
+
|
|
299
|
+
files.forEach(filepath => {
|
|
300
|
+
const parts = filepath.split('/');
|
|
301
|
+
let current = tree;
|
|
302
|
+
|
|
303
|
+
parts.forEach((part, index) => {
|
|
304
|
+
if (index === parts.length - 1) {
|
|
305
|
+
// It's a file
|
|
306
|
+
if (!current._files) current._files = [];
|
|
307
|
+
current._files.push(part);
|
|
308
|
+
} else {
|
|
309
|
+
// It's a directory
|
|
310
|
+
if (!current[part]) current[part] = {};
|
|
311
|
+
current = current[part];
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return tree;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async getCommitGraph(limit = 100) {
|
|
323
|
+
try {
|
|
324
|
+
const result = await this.git.raw([
|
|
325
|
+
'log',
|
|
326
|
+
'--all',
|
|
327
|
+
'--graph',
|
|
328
|
+
'--pretty=format:%H|%P|%s|%an|%ar',
|
|
329
|
+
`--max-count=${limit}`
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
const lines = result.split('\n');
|
|
333
|
+
const commits = [];
|
|
334
|
+
|
|
335
|
+
lines.forEach(line => {
|
|
336
|
+
// Match graph characters: *, |, \, /, space
|
|
337
|
+
const graphMatch = line.match(/^([*|\\\/ ]+)/);
|
|
338
|
+
const graph = graphMatch ? graphMatch[1] : '';
|
|
339
|
+
const data = line.substring(graph.length);
|
|
340
|
+
|
|
341
|
+
if (data && data.includes('|')) {
|
|
342
|
+
const parts = data.split('|');
|
|
343
|
+
if (parts.length >= 5) {
|
|
344
|
+
const [hash, parents, subject, author, date] = parts;
|
|
345
|
+
commits.push({
|
|
346
|
+
graph,
|
|
347
|
+
hash: hash.trim(),
|
|
348
|
+
parents: parents ? parents.trim().split(' ') : [],
|
|
349
|
+
subject: subject.trim(),
|
|
350
|
+
author: author.trim(),
|
|
351
|
+
date: date.trim()
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return commits;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('Error getting commit graph:', error);
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function analyzeRepo(repoPath) {
|
|
366
|
+
const analyzer = new GitAnalyzer(repoPath);
|
|
367
|
+
return await analyzer.analyze();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = { GitAnalyzer, analyzeRepo };
|
package/cli.js
CHANGED
|
@@ -2,23 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
5
7
|
const chalk = require('chalk');
|
|
6
8
|
const boxen = require('boxen');
|
|
9
|
+
const ora = require('ora');
|
|
7
10
|
const { startServer } = require('./server');
|
|
8
11
|
const { analyzeRepo } = require('./analyzer');
|
|
12
|
+
const simpleGit = require('simple-git');
|
|
9
13
|
|
|
10
14
|
program
|
|
11
15
|
.name('gitpulse')
|
|
12
|
-
.description('The ultimate Git UI
|
|
16
|
+
.description('The ultimate Git UI' )
|
|
13
17
|
.version('1.0.0');
|
|
14
18
|
|
|
19
|
+
async function cloneRepository(url, targetDir) {
|
|
20
|
+
const spinner = ora('Cloning repository...').start();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const git = simpleGit();
|
|
24
|
+
await git.clone(url, targetDir, ['--depth', '1000']); // Limit depth for performance
|
|
25
|
+
spinner.succeed(chalk.green('Repository cloned successfully'));
|
|
26
|
+
return targetDir;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
spinner.fail(chalk.red('Failed to clone repository'));
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isGitUrl(input) {
|
|
34
|
+
// Check if input looks like a Git URL
|
|
35
|
+
return input.startsWith('http://') ||
|
|
36
|
+
input.startsWith('https://') ||
|
|
37
|
+
input.startsWith('git@') ||
|
|
38
|
+
input.startsWith('ssh://') ||
|
|
39
|
+
input.includes('github.com') ||
|
|
40
|
+
input.includes('gitlab.com') ||
|
|
41
|
+
input.includes('bitbucket.org');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveGitPath(input) {
|
|
45
|
+
if (isGitUrl(input)) {
|
|
46
|
+
// It's a URL, clone it to a temp directory
|
|
47
|
+
const tempDir = path.join(os.tmpdir(), 'gitpulse-' + Date.now());
|
|
48
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
console.log(chalk.yellow('🌐 Remote repository detected'));
|
|
51
|
+
console.log(chalk.dim(` Cloning to: ${tempDir}`));
|
|
52
|
+
|
|
53
|
+
await cloneRepository(input, tempDir);
|
|
54
|
+
return tempDir;
|
|
55
|
+
} else {
|
|
56
|
+
// It's a local path
|
|
57
|
+
return path.resolve(input);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
15
61
|
program
|
|
16
|
-
.option('-g, --git-folder <path>', 'Path to git repository', process.cwd())
|
|
62
|
+
.option('-g, --git-folder <path>', 'Path to git repository or Git URL', process.cwd())
|
|
17
63
|
.option('-p, --port <number>', 'Port for web server', '3000')
|
|
18
64
|
.option('--no-browser', 'Don\'t open browser automatically')
|
|
19
65
|
.action(async (options) => {
|
|
20
|
-
const gitPath = path.resolve(options.gitFolder);
|
|
21
|
-
|
|
22
66
|
console.log(boxen(
|
|
23
67
|
chalk.bold.cyan('🚀 GitPulse') + '\n\n' +
|
|
24
68
|
chalk.green('The Ultimate Git Repository Analyzer') + '\n' +
|
|
@@ -31,11 +75,13 @@ program
|
|
|
31
75
|
}
|
|
32
76
|
));
|
|
33
77
|
|
|
34
|
-
console.log(chalk.yellow('📁 Repository:'), chalk.white(gitPath));
|
|
35
|
-
console.log(chalk.yellow('🌐 Port:'), chalk.white(options.port));
|
|
36
|
-
console.log();
|
|
37
|
-
|
|
38
78
|
try {
|
|
79
|
+
const gitPath = await resolveGitPath(options.gitFolder);
|
|
80
|
+
|
|
81
|
+
console.log(chalk.yellow('📁 Repository:'), chalk.white(gitPath));
|
|
82
|
+
console.log(chalk.yellow('🌐 Port:'), chalk.white(options.port));
|
|
83
|
+
console.log();
|
|
84
|
+
|
|
39
85
|
// Quick analysis
|
|
40
86
|
const analysis = await analyzeRepo(gitPath);
|
|
41
87
|
|
|
@@ -54,4 +100,4 @@ program
|
|
|
54
100
|
}
|
|
55
101
|
});
|
|
56
102
|
|
|
57
|
-
program.parse();
|
|
103
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mc-gitpulse",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "The ultimate Git UI - CLI + Web interface with advanced repository insights",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
],
|
|
20
20
|
"author": "Mohan Chinnappan",
|
|
21
21
|
"files": [
|
|
22
|
-
"server.js", "
|
|
22
|
+
"server.js", "analyzer.js", "cli.js",
|
|
23
|
+
"public/*"
|
|
23
24
|
],
|
|
24
|
-
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"boxen": "^5.1.2",
|
package/public/index.html
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>GitPulse
|
|
6
|
+
<title>GitPulse - Ultimate Git UI</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<link rel="icon" type="image/x-icon"
|
|
9
9
|
href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
12
12
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
13
13
|
<style>
|
|
@@ -200,7 +200,6 @@
|
|
|
200
200
|
|
|
201
201
|
.commit-node {
|
|
202
202
|
font-family: monospace;
|
|
203
|
-
white-space: pre;
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
/* Modal/Popup Styles */
|
|
@@ -429,7 +428,7 @@
|
|
|
429
428
|
<div class="container mx-auto px-6 py-4">
|
|
430
429
|
<div class="flex items-center justify-between">
|
|
431
430
|
<div class="flex items-center space-x-4">
|
|
432
|
-
<h1 class="text-2xl font-bold text-blue-400">⚡ GitPulse
|
|
431
|
+
<h1 class="text-2xl font-bold text-blue-400">⚡ GitPulse</h1>
|
|
433
432
|
<span class="text-sm text-gray-400 hidden sm:inline">The Ultimate Git Repository Analyzer</span>
|
|
434
433
|
</div>
|
|
435
434
|
<div class="flex items-center space-x-4">
|
|
@@ -1351,22 +1350,77 @@
|
|
|
1351
1350
|
return;
|
|
1352
1351
|
}
|
|
1353
1352
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1353
|
+
// Clear existing content
|
|
1354
|
+
graphElement.innerHTML = '';
|
|
1355
|
+
|
|
1356
|
+
// Build DOM elements directly instead of using innerHTML with strings
|
|
1357
|
+
graph.forEach(commit => {
|
|
1358
|
+
const row = document.createElement('div');
|
|
1359
|
+
row.className = 'commit-node hover:bg-gray-800 py-1 px-2 rounded flex items-start';
|
|
1359
1360
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1361
|
+
// Graph column
|
|
1362
|
+
const graphCol = document.createElement('div');
|
|
1363
|
+
graphCol.className = 'text-gray-400 font-mono inline-block';
|
|
1364
|
+
|
|
1365
|
+
// Process each character in the graph
|
|
1366
|
+
const graphChars = (commit.graph || '').split('');
|
|
1367
|
+
graphChars.forEach(char => {
|
|
1368
|
+
const span = document.createElement('span');
|
|
1369
|
+
switch(char) {
|
|
1370
|
+
case '*':
|
|
1371
|
+
span.className = 'text-yellow-400';
|
|
1372
|
+
span.textContent = '●';
|
|
1373
|
+
break;
|
|
1374
|
+
case '|':
|
|
1375
|
+
span.className = 'text-blue-400';
|
|
1376
|
+
span.textContent = '│';
|
|
1377
|
+
break;
|
|
1378
|
+
case '/':
|
|
1379
|
+
span.className = 'text-green-400';
|
|
1380
|
+
span.textContent = '╱';
|
|
1381
|
+
break;
|
|
1382
|
+
case '\\':
|
|
1383
|
+
span.className = 'text-purple-400';
|
|
1384
|
+
span.textContent = '╲';
|
|
1385
|
+
break;
|
|
1386
|
+
case ' ':
|
|
1387
|
+
span.innerHTML = ' ';
|
|
1388
|
+
break;
|
|
1389
|
+
default:
|
|
1390
|
+
span.textContent = char;
|
|
1391
|
+
}
|
|
1392
|
+
graphCol.appendChild(span);
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// Hash column
|
|
1396
|
+
const hashCol = document.createElement('span');
|
|
1397
|
+
hashCol.className = 'text-yellow-500 mx-2 font-mono flex-shrink-0';
|
|
1398
|
+
hashCol.textContent = commit.hash.substring(0, 7);
|
|
1399
|
+
|
|
1400
|
+
// Subject column
|
|
1401
|
+
const subjectCol = document.createElement('span');
|
|
1402
|
+
subjectCol.className = 'text-gray-300 flex-1';
|
|
1403
|
+
subjectCol.textContent = commit.subject;
|
|
1404
|
+
|
|
1405
|
+
// Author column
|
|
1406
|
+
const authorCol = document.createElement('span');
|
|
1407
|
+
authorCol.className = 'text-blue-400 ml-2 flex-shrink-0';
|
|
1408
|
+
authorCol.textContent = `(${commit.author})`;
|
|
1409
|
+
|
|
1410
|
+
// Date column
|
|
1411
|
+
const dateCol = document.createElement('span');
|
|
1412
|
+
dateCol.className = 'text-gray-500 ml-2 whitespace-nowrap flex-shrink-0';
|
|
1413
|
+
dateCol.textContent = commit.date;
|
|
1414
|
+
|
|
1415
|
+
// Assemble row
|
|
1416
|
+
row.appendChild(graphCol);
|
|
1417
|
+
row.appendChild(hashCol);
|
|
1418
|
+
row.appendChild(subjectCol);
|
|
1419
|
+
row.appendChild(authorCol);
|
|
1420
|
+
row.appendChild(dateCol);
|
|
1421
|
+
|
|
1422
|
+
graphElement.appendChild(row);
|
|
1423
|
+
});
|
|
1370
1424
|
|
|
1371
1425
|
console.log(`Git graph loaded: ${graph.length} commits`);
|
|
1372
1426
|
|