Skip to content

Commit eac9e1f

Browse files
authored
feat: restore committers commands (#409)
1 parent f88dc9c commit eac9e1f

5 files changed

Lines changed: 877 additions & 15 deletions

File tree

README.md

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ $ npm install -g @herodevs/cli@beta
7272
$ hd COMMAND
7373
running command...
7474
$ hd (--version)
75-
@herodevs/cli/2.0.0-beta.10 darwin-arm64 node-v22.18.0
75+
@herodevs/cli/2.0.0-beta.12 darwin-arm64 node-v24.10.0
7676
$ hd --help [COMMAND]
7777
USAGE
7878
$ hd COMMAND
@@ -82,6 +82,7 @@ USAGE
8282
## Commands
8383
<!-- commands -->
8484
* [`hd help [COMMAND]`](#hd-help-command)
85+
* [`hd report committers`](#hd-report-committers)
8586
* [`hd scan eol`](#hd-scan-eol)
8687
* [`hd update [CHANNEL]`](#hd-update-channel)
8788
* **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.
@@ -95,7 +96,7 @@ USAGE
9596
$ hd help [COMMAND...] [-n]
9697
9798
ARGUMENTS
98-
COMMAND... Command to show help for.
99+
[COMMAND...] Command to show help for.
99100
100101
FLAGS
101102
-n, --nested-commands Include all nested commands in the output.
@@ -104,26 +105,59 @@ DESCRIPTION
104105
Display help for hd.
105106
```
106107

107-
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.33/src/commands/help.ts)_
108+
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.34/src/commands/help.ts)_
109+
110+
## `hd report committers`
111+
112+
Generate report of committers to a git repository
113+
114+
```
115+
USAGE
116+
$ hd report committers [--json] [-m <value>] [-c] [-s]
117+
118+
FLAGS
119+
-c, --csv Output in CSV format
120+
-m, --months=<value> [default: 12] The number of months of git history to review
121+
-s, --save Save the committers report as herodevs.committers.<output>
122+
123+
GLOBAL FLAGS
124+
--json Format output as json.
125+
126+
DESCRIPTION
127+
Generate report of committers to a git repository
128+
129+
EXAMPLES
130+
$ hd report committers
131+
132+
$ hd report committers --csv -s
133+
134+
$ hd report committers --json
135+
136+
$ hd report committers --csv
137+
```
138+
139+
_See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.12/src/commands/report/committers.ts)_
108140

109141
## `hd scan eol`
110142

111143
Scan a given SBOM for EOL data
112144

113145
```
114146
USAGE
115-
$ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>] [--saveTrimmedSbom] [--hideReportUrl] [--version]
147+
$ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>]
148+
[--saveTrimmedSbom] [--hideReportUrl] [--version]
116149
117150
FLAGS
118-
-d, --dir=<value> [default: <current directory>] The directory to scan in order to scan for EOL
119-
-f, --file=<value> The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)
120-
-s, --save Save the generated report as herodevs.report.json in the scanned directory
121-
-o, --output=<value> Save the generated report to a custom path (requires --save, defaults to herodevs.report.json when not provided)
122-
--hideReportUrl Hide the generated web report URL for this scan
123-
--saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory
124-
--sbomOutput=<value> Save the generated SBOM to a custom path (requires --saveSbom, defaults to herodevs.sbom.json when not provided)
125-
--saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory
126-
--version Show CLI version.
151+
-d, --dir=<value> [default: <current directory>] The directory to scan in order to create a cyclonedx SBOM
152+
-f, --file=<value> The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)
153+
-o, --output=<value> Save the generated report to a custom path (defaults to herodevs.report.json when not
154+
provided)
155+
-s, --save Save the generated report as herodevs.report.json in the scanned directory
156+
--hideReportUrl Hide the generated web report URL for this scan
157+
--saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory
158+
--saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory
159+
--sbomOutput=<value> Save the generated SBOM to a custom path (defaults to herodevs.sbom.json when not provided)
160+
--version Show CLI version.
127161
128162
GLOBAL FLAGS
129163
--json Format output as json.
@@ -157,7 +191,7 @@ EXAMPLES
157191
$ hd scan eol --json
158192
```
159193

160-
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.10/src/commands/scan/eol.ts)_
194+
_See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.12/src/commands/scan/eol.ts)_
161195

162196
## `hd update [CHANNEL]`
163197

@@ -197,7 +231,7 @@ EXAMPLES
197231
$ hd update --available
198232
```
199233

200-
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.8/src/commands/update.ts)_
234+
_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.13/src/commands/update.ts)_
201235
<!-- commandsstop -->
202236

203237
## CI/CD Usage

src/commands/report/committers.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { spawnSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { Command, Flags } from '@oclif/core';
5+
import { filenamePrefix } from '../../config/constants.ts';
6+
import {
7+
type CommitEntry,
8+
calculateOverallStats,
9+
formatAsCsv,
10+
formatAsText,
11+
groupCommitsByMonth,
12+
parseGitLogOutput,
13+
type ReportData,
14+
} from '../../service/committers.svc.ts';
15+
import { getErrorMessage, isErrnoException } from '../../service/error.svc.ts';
16+
17+
export default class Committers extends Command {
18+
static override description = 'Generate report of committers to a git repository';
19+
static enableJsonFlag = true;
20+
static override examples = [
21+
'<%= config.bin %> <%= command.id %>',
22+
'<%= config.bin %> <%= command.id %> --csv -s',
23+
'<%= config.bin %> <%= command.id %> --json',
24+
'<%= config.bin %> <%= command.id %> --csv',
25+
];
26+
27+
static override flags = {
28+
months: Flags.integer({
29+
char: 'm',
30+
description: 'The number of months of git history to review',
31+
default: 12,
32+
}),
33+
csv: Flags.boolean({
34+
char: 'c',
35+
description: 'Output in CSV format',
36+
default: false,
37+
}),
38+
save: Flags.boolean({
39+
char: 's',
40+
description: `Save the committers report as ${filenamePrefix}.committers.<output>`,
41+
default: false,
42+
}),
43+
};
44+
45+
public async run(): Promise<ReportData | string> {
46+
const { flags } = await this.parse(Committers);
47+
const { months, csv, save } = flags;
48+
const isJson = this.jsonEnabled();
49+
50+
const sinceDate = `${months} months ago`;
51+
this.log('Starting committers report with flags: %O', flags);
52+
53+
try {
54+
// Generate structured report data
55+
const entries = this.fetchGitCommitData(sinceDate);
56+
this.log('Fetched %d commit entries', entries.length);
57+
const reportData = this.generateReportData(entries);
58+
59+
// Handle different output scenarios
60+
if (isJson) {
61+
// JSON mode
62+
if (save) {
63+
try {
64+
fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.json`), JSON.stringify(reportData, null, 2));
65+
this.log('Report written to json');
66+
} catch (error) {
67+
this.error(`Failed to save JSON report: ${getErrorMessage(error)}`);
68+
}
69+
}
70+
return reportData;
71+
}
72+
73+
const textOutput = formatAsText(reportData);
74+
75+
if (csv) {
76+
// CSV mode
77+
const csvOutput = formatAsCsv(reportData);
78+
if (save) {
79+
try {
80+
fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.csv`), csvOutput);
81+
this.log('Report written to csv');
82+
} catch (error) {
83+
this.error(`Failed to save CSV report: ${getErrorMessage(error)}`);
84+
}
85+
} else {
86+
this.log(textOutput);
87+
}
88+
return csvOutput;
89+
}
90+
91+
if (save) {
92+
try {
93+
fs.writeFileSync(path.resolve(`${filenamePrefix}.committers.txt`), textOutput);
94+
this.log('Report written to txt');
95+
} catch (error) {
96+
this.error(`Failed to save txt report: ${getErrorMessage(error)}`);
97+
}
98+
} else {
99+
this.log(textOutput);
100+
}
101+
return textOutput;
102+
} catch (error) {
103+
this.error(`Failed to generate report: ${getErrorMessage(error)}`);
104+
}
105+
}
106+
107+
/**
108+
* Generates structured report data
109+
* @param entries - parsed git log output for commits
110+
*/
111+
private generateReportData(entries: CommitEntry[]): ReportData {
112+
if (entries.length === 0) {
113+
return { monthly: {}, overall: { total: 0 } };
114+
}
115+
116+
const monthlyData = groupCommitsByMonth(entries);
117+
const overallStats = calculateOverallStats(entries);
118+
const grandTotal = entries.length;
119+
120+
// Format into a structured report data object
121+
const report: ReportData = {
122+
monthly: {},
123+
overall: { ...overallStats, total: grandTotal },
124+
};
125+
126+
// Add monthly totals
127+
for (const [month, authors] of Object.entries(monthlyData)) {
128+
const monthTotal = Object.values(authors).reduce((sum, count) => sum + count, 0);
129+
report.monthly[month] = { ...authors, total: monthTotal };
130+
}
131+
132+
return report;
133+
}
134+
135+
/**
136+
* Fetches git commit data with month and author information
137+
* @param sinceDate - Date range for git log
138+
*/
139+
private fetchGitCommitData(sinceDate: string): CommitEntry[] {
140+
const logProcess = spawnSync(
141+
'git',
142+
[
143+
'log',
144+
'--all', // Include committers on all branches in the repo
145+
'--format="%ad|%an"', // Format: date|author
146+
'--date=format:%Y-%m', // Format date as YYYY-MM
147+
`--since="${sinceDate}"`,
148+
],
149+
{ encoding: 'utf-8' },
150+
);
151+
152+
if (logProcess.error) {
153+
if (isErrnoException(logProcess.error)) {
154+
if (logProcess.error.code === 'ENOENT') {
155+
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
156+
}
157+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
158+
}
159+
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
160+
}
161+
162+
if (logProcess.status !== 0) {
163+
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
164+
}
165+
166+
if (!logProcess.stdout) {
167+
return [];
168+
}
169+
170+
return parseGitLogOutput(logProcess.stdout);
171+
}
172+
}

0 commit comments

Comments
 (0)