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 CHANGED
@@ -50,7 +50,7 @@
50
50
 
51
51
  ## Screenshot
52
52
 
53
- ![Git Diff on postgres repo](https://raw.githubusercontent.com/mchinnappan100/npmjs-images/main/gitpluse/gitpulse-demo-2.png)
53
+ ![Git Diff on postgres repo](https://raw.githubusercontent.com/mchinnappan100/npmjs-images/main/gitpluse/gitpulse-demo-3.png)
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 - Better than SourceTree, approved by Linus')
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.2",
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", "public/*"
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 - Ultimate Git UI</title>
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 </h1>
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
- graphElement.innerHTML = graph.map(commit => {
1355
- const graphPart = commit.graph.replace(/\*/g, '<span class="text-yellow-400">●</span>')
1356
- .replace(/\|/g, '<span class="text-blue-400">│</span>')
1357
- .replace(/\//g, '<span class="text-green-400">╱</span>')
1358
- .replace(/\\/g, '<span class="text-purple-400">╲</span>');
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
- return `
1361
- <div class="commit-node hover:bg-gray-800 py-1 px-2 rounded">
1362
- <span class="text-gray-400">${graphPart}</span>
1363
- <span class="text-yellow-500 mx-2">${commit.hash.substring(0, 7)}</span>
1364
- <span class="text-gray-300">${escapeHtml(commit.subject)}</span>
1365
- <span class="text-blue-400 ml-2">(${escapeHtml(commit.author)})</span>
1366
- <span class="text-gray-500 ml-2">${escapeHtml(commit.date)}</span>
1367
- </div>
1368
- `;
1369
- }).join('');
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 = '&nbsp;';
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