openspec-stat 1.0.0 → 1.2.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/README.md +18 -0
- package/README.zh-CN.md +16 -0
- package/dist/cjs/cli.js +20 -37
- package/dist/cjs/formatters.js +151 -13
- package/dist/cjs/git-analyzer.js +10 -23
- package/dist/cjs/i18n/index.js +1 -1
- package/dist/cjs/i18n/locales/en.json +14 -8
- package/dist/cjs/i18n/locales/zh-CN.json +14 -8
- package/dist/cjs/stats-aggregator.js +35 -5
- package/dist/cjs/types.d.ts +11 -0
- package/dist/esm/cli.js +1 -1
- package/dist/esm/formatters.js +222 -37
- package/dist/esm/git-analyzer.js +5 -3
- package/dist/esm/i18n/index.js +1 -1
- package/dist/esm/i18n/locales/en.json +14 -8
- package/dist/esm/i18n/locales/zh-CN.json +14 -8
- package/dist/esm/stats-aggregator.js +74 -19
- package/dist/esm/types.d.ts +11 -0
- package/package.json +35 -3
package/README.md
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
[](https://npmjs.com/package/openspec-stat)
|
|
4
4
|
[](https://npmjs.com/package/openspec-stat)
|
|
5
5
|
|
|
6
|
+
English | [简体中文](./README.zh-CN.md)
|
|
7
|
+
|
|
6
8
|
A CLI tool for tracking team members' OpenSpec proposals and code changes in Git repositories.
|
|
7
9
|
|
|
8
10
|
## Features
|
|
9
11
|
|
|
10
12
|
- ✅ Track Git commits within specified time ranges
|
|
11
13
|
- ✅ Identify commits containing both OpenSpec proposals and code changes
|
|
14
|
+
- ✅ **Proposal-based statistics summary** - Aggregate statistics by proposal to avoid merge commit bias
|
|
12
15
|
- ✅ Group statistics by author (commits, proposals, code changes)
|
|
13
16
|
- ✅ Support multiple branches and wildcard filtering
|
|
14
17
|
- ✅ Author name mapping (handle multiple Git accounts for the same person)
|
|
@@ -161,6 +164,11 @@ Statistics include:
|
|
|
161
164
|
- **Deletions**: Lines of code deleted
|
|
162
165
|
- **Net Changes**: Additions - Deletions
|
|
163
166
|
|
|
167
|
+
The tool provides two perspectives:
|
|
168
|
+
|
|
169
|
+
1. **Proposal Summary**: Aggregates statistics by proposal, showing total code changes per proposal and all contributors. This avoids statistical bias from merge commits.
|
|
170
|
+
2. **Author Summary**: Groups statistics by contributor, showing individual author contributions.
|
|
171
|
+
|
|
164
172
|
## Output Formats
|
|
165
173
|
|
|
166
174
|
### Table Format (Default)
|
|
@@ -171,6 +179,16 @@ Time Range: 2024-01-01 00:00:00 ~ 2024-01-31 23:59:59
|
|
|
171
179
|
Branches: origin/master
|
|
172
180
|
Total Commits: 15
|
|
173
181
|
|
|
182
|
+
📋 Proposal Summary (by proposal)
|
|
183
|
+
┌──────────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
|
|
184
|
+
│ Proposal │ Commits │ Contributors │ Code Files │ Additions │ Deletions │ Net Changes │
|
|
185
|
+
├──────────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
|
|
186
|
+
│ feature-123 │ 5 │ John Doe, Jane S.│ 30 │ +890 │ -234 │ +656 │
|
|
187
|
+
│ feature-456 │ 3 │ John Doe │ 15 │ +344 │ -100 │ +244 │
|
|
188
|
+
└──────────────┴─────────┴──────────────────┴────────────┴───────────┴───────────┴─────────────┘
|
|
189
|
+
📊 Total: 2 proposals | 8 commits | 45 files | +1234/-334 lines (net: +900)
|
|
190
|
+
|
|
191
|
+
👥 Author Summary (by contributor)
|
|
174
192
|
┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
|
|
175
193
|
│ Author │ Commits │ OpenSpec Proposals│ Code Files │ Additions │ Deletions │ Net Changes │
|
|
176
194
|
├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
|
package/README.zh-CN.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
- ✅ 追踪指定时间范围内的 Git 提交
|
|
13
13
|
- ✅ 识别同时包含 OpenSpec 提案和代码变更的提交
|
|
14
|
+
- ✅ **提案维度统计汇总** - 按提案聚合统计,避免 merge commit 导致的统计偏差
|
|
14
15
|
- ✅ 按作者分组统计(提交数、提案数、代码变更)
|
|
15
16
|
- ✅ 支持多分支和通配符过滤
|
|
16
17
|
- ✅ 作者名称映射(处理同一人的多个 Git 账号)
|
|
@@ -163,6 +164,11 @@ openspec-stat --lang zh-CN --verbose
|
|
|
163
164
|
- **删除行数**:删除的代码行数
|
|
164
165
|
- **净变更**:新增行数 - 删除行数
|
|
165
166
|
|
|
167
|
+
工具提供两个统计视角:
|
|
168
|
+
|
|
169
|
+
1. **提案汇总**:按提案聚合统计,显示每个提案的总代码变更量和所有贡献者,避免 merge commit 导致的统计偏差
|
|
170
|
+
2. **作者汇总**:按贡献者分组统计,显示各个作者的个人贡献情况
|
|
171
|
+
|
|
166
172
|
## 输出格式
|
|
167
173
|
|
|
168
174
|
### 表格格式(默认)
|
|
@@ -173,6 +179,16 @@ openspec-stat --lang zh-CN --verbose
|
|
|
173
179
|
分支:origin/master
|
|
174
180
|
总提交数:15
|
|
175
181
|
|
|
182
|
+
📋 提案汇总(按提案统计)
|
|
183
|
+
┌──────────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
|
|
184
|
+
│ 提案 │ 提交数 │ 贡献者 │ 代码文件 │ 新增行数 │ 删除行数 │ 净变更 │
|
|
185
|
+
├──────────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
|
|
186
|
+
│ feature-123 │ 5 │ 张三, 李四 │ 30 │ +890 │ -234 │ +656 │
|
|
187
|
+
│ feature-456 │ 3 │ 张三 │ 15 │ +344 │ -100 │ +244 │
|
|
188
|
+
└──────────────┴─────────┴──────────────────┴────────────┴───────────┴───────────┴─────────────┘
|
|
189
|
+
📊 总计:2 个提案 | 8 次提交 | 45 个文件 | +1234/-334 行(净变更:+900)
|
|
190
|
+
|
|
191
|
+
👥 作者汇总(按贡献者统计)
|
|
176
192
|
┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
|
|
177
193
|
│ 作者 │ 提交数 │ 提案数 │ 代码文件 │ 新增行数 │ 删除行数 │ 净变更 │
|
|
178
194
|
├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
|
package/dist/cjs/cli.js
CHANGED
|
@@ -41,19 +41,10 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
41
41
|
let since;
|
|
42
42
|
let until;
|
|
43
43
|
if (options.since || options.until) {
|
|
44
|
-
since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(
|
|
45
|
-
|
|
46
|
-
config.defaultUntilHours
|
|
47
|
-
).since;
|
|
48
|
-
until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(
|
|
49
|
-
config.defaultSinceHours,
|
|
50
|
-
config.defaultUntilHours
|
|
51
|
-
).until;
|
|
44
|
+
since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).since;
|
|
45
|
+
until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).until;
|
|
52
46
|
} else {
|
|
53
|
-
const defaultRange = (0, import_time_utils.getDefaultTimeRange)(
|
|
54
|
-
config.defaultSinceHours,
|
|
55
|
-
config.defaultUntilHours
|
|
56
|
-
);
|
|
47
|
+
const defaultRange = (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours);
|
|
57
48
|
since = defaultRange.since;
|
|
58
49
|
until = defaultRange.until;
|
|
59
50
|
}
|
|
@@ -73,14 +64,16 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
73
64
|
})
|
|
74
65
|
)
|
|
75
66
|
);
|
|
76
|
-
console.log(
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
console.log(
|
|
68
|
+
import_chalk.default.blue(
|
|
69
|
+
(0, import_i18n.t)("info.branches", {
|
|
70
|
+
branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
);
|
|
79
74
|
const analyzer = new import_git_analyzer.GitAnalyzer(options.repo, config);
|
|
80
75
|
console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
|
|
81
|
-
const activeAuthors = await analyzer.getActiveAuthors(
|
|
82
|
-
config.activeUserWeeks || 2
|
|
83
|
-
);
|
|
76
|
+
const activeAuthors = await analyzer.getActiveAuthors(config.activeUserWeeks || 2);
|
|
84
77
|
if (options.verbose) {
|
|
85
78
|
console.log(
|
|
86
79
|
import_chalk.default.gray(
|
|
@@ -103,10 +96,12 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
103
96
|
const commit = commits[i];
|
|
104
97
|
if (options.verbose && i % 10 === 0) {
|
|
105
98
|
console.log(
|
|
106
|
-
import_chalk.default.gray(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
import_chalk.default.gray(
|
|
100
|
+
(0, import_i18n.t)("info.analysisProgress", {
|
|
101
|
+
current: String(i + 1),
|
|
102
|
+
total: String(commits.length)
|
|
103
|
+
})
|
|
104
|
+
)
|
|
110
105
|
);
|
|
111
106
|
}
|
|
112
107
|
const analysis = await analyzer.analyzeCommit(commit);
|
|
@@ -115,24 +110,12 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
|
|
|
115
110
|
}
|
|
116
111
|
}
|
|
117
112
|
if (analyses.length === 0) {
|
|
118
|
-
console.log(
|
|
119
|
-
import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits"))
|
|
120
|
-
);
|
|
113
|
+
console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits")));
|
|
121
114
|
return;
|
|
122
115
|
}
|
|
123
|
-
console.log(
|
|
124
|
-
import_chalk.default.blue(
|
|
125
|
-
(0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })
|
|
126
|
-
)
|
|
127
|
-
);
|
|
116
|
+
console.log(import_chalk.default.blue((0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })));
|
|
128
117
|
const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
|
|
129
|
-
const result = aggregator.aggregate(
|
|
130
|
-
analyses,
|
|
131
|
-
since,
|
|
132
|
-
until,
|
|
133
|
-
branches,
|
|
134
|
-
options.author
|
|
135
|
-
);
|
|
118
|
+
const result = aggregator.aggregate(analyses, since, until, branches, options.author);
|
|
136
119
|
const formatter = new import_formatters.OutputFormatter();
|
|
137
120
|
if (options.json) {
|
|
138
121
|
console.log(formatter.formatJSON(result));
|
package/dist/cjs/formatters.js
CHANGED
|
@@ -47,9 +47,60 @@ var OutputFormatter = class {
|
|
|
47
47
|
);
|
|
48
48
|
output += import_chalk.default.gray((0, import_i18n.t)("output.branches", { branches: result.branches.join(", ") }));
|
|
49
49
|
output += import_chalk.default.gray((0, import_i18n.t)("output.totalCommits", { count: String(result.totalCommits) }));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if (result.proposals.size > 0) {
|
|
51
|
+
output += import_chalk.default.bold.magenta(`
|
|
52
|
+
${(0, import_i18n.t)("output.proposalSummary")}
|
|
53
|
+
`);
|
|
54
|
+
const proposalTable = new import_cli_table3.default({
|
|
55
|
+
head: [
|
|
56
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.proposal")),
|
|
57
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.commits")),
|
|
58
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.contributors")),
|
|
59
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.codeFiles")),
|
|
60
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.additions")),
|
|
61
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.deletions")),
|
|
62
|
+
import_chalk.default.magenta((0, import_i18n.t)("table.netChanges"))
|
|
63
|
+
],
|
|
64
|
+
style: {
|
|
65
|
+
head: [],
|
|
66
|
+
border: []
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
|
|
70
|
+
for (const proposalStats of sortedProposals) {
|
|
71
|
+
const contributors = Array.from(proposalStats.contributors).join(", ");
|
|
72
|
+
proposalTable.push([
|
|
73
|
+
proposalStats.proposal,
|
|
74
|
+
proposalStats.commits.toString(),
|
|
75
|
+
contributors,
|
|
76
|
+
proposalStats.codeFilesChanged.toString(),
|
|
77
|
+
import_chalk.default.green(`+${proposalStats.additions}`),
|
|
78
|
+
import_chalk.default.red(`-${proposalStats.deletions}`),
|
|
79
|
+
proposalStats.netChanges >= 0 ? import_chalk.default.green(`+${proposalStats.netChanges}`) : import_chalk.default.red(`${proposalStats.netChanges}`)
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
output += proposalTable.toString() + "\n";
|
|
83
|
+
const totalProposals = result.proposals.size;
|
|
84
|
+
const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
|
|
85
|
+
const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
|
|
86
|
+
const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
|
|
87
|
+
const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
|
|
88
|
+
const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
|
|
89
|
+
output += import_chalk.default.gray(
|
|
90
|
+
(0, import_i18n.t)("output.proposalTotal", {
|
|
91
|
+
count: totalProposals.toString(),
|
|
92
|
+
commits: totalProposalCommits.toString(),
|
|
93
|
+
files: totalProposalFiles.toString(),
|
|
94
|
+
additions: totalProposalAdditions.toString(),
|
|
95
|
+
deletions: totalProposalDeletions.toString(),
|
|
96
|
+
netChanges: totalProposalNetChanges.toString()
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
output += import_chalk.default.bold.cyan(`
|
|
101
|
+
${(0, import_i18n.t)("output.authorSummary")}
|
|
102
|
+
`);
|
|
103
|
+
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
53
104
|
for (const stats of sortedAuthors) {
|
|
54
105
|
output += import_chalk.default.bold.cyan(`
|
|
55
106
|
${stats.author}
|
|
@@ -70,9 +121,7 @@ ${stats.author}
|
|
|
70
121
|
border: []
|
|
71
122
|
}
|
|
72
123
|
});
|
|
73
|
-
const sortedBranches = Array.from(stats.branchStats.values()).sort(
|
|
74
|
-
(a, b) => b.commits - a.commits
|
|
75
|
-
);
|
|
124
|
+
const sortedBranches = Array.from(stats.branchStats.values()).sort((a, b) => b.commits - a.commits);
|
|
76
125
|
for (const branchStat of sortedBranches) {
|
|
77
126
|
branchTable.push([
|
|
78
127
|
branchStat.branch,
|
|
@@ -135,6 +184,26 @@ ${stats.author}
|
|
|
135
184
|
},
|
|
136
185
|
branches: result.branches,
|
|
137
186
|
totalCommits: result.totalCommits,
|
|
187
|
+
proposals: {
|
|
188
|
+
items: Array.from(result.proposals.values()).map((stats) => ({
|
|
189
|
+
proposal: stats.proposal,
|
|
190
|
+
commits: stats.commits,
|
|
191
|
+
contributors: Array.from(stats.contributors),
|
|
192
|
+
contributorCount: stats.contributors.size,
|
|
193
|
+
codeFilesChanged: stats.codeFilesChanged,
|
|
194
|
+
additions: stats.additions,
|
|
195
|
+
deletions: stats.deletions,
|
|
196
|
+
netChanges: stats.netChanges
|
|
197
|
+
})),
|
|
198
|
+
summary: {
|
|
199
|
+
totalProposals: result.proposals.size,
|
|
200
|
+
totalCommits: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0),
|
|
201
|
+
totalCodeFiles: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0),
|
|
202
|
+
totalAdditions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0),
|
|
203
|
+
totalDeletions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0),
|
|
204
|
+
totalNetChanges: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0)
|
|
205
|
+
}
|
|
206
|
+
},
|
|
138
207
|
authors: Array.from(result.authors.values()).map((stats) => {
|
|
139
208
|
var _a;
|
|
140
209
|
return {
|
|
@@ -155,12 +224,46 @@ ${stats.author}
|
|
|
155
224
|
formatCSV(result) {
|
|
156
225
|
var _a;
|
|
157
226
|
const rows = [];
|
|
227
|
+
rows.push(`
|
|
228
|
+
# ${(0, import_i18n.t)("output.proposalSummary")}`);
|
|
158
229
|
rows.push(
|
|
159
|
-
`${(0, import_i18n.t)("table.
|
|
230
|
+
`${(0, import_i18n.t)("table.proposal")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.contributors")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")}`
|
|
160
231
|
);
|
|
161
|
-
const
|
|
162
|
-
|
|
232
|
+
const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
|
|
233
|
+
for (const stats of sortedProposals) {
|
|
234
|
+
const contributors = Array.from(stats.contributors).join(";");
|
|
235
|
+
rows.push(
|
|
236
|
+
[
|
|
237
|
+
stats.proposal,
|
|
238
|
+
stats.commits,
|
|
239
|
+
`"${contributors}"`,
|
|
240
|
+
stats.codeFilesChanged,
|
|
241
|
+
stats.additions,
|
|
242
|
+
stats.deletions,
|
|
243
|
+
stats.netChanges
|
|
244
|
+
].join(",")
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const totalProposals = result.proposals.size;
|
|
248
|
+
const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
|
|
249
|
+
const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
|
|
250
|
+
const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
|
|
251
|
+
const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
|
|
252
|
+
const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
|
|
253
|
+
rows.push("");
|
|
254
|
+
rows.push(`# ${(0, import_i18n.t)("output.proposalTotalLabel")}`);
|
|
255
|
+
rows.push(`${(0, import_i18n.t)("table.proposals")},${totalProposals}`);
|
|
256
|
+
rows.push(`${(0, import_i18n.t)("table.commits")},${totalProposalCommits}`);
|
|
257
|
+
rows.push(`${(0, import_i18n.t)("table.codeFiles")},${totalProposalFiles}`);
|
|
258
|
+
rows.push(`${(0, import_i18n.t)("table.additions")},${totalProposalAdditions}`);
|
|
259
|
+
rows.push(`${(0, import_i18n.t)("table.deletions")},${totalProposalDeletions}`);
|
|
260
|
+
rows.push(`${(0, import_i18n.t)("table.netChanges")},${totalProposalNetChanges}`);
|
|
261
|
+
rows.push(`
|
|
262
|
+
# ${(0, import_i18n.t)("output.authorSummary")}`);
|
|
263
|
+
rows.push(
|
|
264
|
+
`${(0, import_i18n.t)("table.author")},${(0, import_i18n.t)("table.period")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.proposalsCount")},${(0, import_i18n.t)("table.proposalsList")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")},${(0, import_i18n.t)("table.lastCommitDate")}`
|
|
163
265
|
);
|
|
266
|
+
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
164
267
|
for (const stats of sortedAuthors) {
|
|
165
268
|
const proposals = Array.from(stats.openspecProposals).join(";");
|
|
166
269
|
rows.push(
|
|
@@ -189,13 +292,48 @@ ${stats.author}
|
|
|
189
292
|
});
|
|
190
293
|
md += (0, import_i18n.t)("markdown.branches", { branches: result.branches.join(", ") });
|
|
191
294
|
md += (0, import_i18n.t)("markdown.totalCommits", { count: String(result.totalCommits) });
|
|
192
|
-
md +=
|
|
295
|
+
md += `
|
|
296
|
+
## ${(0, import_i18n.t)("output.proposalSummary")}
|
|
297
|
+
|
|
298
|
+
`;
|
|
299
|
+
md += `| ${(0, import_i18n.t)("table.proposal")} | ${(0, import_i18n.t)("table.commits")} | ${(0, import_i18n.t)("table.contributors")} | ${(0, import_i18n.t)("table.codeFiles")} | ${(0, import_i18n.t)("table.additions")} | ${(0, import_i18n.t)("table.deletions")} | ${(0, import_i18n.t)("table.netChanges")} |
|
|
300
|
+
`;
|
|
301
|
+
md += "|--------|---------|-------------|------------|-----------|-----------|-------------|\n";
|
|
302
|
+
const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
|
|
303
|
+
for (const stats of sortedProposals) {
|
|
304
|
+
const contributors = Array.from(stats.contributors).join(", ");
|
|
305
|
+
md += `| ${stats.proposal} | ${stats.commits} | ${contributors} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
const totalProposals = result.proposals.size;
|
|
309
|
+
const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
|
|
310
|
+
const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
|
|
311
|
+
const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
|
|
312
|
+
const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
|
|
313
|
+
const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
|
|
314
|
+
md += `
|
|
315
|
+
**${(0, import_i18n.t)("output.proposalTotalLabel")}**
|
|
316
|
+
`;
|
|
317
|
+
md += `- ${(0, import_i18n.t)("table.proposals")}: ${totalProposals}
|
|
318
|
+
`;
|
|
319
|
+
md += `- ${(0, import_i18n.t)("table.commits")}: ${totalProposalCommits}
|
|
320
|
+
`;
|
|
321
|
+
md += `- ${(0, import_i18n.t)("table.codeFiles")}: ${totalProposalFiles}
|
|
322
|
+
`;
|
|
323
|
+
md += `- ${(0, import_i18n.t)("table.additions")}: +${totalProposalAdditions}
|
|
324
|
+
`;
|
|
325
|
+
md += `- ${(0, import_i18n.t)("table.deletions")}: -${totalProposalDeletions}
|
|
326
|
+
`;
|
|
327
|
+
md += `- ${(0, import_i18n.t)("table.netChanges")}: ${totalProposalNetChanges >= 0 ? "+" : ""}${totalProposalNetChanges}
|
|
328
|
+
`;
|
|
329
|
+
md += `
|
|
330
|
+
## ${(0, import_i18n.t)("output.authorSummary")}
|
|
331
|
+
|
|
332
|
+
`;
|
|
193
333
|
md += `| ${(0, import_i18n.t)("table.author")} | ${(0, import_i18n.t)("table.period")} | ${(0, import_i18n.t)("table.commits")} | ${(0, import_i18n.t)("table.proposals")} | ${(0, import_i18n.t)("table.codeFiles")} | ${(0, import_i18n.t)("table.additions")} | ${(0, import_i18n.t)("table.deletions")} | ${(0, import_i18n.t)("table.netChanges")} |
|
|
194
334
|
`;
|
|
195
335
|
md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
|
|
196
|
-
const sortedAuthors = Array.from(result.authors.values()).sort(
|
|
197
|
-
(a, b) => b.commits - a.commits
|
|
198
|
-
);
|
|
336
|
+
const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
|
|
199
337
|
for (const stats of sortedAuthors) {
|
|
200
338
|
md += `| ${stats.author} | ${stats.statisticsPeriod || "-"} | ${stats.commits} | ${stats.openspecProposals.size} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
|
|
201
339
|
`;
|
package/dist/cjs/git-analyzer.js
CHANGED
|
@@ -44,13 +44,15 @@ var GitAnalyzer = class {
|
|
|
44
44
|
const untilStr = until.toISOString();
|
|
45
45
|
const logOptions = {
|
|
46
46
|
"--since": sinceStr,
|
|
47
|
-
"--until": untilStr
|
|
48
|
-
"--all": null
|
|
47
|
+
"--until": untilStr
|
|
49
48
|
};
|
|
50
49
|
if (branches.length > 0) {
|
|
50
|
+
delete logOptions["--all"];
|
|
51
51
|
for (const branch of branches) {
|
|
52
|
-
logOptions[
|
|
52
|
+
logOptions[branch] = null;
|
|
53
53
|
}
|
|
54
|
+
} else {
|
|
55
|
+
logOptions["--all"] = null;
|
|
54
56
|
}
|
|
55
57
|
const log = await this.git.log(logOptions);
|
|
56
58
|
const commits = [];
|
|
@@ -83,11 +85,7 @@ var GitAnalyzer = class {
|
|
|
83
85
|
}
|
|
84
86
|
async analyzeCommit(commit) {
|
|
85
87
|
try {
|
|
86
|
-
const show = await this.git.show([
|
|
87
|
-
"--numstat",
|
|
88
|
-
"--format=",
|
|
89
|
-
commit.hash
|
|
90
|
-
]);
|
|
88
|
+
const show = await this.git.show(["--numstat", "--format=", commit.hash]);
|
|
91
89
|
const lines = show.split("\n").filter((line) => line.trim());
|
|
92
90
|
const fileChanges = [];
|
|
93
91
|
const openspecProposals = /* @__PURE__ */ new Set();
|
|
@@ -103,9 +101,7 @@ var GitAnalyzer = class {
|
|
|
103
101
|
const additions = addStr === "-" ? 0 : parseInt(addStr, 10);
|
|
104
102
|
const deletions = delStr === "-" ? 0 : parseInt(delStr, 10);
|
|
105
103
|
if (path.startsWith(openspecDir)) {
|
|
106
|
-
const proposalMatch = path.match(
|
|
107
|
-
new RegExp(`^${openspecDir}changes/([^/]+)`)
|
|
108
|
-
);
|
|
104
|
+
const proposalMatch = path.match(new RegExp(`^${openspecDir}changes/([^/]+)`));
|
|
109
105
|
if (proposalMatch) {
|
|
110
106
|
openspecProposals.add(proposalMatch[1]);
|
|
111
107
|
}
|
|
@@ -123,14 +119,8 @@ var GitAnalyzer = class {
|
|
|
123
119
|
}
|
|
124
120
|
}
|
|
125
121
|
if (openspecProposals.size > 0 && hasCodeChanges) {
|
|
126
|
-
const totalAdditions = fileChanges.reduce(
|
|
127
|
-
|
|
128
|
-
0
|
|
129
|
-
);
|
|
130
|
-
const totalDeletions = fileChanges.reduce(
|
|
131
|
-
(sum, f) => sum + f.deletions,
|
|
132
|
-
0
|
|
133
|
-
);
|
|
122
|
+
const totalAdditions = fileChanges.reduce((sum, f) => sum + f.additions, 0);
|
|
123
|
+
const totalDeletions = fileChanges.reduce((sum, f) => sum + f.deletions, 0);
|
|
134
124
|
return {
|
|
135
125
|
commit,
|
|
136
126
|
openspecProposals,
|
|
@@ -155,10 +145,7 @@ var GitAnalyzer = class {
|
|
|
155
145
|
});
|
|
156
146
|
const authors = /* @__PURE__ */ new Set();
|
|
157
147
|
for (const commit of log.all) {
|
|
158
|
-
const normalizedAuthor = (0, import_config.normalizeAuthor)(
|
|
159
|
-
commit.author_name,
|
|
160
|
-
this.config.authorMapping
|
|
161
|
-
);
|
|
148
|
+
const normalizedAuthor = (0, import_config.normalizeAuthor)(commit.author_name, this.config.authorMapping);
|
|
162
149
|
authors.add(normalizedAuthor);
|
|
163
150
|
}
|
|
164
151
|
return authors;
|
package/dist/cjs/i18n/index.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"cli.option.config": "Configuration file path",
|
|
13
13
|
"cli.option.verbose": "Verbose output mode",
|
|
14
14
|
"cli.option.lang": "Language for output (en, zh-CN)",
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
"loading.config": "🔍 Loading configuration...",
|
|
17
17
|
"loading.activeUsers": "🔍 Fetching active users...",
|
|
18
18
|
"loading.analyzing": "🔍 Analyzing commit history...",
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
"info.timeRange": "📅 Time Range: {{since}} ~ {{until}}",
|
|
21
21
|
"info.branches": "🌿 Branches: {{branches}}",
|
|
22
22
|
"info.allBranches": "All branches",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"info.foundCommits": "📝 Found {{count}} commits, analyzing...",
|
|
25
25
|
"info.analysisProgress": " Analysis progress: {{current}}/{{total}}",
|
|
26
26
|
"info.qualifyingCommits": "✅ Found {{count}} qualifying commits (containing OpenSpec proposals and code changes)",
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
"warning.noCommits": "⚠️ No commits found matching the criteria",
|
|
29
29
|
"warning.noQualifyingCommits": "⚠️ No commits found containing both OpenSpec proposals and code changes",
|
|
30
30
|
"warning.noBranches": "⚠️ No remote branches found",
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
"error.prefix": "❌ Error:",
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
"branch.fetching": "\n🔍 Fetching active branches...",
|
|
35
35
|
"branch.selectMode": "How would you like to select branches?",
|
|
36
36
|
"branch.mode.select": "Select from active branches",
|
|
@@ -42,17 +42,23 @@
|
|
|
42
42
|
"branch.customSeparator": "--- Custom input ---",
|
|
43
43
|
"branch.selected": "\n✓ Selected branches:",
|
|
44
44
|
"branch.lastCommit": "last commit: {{date}}",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"output.title": "\n📊 OpenSpec Statistics Report\n",
|
|
47
47
|
"output.timeRange": "Time Range: {{since}} ~ {{until}}\n",
|
|
48
48
|
"output.branches": "Branches: {{branches}}\n",
|
|
49
49
|
"output.totalCommits": "Total Commits: {{count}}\n\n",
|
|
50
50
|
"output.proposals": " Proposals: {{proposals}}\n",
|
|
51
|
-
|
|
51
|
+
"output.proposalSummary": "📋 Proposal Summary (by proposal)",
|
|
52
|
+
"output.proposalTotal": " 📊 Total: {{count}} proposals | {{commits}} commits | {{files}} files | +{{additions}}/-{{deletions}} lines (net: {{netChanges}})\n",
|
|
53
|
+
"output.proposalTotalLabel": "Proposal Summary Total",
|
|
54
|
+
"output.authorSummary": "👥 Author Summary (by contributor)",
|
|
55
|
+
|
|
52
56
|
"table.branch": "Branch",
|
|
53
57
|
"table.period": "Period",
|
|
54
58
|
"table.commits": "Commits",
|
|
55
59
|
"table.proposals": "Proposals",
|
|
60
|
+
"table.proposal": "Proposal",
|
|
61
|
+
"table.contributors": "Contributors",
|
|
56
62
|
"table.codeFiles": "Code Files",
|
|
57
63
|
"table.additions": "Additions",
|
|
58
64
|
"table.deletions": "Deletions",
|
|
@@ -62,7 +68,7 @@
|
|
|
62
68
|
"table.proposalsList": "Proposals List",
|
|
63
69
|
"table.proposalsCount": "Proposals Count",
|
|
64
70
|
"table.totalDeduplicated": "TOTAL (Deduplicated)",
|
|
65
|
-
|
|
71
|
+
|
|
66
72
|
"markdown.title": "# OpenSpec Statistics Report\n\n",
|
|
67
73
|
"markdown.timeRange": "**Time Range**: {{since}} ~ {{until}}\n\n",
|
|
68
74
|
"markdown.branches": "**Branches**: {{branches}}\n\n",
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"cli.option.config": "配置文件路径",
|
|
13
13
|
"cli.option.verbose": "详细输出模式",
|
|
14
14
|
"cli.option.lang": "输出语言(en, zh-CN)",
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
"loading.config": "🔍 正在加载配置...",
|
|
17
17
|
"loading.activeUsers": "🔍 正在获取活跃用户...",
|
|
18
18
|
"loading.analyzing": "🔍 正在分析提交历史...",
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
"info.timeRange": "📅 时间范围:{{since}} ~ {{until}}",
|
|
21
21
|
"info.branches": "🌿 分支:{{branches}}",
|
|
22
22
|
"info.allBranches": "所有分支",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"info.foundCommits": "📝 找到 {{count}} 个提交,正在分析...",
|
|
25
25
|
"info.analysisProgress": " 分析进度:{{current}}/{{total}}",
|
|
26
26
|
"info.qualifyingCommits": "✅ 找到 {{count}} 个符合条件的提交(包含 OpenSpec 提案和代码变更)",
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
"warning.noCommits": "⚠️ 未找到符合条件的提交",
|
|
29
29
|
"warning.noQualifyingCommits": "⚠️ 未找到同时包含 OpenSpec 提案和代码变更的提交",
|
|
30
30
|
"warning.noBranches": "⚠️ 未找到远程分支",
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
"error.prefix": "❌ 错误:",
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
"branch.fetching": "\n🔍 正在获取活跃分支...",
|
|
35
35
|
"branch.selectMode": "您想如何选择分支?",
|
|
36
36
|
"branch.mode.select": "从活跃分支中选择",
|
|
@@ -42,17 +42,23 @@
|
|
|
42
42
|
"branch.customSeparator": "--- 自定义输入 ---",
|
|
43
43
|
"branch.selected": "\n✓ 已选择的分支:",
|
|
44
44
|
"branch.lastCommit": "最后提交:{{date}}",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"output.title": "\n📊 OpenSpec 统计报告\n",
|
|
47
47
|
"output.timeRange": "时间范围:{{since}} ~ {{until}}\n",
|
|
48
48
|
"output.branches": "分支:{{branches}}\n",
|
|
49
49
|
"output.totalCommits": "总提交数:{{count}}\n\n",
|
|
50
50
|
"output.proposals": " 提案:{{proposals}}\n",
|
|
51
|
-
|
|
51
|
+
"output.proposalSummary": "📋 提案汇总(按提案统计)",
|
|
52
|
+
"output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
|
|
53
|
+
"output.proposalTotalLabel": "提案汇总总计",
|
|
54
|
+
"output.authorSummary": "👥 作者汇总(按贡献者统计)",
|
|
55
|
+
|
|
52
56
|
"table.branch": "分支",
|
|
53
57
|
"table.period": "周期",
|
|
54
58
|
"table.commits": "提交数",
|
|
55
59
|
"table.proposals": "提案数",
|
|
60
|
+
"table.proposal": "提案",
|
|
61
|
+
"table.contributors": "贡献者",
|
|
56
62
|
"table.codeFiles": "代码文件",
|
|
57
63
|
"table.additions": "新增行数",
|
|
58
64
|
"table.deletions": "删除行数",
|
|
@@ -62,7 +68,7 @@
|
|
|
62
68
|
"table.proposalsList": "提案列表",
|
|
63
69
|
"table.proposalsCount": "提案数量",
|
|
64
70
|
"table.totalDeduplicated": "总计(去重)",
|
|
65
|
-
|
|
71
|
+
|
|
66
72
|
"markdown.title": "# OpenSpec 统计报告\n\n",
|
|
67
73
|
"markdown.timeRange": "**时间范围**:{{since}} ~ {{until}}\n\n",
|
|
68
74
|
"markdown.branches": "**分支**:{{branches}}\n\n",
|
|
@@ -30,11 +30,9 @@ var StatsAggregator = class {
|
|
|
30
30
|
}
|
|
31
31
|
aggregate(analyses, since, until, branches, filterAuthor) {
|
|
32
32
|
const authorStatsMap = /* @__PURE__ */ new Map();
|
|
33
|
+
const proposalStatsMap = /* @__PURE__ */ new Map();
|
|
33
34
|
for (const analysis of analyses) {
|
|
34
|
-
const normalizedAuthor = (0, import_config.normalizeAuthor)(
|
|
35
|
-
analysis.commit.author,
|
|
36
|
-
this.config.authorMapping
|
|
37
|
-
);
|
|
35
|
+
const normalizedAuthor = (0, import_config.normalizeAuthor)(analysis.commit.author, this.config.authorMapping);
|
|
38
36
|
if (this.activeAuthors && !this.activeAuthors.has(normalizedAuthor)) {
|
|
39
37
|
continue;
|
|
40
38
|
}
|
|
@@ -62,6 +60,29 @@ var StatsAggregator = class {
|
|
|
62
60
|
stats.codeFilesChanged += analysis.codeFiles.length;
|
|
63
61
|
for (const proposal of analysis.openspecProposals) {
|
|
64
62
|
stats.openspecProposals.add(proposal);
|
|
63
|
+
let proposalStats = proposalStatsMap.get(proposal);
|
|
64
|
+
if (!proposalStats) {
|
|
65
|
+
proposalStats = {
|
|
66
|
+
proposal,
|
|
67
|
+
commits: 0,
|
|
68
|
+
contributors: /* @__PURE__ */ new Set(),
|
|
69
|
+
codeFilesChanged: 0,
|
|
70
|
+
additions: 0,
|
|
71
|
+
deletions: 0,
|
|
72
|
+
netChanges: 0,
|
|
73
|
+
commitHashes: /* @__PURE__ */ new Set()
|
|
74
|
+
};
|
|
75
|
+
proposalStatsMap.set(proposal, proposalStats);
|
|
76
|
+
}
|
|
77
|
+
if (!proposalStats.commitHashes.has(analysis.commit.hash)) {
|
|
78
|
+
proposalStats.commitHashes.add(analysis.commit.hash);
|
|
79
|
+
proposalStats.commits++;
|
|
80
|
+
proposalStats.contributors.add(normalizedAuthor);
|
|
81
|
+
proposalStats.codeFilesChanged += analysis.codeFiles.length;
|
|
82
|
+
proposalStats.additions += analysis.totalAdditions;
|
|
83
|
+
proposalStats.deletions += analysis.totalDeletions;
|
|
84
|
+
proposalStats.netChanges += analysis.netChanges;
|
|
85
|
+
}
|
|
65
86
|
}
|
|
66
87
|
if (!stats.lastCommitDate || analysis.commit.date > stats.lastCommitDate) {
|
|
67
88
|
stats.lastCommitDate = analysis.commit.date;
|
|
@@ -103,10 +124,19 @@ var StatsAggregator = class {
|
|
|
103
124
|
stats.statisticsPeriod = days === 0 ? "1 day" : `${days + 1} days`;
|
|
104
125
|
}
|
|
105
126
|
}
|
|
127
|
+
const actualBranches = /* @__PURE__ */ new Set();
|
|
128
|
+
for (const stats of authorStatsMap.values()) {
|
|
129
|
+
if (stats.branchStats) {
|
|
130
|
+
for (const branch of stats.branchStats.keys()) {
|
|
131
|
+
actualBranches.add(branch);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
106
135
|
return {
|
|
107
136
|
timeRange: { since, until },
|
|
108
|
-
branches,
|
|
137
|
+
branches: actualBranches.size > 0 ? Array.from(actualBranches) : branches,
|
|
109
138
|
authors: authorStatsMap,
|
|
139
|
+
proposals: proposalStatsMap,
|
|
110
140
|
totalCommits: analyses.length
|
|
111
141
|
};
|
|
112
142
|
}
|