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 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,8 @@
1
+ interface ChangelogOptions {
2
+ since?: string;
3
+ count?: number;
4
+ markdown?: boolean;
5
+ }
6
+ export declare function changelog(options?: ChangelogOptions): Promise<void>;
7
+ export {};
8
+ //# sourceMappingURL=changelog.d.ts.map
@@ -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,6 @@
1
+ interface StatsOptions {
2
+ limit?: number;
3
+ }
4
+ export declare function stats(options?: StatsOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=stats.d.ts.map
@@ -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,2 @@
1
+ export declare function summary(): Promise<void>;
2
+ //# sourceMappingURL=summary.d.ts.map
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }
@@ -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
+ }