git-glance 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/commands/changelog.d.ts +8 -0
- package/dist/commands/changelog.d.ts.map +1 -0
- package/dist/commands/changelog.js +95 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +86 -0
- package/dist/commands/summary.d.ts +2 -0
- package/dist/commands/summary.d.ts.map +1 -0
- package/dist/commands/summary.js +85 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/utils/format.d.ts +13 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +68 -0
- package/dist/utils/git.d.ts +26 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +96 -0
- package/package.json +41 -0
- package/src/commands/changelog.ts +83 -0
- package/src/commands/stats.ts +64 -0
- package/src/commands/summary.ts +58 -0
- package/src/index.ts +44 -0
- package/src/utils/format.ts +63 -0
- package/src/utils/git.ts +120 -0
- package/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deepankar Rawat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# git-glance
|
|
2
|
+
|
|
3
|
+
> Beautiful git repository summaries, statistics, and changelogs
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# From npm (coming soon)
|
|
11
|
+
npm install -g git-glance
|
|
12
|
+
|
|
13
|
+
# From GitHub
|
|
14
|
+
npm install -g github:dprrwt/git-glance
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Run `git-glance` in any git repository:
|
|
20
|
+
|
|
21
|
+
### Summary (default)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git-glance
|
|
25
|
+
# or
|
|
26
|
+
git-glance summary
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Shows:
|
|
30
|
+
- Repository info (branch, status, remote)
|
|
31
|
+
- Commit statistics
|
|
32
|
+
- Top contributors with progress bars
|
|
33
|
+
- Recent commits
|
|
34
|
+
|
|
35
|
+
### Statistics
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git-glance stats
|
|
39
|
+
git-glance stats --limit 100 # Analyze last 100 commits only
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Shows detailed commit statistics:
|
|
43
|
+
- Total commits
|
|
44
|
+
- All contributors with commit counts
|
|
45
|
+
- Project age
|
|
46
|
+
- Commits per day
|
|
47
|
+
|
|
48
|
+
### Changelog
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git-glance changelog
|
|
52
|
+
git-glance log # Alias
|
|
53
|
+
git-glance log --since "1 week ago" # Since specific date
|
|
54
|
+
git-glance log --count 50 # Last 50 commits
|
|
55
|
+
git-glance log --markdown # Output as markdown
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Generates a formatted changelog grouped by date.
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
### Repository Summary
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
═══ 📊 my-project ═══
|
|
66
|
+
|
|
67
|
+
Branch: main
|
|
68
|
+
Status: ✓ Clean
|
|
69
|
+
Remote: git@github.com:user/my-project.git
|
|
70
|
+
|
|
71
|
+
═══ 📈 Statistics ═══
|
|
72
|
+
|
|
73
|
+
Total Commits: 142
|
|
74
|
+
Contributors: 3
|
|
75
|
+
First Commit: Jan 15, 2024
|
|
76
|
+
Last Commit: Feb 3, 2024
|
|
77
|
+
Commits/Day: 7.5
|
|
78
|
+
|
|
79
|
+
═══ 👥 Top Contributors ═══
|
|
80
|
+
|
|
81
|
+
███████████████░░░░░ John Doe (95 commits, 67%)
|
|
82
|
+
████████░░░░░░░░░░░░ Jane Smith (35 commits, 25%)
|
|
83
|
+
██░░░░░░░░░░░░░░░░░░ Bob Wilson (12 commits, 8%)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Markdown Changelog
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git-glance log --since "last week" --markdown > CHANGELOG.md
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT © [Deepankar Rawat](https://github.com/dprrwt)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changelog.d.ts","sourceRoot":"","sources":["../../src/commands/changelog.ts"],"names":[],"mappings":"AAGA,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB7E"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.changelog = changelog;
|
|
37
|
+
const git_1 = require("../utils/git");
|
|
38
|
+
const fmt = __importStar(require("../utils/format"));
|
|
39
|
+
async function changelog(options = {}) {
|
|
40
|
+
if (!(await (0, git_1.isGitRepo)())) {
|
|
41
|
+
console.log(fmt.error('Error: Not a git repository'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const repo = await (0, git_1.getRepoInfo)();
|
|
45
|
+
const commits = options.since
|
|
46
|
+
? await (0, git_1.getCommitsSince)(options.since)
|
|
47
|
+
: await (0, git_1.getRecentCommits)(options.count || 20);
|
|
48
|
+
if (commits.length === 0) {
|
|
49
|
+
console.log(fmt.warning('No commits found for the specified period.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (options.markdown) {
|
|
53
|
+
outputMarkdown(repo.name, commits, options.since);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
outputPretty(commits, options.since);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function outputPretty(commits, since) {
|
|
60
|
+
const title = since ? `Changes since ${since}` : 'Recent Changes';
|
|
61
|
+
console.log(fmt.header(`📋 ${title}`));
|
|
62
|
+
// Group by date
|
|
63
|
+
const byDate = new Map();
|
|
64
|
+
for (const commit of commits) {
|
|
65
|
+
const existing = byDate.get(commit.date) || [];
|
|
66
|
+
existing.push(commit);
|
|
67
|
+
byDate.set(commit.date, existing);
|
|
68
|
+
}
|
|
69
|
+
for (const [date, dateCommits] of byDate) {
|
|
70
|
+
console.log(`\n ${fmt.highlight(date)}`);
|
|
71
|
+
for (const commit of dateCommits) {
|
|
72
|
+
console.log(` ${fmt.dim('•')} ${fmt.value(commit.message)}`);
|
|
73
|
+
console.log(` ${fmt.dim(`${commit.hash} by ${commit.author}`)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
78
|
+
function outputMarkdown(repoName, commits, since) {
|
|
79
|
+
const title = since ? `Changes since ${since}` : 'Changelog';
|
|
80
|
+
console.log(`# ${repoName} - ${title}\n`);
|
|
81
|
+
// Group by date
|
|
82
|
+
const byDate = new Map();
|
|
83
|
+
for (const commit of commits) {
|
|
84
|
+
const existing = byDate.get(commit.date) || [];
|
|
85
|
+
existing.push(commit);
|
|
86
|
+
byDate.set(commit.date, existing);
|
|
87
|
+
}
|
|
88
|
+
for (const [date, dateCommits] of byDate) {
|
|
89
|
+
console.log(`## ${date}\n`);
|
|
90
|
+
for (const commit of dateCommits) {
|
|
91
|
+
console.log(`- ${commit.message} (\`${commit.hash}\` by ${commit.author})`);
|
|
92
|
+
}
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/commands/stats.ts"],"names":[],"mappings":"AAGA,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDrE"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.stats = stats;
|
|
37
|
+
const git_1 = require("../utils/git");
|
|
38
|
+
const fmt = __importStar(require("../utils/format"));
|
|
39
|
+
async function stats(options = {}) {
|
|
40
|
+
if (!(await (0, git_1.isGitRepo)())) {
|
|
41
|
+
console.log(fmt.error('Error: Not a git repository'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const limit = options.limit;
|
|
45
|
+
const commitStats = await (0, git_1.getCommitStats)(limit);
|
|
46
|
+
console.log(fmt.header('📈 Commit Statistics'));
|
|
47
|
+
if (limit) {
|
|
48
|
+
console.log(fmt.dim(` (Analyzing last ${limit} commits)\n`));
|
|
49
|
+
}
|
|
50
|
+
// Overview
|
|
51
|
+
console.log(fmt.table([
|
|
52
|
+
['Total Commits', String(commitStats.totalCommits)],
|
|
53
|
+
['Contributors', String(commitStats.authors.size)],
|
|
54
|
+
['First Commit', fmt.formatDate(commitStats.firstCommit)],
|
|
55
|
+
['Last Commit', fmt.formatDate(commitStats.lastCommit)],
|
|
56
|
+
['Commits/Day', fmt.formatNumber(commitStats.commitsPerDay)],
|
|
57
|
+
]));
|
|
58
|
+
// Calculate project age
|
|
59
|
+
if (commitStats.firstCommit && commitStats.lastCommit) {
|
|
60
|
+
const ageMs = commitStats.lastCommit.getTime() - commitStats.firstCommit.getTime();
|
|
61
|
+
const days = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
62
|
+
const months = Math.floor(days / 30);
|
|
63
|
+
const years = Math.floor(days / 365);
|
|
64
|
+
let ageStr;
|
|
65
|
+
if (years > 0) {
|
|
66
|
+
ageStr = `${years} year${years > 1 ? 's' : ''}, ${months % 12} month${months % 12 !== 1 ? 's' : ''}`;
|
|
67
|
+
}
|
|
68
|
+
else if (months > 0) {
|
|
69
|
+
ageStr = `${months} month${months > 1 ? 's' : ''}, ${days % 30} day${days % 30 !== 1 ? 's' : ''}`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
ageStr = `${days} day${days !== 1 ? 's' : ''}`;
|
|
73
|
+
}
|
|
74
|
+
console.log(`\n ${fmt.label('Project Age')} ${fmt.value(ageStr)}`);
|
|
75
|
+
}
|
|
76
|
+
// All Contributors (sorted)
|
|
77
|
+
console.log(fmt.header('👥 All Contributors'));
|
|
78
|
+
const sorted = [...commitStats.authors.entries()].sort((a, b) => b[1] - a[1]);
|
|
79
|
+
const maxCount = sorted[0]?.[1] || 1;
|
|
80
|
+
for (const [author, count] of sorted) {
|
|
81
|
+
const percentage = (count / commitStats.totalCommits) * 100;
|
|
82
|
+
const bar = fmt.progressBar(count, maxCount, 20);
|
|
83
|
+
console.log(` ${bar} ${fmt.value(author.padEnd(25))} ${fmt.highlight(String(count).padStart(5))} ${fmt.dim(`(${percentage.toFixed(1)}%)`)}`);
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/commands/summary.ts"],"names":[],"mappings":"AAGA,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAsD7C"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.summary = summary;
|
|
37
|
+
const git_1 = require("../utils/git");
|
|
38
|
+
const fmt = __importStar(require("../utils/format"));
|
|
39
|
+
async function summary() {
|
|
40
|
+
if (!(await (0, git_1.isGitRepo)())) {
|
|
41
|
+
console.log(fmt.error('Error: Not a git repository'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const [repo, stats, recent] = await Promise.all([
|
|
45
|
+
(0, git_1.getRepoInfo)(),
|
|
46
|
+
(0, git_1.getCommitStats)(),
|
|
47
|
+
(0, git_1.getRecentCommits)(5),
|
|
48
|
+
]);
|
|
49
|
+
// Header
|
|
50
|
+
console.log(fmt.header(`📊 ${repo.name}`));
|
|
51
|
+
// Repository Info
|
|
52
|
+
console.log(fmt.table([
|
|
53
|
+
['Branch', repo.branch],
|
|
54
|
+
['Status', repo.isClean ? fmt.success('✓ Clean') : fmt.warning(`${repo.uncommittedChanges} changes`)],
|
|
55
|
+
['Remote', repo.remoteUrl || fmt.dim('none')],
|
|
56
|
+
]));
|
|
57
|
+
// Statistics
|
|
58
|
+
console.log(fmt.header('📈 Statistics'));
|
|
59
|
+
console.log(fmt.table([
|
|
60
|
+
['Total Commits', String(stats.totalCommits)],
|
|
61
|
+
['Contributors', String(stats.authors.size)],
|
|
62
|
+
['First Commit', fmt.formatDate(stats.firstCommit)],
|
|
63
|
+
['Last Commit', fmt.formatDate(stats.lastCommit)],
|
|
64
|
+
['Commits/Day', fmt.formatNumber(stats.commitsPerDay)],
|
|
65
|
+
]));
|
|
66
|
+
// Top Contributors
|
|
67
|
+
if (stats.authors.size > 0) {
|
|
68
|
+
console.log(fmt.header('👥 Top Contributors'));
|
|
69
|
+
const sorted = [...stats.authors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
70
|
+
for (const [author, count] of sorted) {
|
|
71
|
+
const percentage = (count / stats.totalCommits) * 100;
|
|
72
|
+
const bar = fmt.progressBar(count, stats.totalCommits, 15);
|
|
73
|
+
console.log(` ${bar} ${fmt.value(author)} ${fmt.dim(`(${count} commits, ${percentage.toFixed(0)}%)`)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Recent Commits
|
|
77
|
+
if (recent.length > 0) {
|
|
78
|
+
console.log(fmt.header('📝 Recent Commits'));
|
|
79
|
+
for (const commit of recent) {
|
|
80
|
+
console.log(` ${fmt.dim(commit.hash)} ${fmt.value(commit.message)}`);
|
|
81
|
+
console.log(` ${fmt.dim(`${commit.author} • ${commit.date}`)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const summary_1 = require("./commands/summary");
|
|
6
|
+
const stats_1 = require("./commands/stats");
|
|
7
|
+
const changelog_1 = require("./commands/changelog");
|
|
8
|
+
const program = new commander_1.Command();
|
|
9
|
+
program
|
|
10
|
+
.name('git-glance')
|
|
11
|
+
.description('Beautiful git repository summaries, statistics, and changelogs')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
// Default command: summary
|
|
14
|
+
program
|
|
15
|
+
.command('summary', { isDefault: true })
|
|
16
|
+
.description('Show repository summary (default)')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
await (0, summary_1.summary)();
|
|
19
|
+
});
|
|
20
|
+
// Stats command
|
|
21
|
+
program
|
|
22
|
+
.command('stats')
|
|
23
|
+
.description('Show detailed commit statistics')
|
|
24
|
+
.option('-l, --limit <number>', 'Limit to last N commits', parseInt)
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
await (0, stats_1.stats)(options);
|
|
27
|
+
});
|
|
28
|
+
// Changelog command
|
|
29
|
+
program
|
|
30
|
+
.command('changelog')
|
|
31
|
+
.alias('log')
|
|
32
|
+
.description('Generate changelog from commits')
|
|
33
|
+
.option('-s, --since <date>', 'Show commits since date (e.g., "1 week ago", "2024-01-01")')
|
|
34
|
+
.option('-n, --count <number>', 'Number of commits to show', parseInt)
|
|
35
|
+
.option('-m, --markdown', 'Output in markdown format')
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
await (0, changelog_1.changelog)(options);
|
|
38
|
+
});
|
|
39
|
+
program.parse();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function header(text: string): string;
|
|
2
|
+
export declare function label(text: string): string;
|
|
3
|
+
export declare function value(text: string | number): string;
|
|
4
|
+
export declare function highlight(text: string | number): string;
|
|
5
|
+
export declare function success(text: string): string;
|
|
6
|
+
export declare function warning(text: string): string;
|
|
7
|
+
export declare function error(text: string): string;
|
|
8
|
+
export declare function dim(text: string): string;
|
|
9
|
+
export declare function formatDate(date: Date | null): string;
|
|
10
|
+
export declare function formatNumber(num: number, decimals?: number): string;
|
|
11
|
+
export declare function progressBar(current: number, total: number, width?: number): string;
|
|
12
|
+
export declare function table(rows: [string, string][]): string;
|
|
13
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAEA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAOpD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAGtE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,MAAM,CAMtF;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,MAAM,CAMtD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.header = header;
|
|
7
|
+
exports.label = label;
|
|
8
|
+
exports.value = value;
|
|
9
|
+
exports.highlight = highlight;
|
|
10
|
+
exports.success = success;
|
|
11
|
+
exports.warning = warning;
|
|
12
|
+
exports.error = error;
|
|
13
|
+
exports.dim = dim;
|
|
14
|
+
exports.formatDate = formatDate;
|
|
15
|
+
exports.formatNumber = formatNumber;
|
|
16
|
+
exports.progressBar = progressBar;
|
|
17
|
+
exports.table = table;
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
function header(text) {
|
|
20
|
+
return chalk_1.default.bold.cyan(`\n═══ ${text} ═══\n`);
|
|
21
|
+
}
|
|
22
|
+
function label(text) {
|
|
23
|
+
return chalk_1.default.gray(text + ':');
|
|
24
|
+
}
|
|
25
|
+
function value(text) {
|
|
26
|
+
return chalk_1.default.white(String(text));
|
|
27
|
+
}
|
|
28
|
+
function highlight(text) {
|
|
29
|
+
return chalk_1.default.yellow(String(text));
|
|
30
|
+
}
|
|
31
|
+
function success(text) {
|
|
32
|
+
return chalk_1.default.green(text);
|
|
33
|
+
}
|
|
34
|
+
function warning(text) {
|
|
35
|
+
return chalk_1.default.yellow(text);
|
|
36
|
+
}
|
|
37
|
+
function error(text) {
|
|
38
|
+
return chalk_1.default.red(text);
|
|
39
|
+
}
|
|
40
|
+
function dim(text) {
|
|
41
|
+
return chalk_1.default.gray(text);
|
|
42
|
+
}
|
|
43
|
+
function formatDate(date) {
|
|
44
|
+
if (!date)
|
|
45
|
+
return 'N/A';
|
|
46
|
+
return date.toLocaleDateString('en-US', {
|
|
47
|
+
year: 'numeric',
|
|
48
|
+
month: 'short',
|
|
49
|
+
day: 'numeric',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function formatNumber(num, decimals = 1) {
|
|
53
|
+
if (Number.isInteger(num))
|
|
54
|
+
return String(num);
|
|
55
|
+
return num.toFixed(decimals);
|
|
56
|
+
}
|
|
57
|
+
function progressBar(current, total, width = 20) {
|
|
58
|
+
const percentage = Math.min(1, current / total);
|
|
59
|
+
const filled = Math.round(width * percentage);
|
|
60
|
+
const empty = width - filled;
|
|
61
|
+
return chalk_1.default.green('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
|
|
62
|
+
}
|
|
63
|
+
function table(rows) {
|
|
64
|
+
const maxLabel = Math.max(...rows.map(r => r[0].length));
|
|
65
|
+
return rows
|
|
66
|
+
.map(([l, v]) => ` ${label(l.padEnd(maxLabel))} ${value(v)}`)
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface RepoInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
branch: string;
|
|
4
|
+
remoteUrl: string | null;
|
|
5
|
+
isClean: boolean;
|
|
6
|
+
uncommittedChanges: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CommitStats {
|
|
9
|
+
totalCommits: number;
|
|
10
|
+
authors: Map<string, number>;
|
|
11
|
+
firstCommit: Date | null;
|
|
12
|
+
lastCommit: Date | null;
|
|
13
|
+
commitsPerDay: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CommitInfo {
|
|
16
|
+
hash: string;
|
|
17
|
+
date: string;
|
|
18
|
+
message: string;
|
|
19
|
+
author: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function isGitRepo(): Promise<boolean>;
|
|
22
|
+
export declare function getRepoInfo(): Promise<RepoInfo>;
|
|
23
|
+
export declare function getCommitStats(limit?: number): Promise<CommitStats>;
|
|
24
|
+
export declare function getRecentCommits(count?: number): Promise<CommitInfo[]>;
|
|
25
|
+
export declare function getCommitsSince(since: string): Promise<CommitInfo[]>;
|
|
26
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAOlD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,QAAQ,CAAC,CA2BrD;AAED,wBAAsB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAgCzE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAShF;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAS1E"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isGitRepo = isGitRepo;
|
|
7
|
+
exports.getRepoInfo = getRepoInfo;
|
|
8
|
+
exports.getCommitStats = getCommitStats;
|
|
9
|
+
exports.getRecentCommits = getRecentCommits;
|
|
10
|
+
exports.getCommitsSince = getCommitsSince;
|
|
11
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
12
|
+
const git = (0, simple_git_1.default)();
|
|
13
|
+
async function isGitRepo() {
|
|
14
|
+
try {
|
|
15
|
+
await git.revparse(['--is-inside-work-tree']);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function getRepoInfo() {
|
|
23
|
+
const status = await git.status();
|
|
24
|
+
const branch = status.current || 'unknown';
|
|
25
|
+
let remoteUrl = null;
|
|
26
|
+
try {
|
|
27
|
+
const remotes = await git.getRemotes(true);
|
|
28
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
29
|
+
remoteUrl = origin?.refs.fetch || null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// No remotes
|
|
33
|
+
}
|
|
34
|
+
// Get repo name from directory or remote
|
|
35
|
+
let name = process.cwd().split(/[/\\]/).pop() || 'unknown';
|
|
36
|
+
if (remoteUrl) {
|
|
37
|
+
const match = remoteUrl.match(/\/([^/]+?)(\.git)?$/);
|
|
38
|
+
if (match)
|
|
39
|
+
name = match[1];
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
branch,
|
|
44
|
+
remoteUrl,
|
|
45
|
+
isClean: status.isClean(),
|
|
46
|
+
uncommittedChanges: status.files.length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function getCommitStats(limit) {
|
|
50
|
+
const log = await git.log(limit ? ['--max-count=' + limit] : []);
|
|
51
|
+
const authors = new Map();
|
|
52
|
+
let firstCommit = null;
|
|
53
|
+
let lastCommit = null;
|
|
54
|
+
for (const commit of log.all) {
|
|
55
|
+
// Count by author
|
|
56
|
+
const author = commit.author_name;
|
|
57
|
+
authors.set(author, (authors.get(author) || 0) + 1);
|
|
58
|
+
// Track dates
|
|
59
|
+
const date = new Date(commit.date);
|
|
60
|
+
if (!firstCommit || date < firstCommit)
|
|
61
|
+
firstCommit = date;
|
|
62
|
+
if (!lastCommit || date > lastCommit)
|
|
63
|
+
lastCommit = date;
|
|
64
|
+
}
|
|
65
|
+
// Calculate commits per day
|
|
66
|
+
let commitsPerDay = 0;
|
|
67
|
+
if (firstCommit && lastCommit) {
|
|
68
|
+
const days = Math.max(1, (lastCommit.getTime() - firstCommit.getTime()) / (1000 * 60 * 60 * 24));
|
|
69
|
+
commitsPerDay = log.total / days;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
totalCommits: log.total,
|
|
73
|
+
authors,
|
|
74
|
+
firstCommit,
|
|
75
|
+
lastCommit,
|
|
76
|
+
commitsPerDay,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function getRecentCommits(count = 10) {
|
|
80
|
+
const log = await git.log(['--max-count=' + count]);
|
|
81
|
+
return log.all.map(commit => ({
|
|
82
|
+
hash: commit.hash.substring(0, 7),
|
|
83
|
+
date: new Date(commit.date).toLocaleDateString(),
|
|
84
|
+
message: commit.message.split('\n')[0], // First line only
|
|
85
|
+
author: commit.author_name,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
async function getCommitsSince(since) {
|
|
89
|
+
const log = await git.log(['--since=' + since]);
|
|
90
|
+
return log.all.map(commit => ({
|
|
91
|
+
hash: commit.hash.substring(0, 7),
|
|
92
|
+
date: new Date(commit.date).toLocaleDateString(),
|
|
93
|
+
message: commit.message.split('\n')[0],
|
|
94
|
+
author: commit.author_name,
|
|
95
|
+
}));
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-glance",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Beautiful git repository summaries, statistics, and changelogs",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"git-glance": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"git",
|
|
16
|
+
"cli",
|
|
17
|
+
"summary",
|
|
18
|
+
"statistics",
|
|
19
|
+
"changelog",
|
|
20
|
+
"developer-tools"
|
|
21
|
+
],
|
|
22
|
+
"author": "Deepankar Rawat <dprrwt>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/dprrwt/git-glance"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "^4.1.2",
|
|
30
|
+
"commander": "^11.1.0",
|
|
31
|
+
"simple-git": "^3.22.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.10.0",
|
|
35
|
+
"typescript": "^5.3.0",
|
|
36
|
+
"ts-node": "^10.9.2"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getCommitsSince, getRecentCommits, isGitRepo, getRepoInfo } from '../utils/git';
|
|
2
|
+
import * as fmt from '../utils/format';
|
|
3
|
+
|
|
4
|
+
interface ChangelogOptions {
|
|
5
|
+
since?: string;
|
|
6
|
+
count?: number;
|
|
7
|
+
markdown?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function changelog(options: ChangelogOptions = {}): Promise<void> {
|
|
11
|
+
if (!(await isGitRepo())) {
|
|
12
|
+
console.log(fmt.error('Error: Not a git repository'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const repo = await getRepoInfo();
|
|
17
|
+
const commits = options.since
|
|
18
|
+
? await getCommitsSince(options.since)
|
|
19
|
+
: await getRecentCommits(options.count || 20);
|
|
20
|
+
|
|
21
|
+
if (commits.length === 0) {
|
|
22
|
+
console.log(fmt.warning('No commits found for the specified period.'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (options.markdown) {
|
|
27
|
+
outputMarkdown(repo.name, commits, options.since);
|
|
28
|
+
} else {
|
|
29
|
+
outputPretty(commits, options.since);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CommitInfo {
|
|
34
|
+
hash: string;
|
|
35
|
+
date: string;
|
|
36
|
+
message: string;
|
|
37
|
+
author: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function outputPretty(commits: CommitInfo[], since?: string): void {
|
|
41
|
+
const title = since ? `Changes since ${since}` : 'Recent Changes';
|
|
42
|
+
console.log(fmt.header(`📋 ${title}`));
|
|
43
|
+
|
|
44
|
+
// Group by date
|
|
45
|
+
const byDate = new Map<string, CommitInfo[]>();
|
|
46
|
+
for (const commit of commits) {
|
|
47
|
+
const existing = byDate.get(commit.date) || [];
|
|
48
|
+
existing.push(commit);
|
|
49
|
+
byDate.set(commit.date, existing);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [date, dateCommits] of byDate) {
|
|
53
|
+
console.log(`\n ${fmt.highlight(date)}`);
|
|
54
|
+
for (const commit of dateCommits) {
|
|
55
|
+
console.log(` ${fmt.dim('•')} ${fmt.value(commit.message)}`);
|
|
56
|
+
console.log(` ${fmt.dim(`${commit.hash} by ${commit.author}`)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function outputMarkdown(repoName: string, commits: CommitInfo[], since?: string): void {
|
|
64
|
+
const title = since ? `Changes since ${since}` : 'Changelog';
|
|
65
|
+
|
|
66
|
+
console.log(`# ${repoName} - ${title}\n`);
|
|
67
|
+
|
|
68
|
+
// Group by date
|
|
69
|
+
const byDate = new Map<string, CommitInfo[]>();
|
|
70
|
+
for (const commit of commits) {
|
|
71
|
+
const existing = byDate.get(commit.date) || [];
|
|
72
|
+
existing.push(commit);
|
|
73
|
+
byDate.set(commit.date, existing);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const [date, dateCommits] of byDate) {
|
|
77
|
+
console.log(`## ${date}\n`);
|
|
78
|
+
for (const commit of dateCommits) {
|
|
79
|
+
console.log(`- ${commit.message} (\`${commit.hash}\` by ${commit.author})`);
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getCommitStats, isGitRepo } from '../utils/git';
|
|
2
|
+
import * as fmt from '../utils/format';
|
|
3
|
+
|
|
4
|
+
interface StatsOptions {
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function stats(options: StatsOptions = {}): Promise<void> {
|
|
9
|
+
if (!(await isGitRepo())) {
|
|
10
|
+
console.log(fmt.error('Error: Not a git repository'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const limit = options.limit;
|
|
15
|
+
const commitStats = await getCommitStats(limit);
|
|
16
|
+
|
|
17
|
+
console.log(fmt.header('📈 Commit Statistics'));
|
|
18
|
+
|
|
19
|
+
if (limit) {
|
|
20
|
+
console.log(fmt.dim(` (Analyzing last ${limit} commits)\n`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Overview
|
|
24
|
+
console.log(fmt.table([
|
|
25
|
+
['Total Commits', String(commitStats.totalCommits)],
|
|
26
|
+
['Contributors', String(commitStats.authors.size)],
|
|
27
|
+
['First Commit', fmt.formatDate(commitStats.firstCommit)],
|
|
28
|
+
['Last Commit', fmt.formatDate(commitStats.lastCommit)],
|
|
29
|
+
['Commits/Day', fmt.formatNumber(commitStats.commitsPerDay)],
|
|
30
|
+
]));
|
|
31
|
+
|
|
32
|
+
// Calculate project age
|
|
33
|
+
if (commitStats.firstCommit && commitStats.lastCommit) {
|
|
34
|
+
const ageMs = commitStats.lastCommit.getTime() - commitStats.firstCommit.getTime();
|
|
35
|
+
const days = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
36
|
+
const months = Math.floor(days / 30);
|
|
37
|
+
const years = Math.floor(days / 365);
|
|
38
|
+
|
|
39
|
+
let ageStr: string;
|
|
40
|
+
if (years > 0) {
|
|
41
|
+
ageStr = `${years} year${years > 1 ? 's' : ''}, ${months % 12} month${months % 12 !== 1 ? 's' : ''}`;
|
|
42
|
+
} else if (months > 0) {
|
|
43
|
+
ageStr = `${months} month${months > 1 ? 's' : ''}, ${days % 30} day${days % 30 !== 1 ? 's' : ''}`;
|
|
44
|
+
} else {
|
|
45
|
+
ageStr = `${days} day${days !== 1 ? 's' : ''}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`\n ${fmt.label('Project Age')} ${fmt.value(ageStr)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// All Contributors (sorted)
|
|
52
|
+
console.log(fmt.header('👥 All Contributors'));
|
|
53
|
+
|
|
54
|
+
const sorted = [...commitStats.authors.entries()].sort((a, b) => b[1] - a[1]);
|
|
55
|
+
const maxCount = sorted[0]?.[1] || 1;
|
|
56
|
+
|
|
57
|
+
for (const [author, count] of sorted) {
|
|
58
|
+
const percentage = (count / commitStats.totalCommits) * 100;
|
|
59
|
+
const bar = fmt.progressBar(count, maxCount, 20);
|
|
60
|
+
console.log(` ${bar} ${fmt.value(author.padEnd(25))} ${fmt.highlight(String(count).padStart(5))} ${fmt.dim(`(${percentage.toFixed(1)}%)`)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getRepoInfo, getCommitStats, getRecentCommits, isGitRepo } from '../utils/git';
|
|
2
|
+
import * as fmt from '../utils/format';
|
|
3
|
+
|
|
4
|
+
export async function summary(): Promise<void> {
|
|
5
|
+
if (!(await isGitRepo())) {
|
|
6
|
+
console.log(fmt.error('Error: Not a git repository'));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const [repo, stats, recent] = await Promise.all([
|
|
11
|
+
getRepoInfo(),
|
|
12
|
+
getCommitStats(),
|
|
13
|
+
getRecentCommits(5),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// Header
|
|
17
|
+
console.log(fmt.header(`📊 ${repo.name}`));
|
|
18
|
+
|
|
19
|
+
// Repository Info
|
|
20
|
+
console.log(fmt.table([
|
|
21
|
+
['Branch', repo.branch],
|
|
22
|
+
['Status', repo.isClean ? fmt.success('✓ Clean') : fmt.warning(`${repo.uncommittedChanges} changes`)],
|
|
23
|
+
['Remote', repo.remoteUrl || fmt.dim('none')],
|
|
24
|
+
]));
|
|
25
|
+
|
|
26
|
+
// Statistics
|
|
27
|
+
console.log(fmt.header('📈 Statistics'));
|
|
28
|
+
console.log(fmt.table([
|
|
29
|
+
['Total Commits', String(stats.totalCommits)],
|
|
30
|
+
['Contributors', String(stats.authors.size)],
|
|
31
|
+
['First Commit', fmt.formatDate(stats.firstCommit)],
|
|
32
|
+
['Last Commit', fmt.formatDate(stats.lastCommit)],
|
|
33
|
+
['Commits/Day', fmt.formatNumber(stats.commitsPerDay)],
|
|
34
|
+
]));
|
|
35
|
+
|
|
36
|
+
// Top Contributors
|
|
37
|
+
if (stats.authors.size > 0) {
|
|
38
|
+
console.log(fmt.header('👥 Top Contributors'));
|
|
39
|
+
const sorted = [...stats.authors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
40
|
+
|
|
41
|
+
for (const [author, count] of sorted) {
|
|
42
|
+
const percentage = (count / stats.totalCommits) * 100;
|
|
43
|
+
const bar = fmt.progressBar(count, stats.totalCommits, 15);
|
|
44
|
+
console.log(` ${bar} ${fmt.value(author)} ${fmt.dim(`(${count} commits, ${percentage.toFixed(0)}%)`)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Recent Commits
|
|
49
|
+
if (recent.length > 0) {
|
|
50
|
+
console.log(fmt.header('📝 Recent Commits'));
|
|
51
|
+
for (const commit of recent) {
|
|
52
|
+
console.log(` ${fmt.dim(commit.hash)} ${fmt.value(commit.message)}`);
|
|
53
|
+
console.log(` ${fmt.dim(`${commit.author} • ${commit.date}`)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { summary } from './commands/summary';
|
|
5
|
+
import { stats } from './commands/stats';
|
|
6
|
+
import { changelog } from './commands/changelog';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('git-glance')
|
|
12
|
+
.description('Beautiful git repository summaries, statistics, and changelogs')
|
|
13
|
+
.version('1.0.0');
|
|
14
|
+
|
|
15
|
+
// Default command: summary
|
|
16
|
+
program
|
|
17
|
+
.command('summary', { isDefault: true })
|
|
18
|
+
.description('Show repository summary (default)')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
await summary();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Stats command
|
|
24
|
+
program
|
|
25
|
+
.command('stats')
|
|
26
|
+
.description('Show detailed commit statistics')
|
|
27
|
+
.option('-l, --limit <number>', 'Limit to last N commits', parseInt)
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
await stats(options);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Changelog command
|
|
33
|
+
program
|
|
34
|
+
.command('changelog')
|
|
35
|
+
.alias('log')
|
|
36
|
+
.description('Generate changelog from commits')
|
|
37
|
+
.option('-s, --since <date>', 'Show commits since date (e.g., "1 week ago", "2024-01-01")')
|
|
38
|
+
.option('-n, --count <number>', 'Number of commits to show', parseInt)
|
|
39
|
+
.option('-m, --markdown', 'Output in markdown format')
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
await changelog(options);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program.parse();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function header(text: string): string {
|
|
4
|
+
return chalk.bold.cyan(`\n═══ ${text} ═══\n`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function label(text: string): string {
|
|
8
|
+
return chalk.gray(text + ':');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function value(text: string | number): string {
|
|
12
|
+
return chalk.white(String(text));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function highlight(text: string | number): string {
|
|
16
|
+
return chalk.yellow(String(text));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function success(text: string): string {
|
|
20
|
+
return chalk.green(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function warning(text: string): string {
|
|
24
|
+
return chalk.yellow(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function error(text: string): string {
|
|
28
|
+
return chalk.red(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function dim(text: string): string {
|
|
32
|
+
return chalk.gray(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatDate(date: Date | null): string {
|
|
36
|
+
if (!date) return 'N/A';
|
|
37
|
+
return date.toLocaleDateString('en-US', {
|
|
38
|
+
year: 'numeric',
|
|
39
|
+
month: 'short',
|
|
40
|
+
day: 'numeric',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatNumber(num: number, decimals: number = 1): string {
|
|
45
|
+
if (Number.isInteger(num)) return String(num);
|
|
46
|
+
return num.toFixed(decimals);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function progressBar(current: number, total: number, width: number = 20): string {
|
|
50
|
+
const percentage = Math.min(1, current / total);
|
|
51
|
+
const filled = Math.round(width * percentage);
|
|
52
|
+
const empty = width - filled;
|
|
53
|
+
|
|
54
|
+
return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function table(rows: [string, string][]): string {
|
|
58
|
+
const maxLabel = Math.max(...rows.map(r => r[0].length));
|
|
59
|
+
|
|
60
|
+
return rows
|
|
61
|
+
.map(([l, v]) => ` ${label(l.padEnd(maxLabel))} ${value(v)}`)
|
|
62
|
+
.join('\n');
|
|
63
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import simpleGit, { SimpleGit, LogResult } from 'simple-git';
|
|
2
|
+
|
|
3
|
+
export interface RepoInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
branch: string;
|
|
6
|
+
remoteUrl: string | null;
|
|
7
|
+
isClean: boolean;
|
|
8
|
+
uncommittedChanges: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommitStats {
|
|
12
|
+
totalCommits: number;
|
|
13
|
+
authors: Map<string, number>;
|
|
14
|
+
firstCommit: Date | null;
|
|
15
|
+
lastCommit: Date | null;
|
|
16
|
+
commitsPerDay: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CommitInfo {
|
|
20
|
+
hash: string;
|
|
21
|
+
date: string;
|
|
22
|
+
message: string;
|
|
23
|
+
author: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const git: SimpleGit = simpleGit();
|
|
27
|
+
|
|
28
|
+
export async function isGitRepo(): Promise<boolean> {
|
|
29
|
+
try {
|
|
30
|
+
await git.revparse(['--is-inside-work-tree']);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getRepoInfo(): Promise<RepoInfo> {
|
|
38
|
+
const status = await git.status();
|
|
39
|
+
const branch = status.current || 'unknown';
|
|
40
|
+
|
|
41
|
+
let remoteUrl: string | null = null;
|
|
42
|
+
try {
|
|
43
|
+
const remotes = await git.getRemotes(true);
|
|
44
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
45
|
+
remoteUrl = origin?.refs.fetch || null;
|
|
46
|
+
} catch {
|
|
47
|
+
// No remotes
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get repo name from directory or remote
|
|
51
|
+
let name = process.cwd().split(/[/\\]/).pop() || 'unknown';
|
|
52
|
+
if (remoteUrl) {
|
|
53
|
+
const match = remoteUrl.match(/\/([^/]+?)(\.git)?$/);
|
|
54
|
+
if (match) name = match[1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
branch,
|
|
60
|
+
remoteUrl,
|
|
61
|
+
isClean: status.isClean(),
|
|
62
|
+
uncommittedChanges: status.files.length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getCommitStats(limit?: number): Promise<CommitStats> {
|
|
67
|
+
const log: LogResult = await git.log(limit ? ['--max-count=' + limit] : []);
|
|
68
|
+
|
|
69
|
+
const authors = new Map<string, number>();
|
|
70
|
+
let firstCommit: Date | null = null;
|
|
71
|
+
let lastCommit: Date | null = null;
|
|
72
|
+
|
|
73
|
+
for (const commit of log.all) {
|
|
74
|
+
// Count by author
|
|
75
|
+
const author = commit.author_name;
|
|
76
|
+
authors.set(author, (authors.get(author) || 0) + 1);
|
|
77
|
+
|
|
78
|
+
// Track dates
|
|
79
|
+
const date = new Date(commit.date);
|
|
80
|
+
if (!firstCommit || date < firstCommit) firstCommit = date;
|
|
81
|
+
if (!lastCommit || date > lastCommit) lastCommit = date;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Calculate commits per day
|
|
85
|
+
let commitsPerDay = 0;
|
|
86
|
+
if (firstCommit && lastCommit) {
|
|
87
|
+
const days = Math.max(1, (lastCommit.getTime() - firstCommit.getTime()) / (1000 * 60 * 60 * 24));
|
|
88
|
+
commitsPerDay = log.total / days;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
totalCommits: log.total,
|
|
93
|
+
authors,
|
|
94
|
+
firstCommit,
|
|
95
|
+
lastCommit,
|
|
96
|
+
commitsPerDay,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function getRecentCommits(count: number = 10): Promise<CommitInfo[]> {
|
|
101
|
+
const log = await git.log(['--max-count=' + count]);
|
|
102
|
+
|
|
103
|
+
return log.all.map(commit => ({
|
|
104
|
+
hash: commit.hash.substring(0, 7),
|
|
105
|
+
date: new Date(commit.date).toLocaleDateString(),
|
|
106
|
+
message: commit.message.split('\n')[0], // First line only
|
|
107
|
+
author: commit.author_name,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function getCommitsSince(since: string): Promise<CommitInfo[]> {
|
|
112
|
+
const log = await git.log(['--since=' + since]);
|
|
113
|
+
|
|
114
|
+
return log.all.map(commit => ({
|
|
115
|
+
hash: commit.hash.substring(0, 7),
|
|
116
|
+
date: new Date(commit.date).toLocaleDateString(),
|
|
117
|
+
message: commit.message.split('\n')[0],
|
|
118
|
+
author: commit.author_name,
|
|
119
|
+
}));
|
|
120
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|