wukong-gitlog-cli 1.0.38 → 1.0.40

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.
Files changed (124) hide show
  1. package/.eslintrc +1 -0
  2. package/.prettierrc +2 -1
  3. package/CHANGELOG.md +103 -0
  4. package/README.md +93 -173
  5. package/README.zh-CN.md +85 -137
  6. package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +2871 -0
  7. package/package.json +33 -29
  8. package/rc/.wukonggitlogrc +53 -0
  9. package/scripts/compareHourlyCounts.mjs +42 -0
  10. package/scripts/compareLatest.mjs +106 -0
  11. package/src/app/analyzeAction.mjs +120 -0
  12. package/src/app/exportAction.mjs +215 -0
  13. package/src/app/exportActionProgress.mjs +37 -0
  14. package/src/app/helpers.mjs +292 -0
  15. package/src/app/initAction.mjs +110 -0
  16. package/src/app/initActionWithTemp.mjs +192 -0
  17. package/src/app/journalAction.mjs +117 -0
  18. package/src/app/overtimeAction.mjs +100 -0
  19. package/src/app/runProfileEnd.mjs +0 -0
  20. package/src/app/serveAction.mjs +73 -0
  21. package/src/app/versionAction.mjs +7 -0
  22. package/src/cli/defineOptions.mjs +209 -0
  23. package/src/cli/index.mjs +0 -0
  24. package/src/cli/parseOptions.mjs +126 -8
  25. package/src/constants/index.mjs +16 -2
  26. package/src/domain/author/analyze.mjs +6 -0
  27. package/src/domain/author/map.mjs +0 -0
  28. package/src/domain/export/exportAuthor.mjs +28 -0
  29. package/src/domain/export/exportAuthorChanges.mjs +27 -0
  30. package/src/domain/export/exportAuthorChangesJson.mjs +31 -0
  31. package/src/domain/export/exportByMonth.mjs +157 -0
  32. package/src/domain/export/exportByWeek.mjs +121 -0
  33. package/src/domain/export/exportCommits.mjs +26 -0
  34. package/src/domain/export/exportCommitsExcel.mjs +45 -0
  35. package/src/domain/export/exportCommitsJson.mjs +31 -0
  36. package/src/domain/export/index.mjs +91 -0
  37. package/src/domain/git/ensureGitAvailable.mjs +66 -0
  38. package/src/domain/git/ensureGitRepo.mjs +41 -0
  39. package/src/domain/git/getGitFeatures.mjs +59 -0
  40. package/src/domain/git/getGitLogs.mjs +326 -0
  41. package/src/domain/git/getGitUser.mjs +44 -0
  42. package/src/domain/git/getRepoRoot.mjs +32 -0
  43. package/src/domain/git/gitCapability.mjs +119 -0
  44. package/src/domain/git/index.mjs +96 -0
  45. package/src/domain/git/resolveGerrit.mjs +102 -0
  46. package/src/domain/overtime/analyze.mjs +48 -0
  47. package/src/domain/overtime/index.mjs +3 -0
  48. package/src/domain/overtime/perPeriod.mjs +15 -0
  49. package/src/domain/overtime/render.mjs +15 -0
  50. package/src/i18n/index.mjs +38 -0
  51. package/src/i18n/resources.mjs +252 -0
  52. package/src/index.mjs +132 -649
  53. package/src/infra/cache.mjs +0 -0
  54. package/src/infra/configStore.mjs +128 -0
  55. package/src/infra/fs.mjs +0 -0
  56. package/src/infra/path.mjs +0 -0
  57. package/src/output/csv/overtime.mjs +12 -0
  58. package/src/output/csv.mjs +0 -0
  59. package/src/output/data/readData.mjs +54 -0
  60. package/src/output/data/writeData.mjs +145 -0
  61. package/src/output/excel/commits.mjs +9 -0
  62. package/src/output/excel/outputExcelDayReport.mjs +92 -0
  63. package/src/output/excel/perPeriod.mjs +24 -0
  64. package/src/{excel.mjs → output/excel.mjs} +3 -2
  65. package/src/output/index.mjs +79 -0
  66. package/src/output/json/overtime.mjs +9 -0
  67. package/src/output/tab/overtime.mjs +12 -0
  68. package/src/output/tab.mjs +0 -0
  69. package/src/output/text/commits.mjs +9 -0
  70. package/src/output/text/index.mjs +3 -0
  71. package/src/output/text/outputTxtDayReport.mjs +74 -0
  72. package/src/output/text/overtime.mjs +18 -0
  73. package/src/output/utils/getEsmJs.mjs +10 -0
  74. package/src/output/utils/index.mjs +14 -0
  75. package/src/output/utils/outputPath.mjs +19 -0
  76. package/src/output/utils/writeFile.mjs +10 -0
  77. package/src/serve/index.mjs +0 -0
  78. package/src/{server.mjs → serve/startServer.mjs} +21 -3
  79. package/src/serve/writeData.mjs +0 -0
  80. package/src/utils/authorNormalizer.mjs +28 -2
  81. package/src/utils/buildAuthorChangeStats.mjs +44 -0
  82. package/src/utils/deepMerge.mjs +13 -0
  83. package/src/utils/getPackage.mjs +11 -0
  84. package/src/utils/getProfileDirFile.mjs +12 -0
  85. package/src/utils/{file.mjs → groupRecords.mjs} +8 -9
  86. package/src/utils/index.mjs +5 -2
  87. package/src/utils/logger.mjs +28 -17
  88. package/src/utils/profiler.mjs +0 -101
  89. package/src/utils/resolve.mjs +11 -0
  90. package/src/utils/showVersionInfo.mjs +6 -2
  91. package/src/utils/time.mjs +0 -0
  92. package/src/utils/wait.mjs +2 -0
  93. package/web/app.js +3233 -260
  94. package/web/index.html +175 -22
  95. package/web/revoke/alpha1/app.js +4324 -0
  96. package/web/revoke/alpha1/index.html +266 -0
  97. package/web/revoke/app.before.js +3139 -0
  98. package/web/revoke/index-before.html +181 -0
  99. package/web/static/style.css +155 -9
  100. package/src/git.mjs +0 -256
  101. package/src/handlers/handleServe.mjs +0 -203
  102. package/src/lib/configStore.mjs +0 -11
  103. package/src/lib/memoize.mjs +0 -14
  104. package/src/utils/analyzeOvertimeCached.mjs +0 -7
  105. package/src/utils/checkUpdate.mjs +0 -130
  106. package/src/utils/exitWithTime.mjs +0 -17
  107. package/src/utils/handleSuccess.mjs +0 -9
  108. package/src/utils/logDev.mjs +0 -19
  109. package/src/utils/output.mjs +0 -26
  110. package/src/utils/profiler/diff.mjs +0 -26
  111. package/src/utils/profiler/format.mjs +0 -11
  112. package/src/utils/profiler/index.mjs +0 -144
  113. package/src/utils/profiler/trace.mjs +0 -26
  114. package/src/utils/time/scopeTimer.mjs +0 -37
  115. package/src/utils/time/timer.mjs +0 -33
  116. package/src/utils/time/withTimer.mjs +0 -11
  117. package/src/utils/timer.mjs +0 -35
  118. /package/src/{overtime → domain/overtime}/createOvertimeStats.mjs +0 -0
  119. /package/src/{overtime → domain/overtime}/overtime.mjs +0 -0
  120. /package/src/{json.mjs → output/json.mjs} +0 -0
  121. /package/src/{renderAuthorMapText.mjs → output/renderAuthorMapText.mjs} +0 -0
  122. /package/src/{stats-text.mjs → output/stats-text.mjs} +0 -0
  123. /package/src/{stats.mjs → output/stats.mjs} +0 -0
  124. /package/src/{text.mjs → output/text.mjs} +0 -0
@@ -0,0 +1,181 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>wukong-gitlog-cli — Overtime Dashboard</title>
7
+ <link rel="stylesheet" href="./static/style.css" />
8
+ <script src="./static/echarts.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <h1>Wukong Gitlog Overtime Dashboard</h1>
13
+ </header>
14
+ <main>
15
+ <section id="chartsHalf">
16
+ <div class="chart-card">
17
+ <h2>下班时间 vs 工作时间提交占比</h2>
18
+ <div id="outsideVsInsideChart" class="echart"></div>
19
+ </div>
20
+ <div class="chart-card" id="kpiCard">
21
+ <h2>关键指标</h2>
22
+ <div id="kpiContent"></div>
23
+ </div>
24
+ </section>
25
+ <section id="charts">
26
+ <div class="chart-card">
27
+ <h2>每小时加班分布 (小时 -> 提交数)</h2>
28
+ <div id="hourlyOvertimeChart" data-title="每小时加班分布" class="echart"></div>
29
+ </div>
30
+
31
+ <div class="chart-card">
32
+ <h2>按日提交趋势(次数)</h2>
33
+ <div id="dailyTrendChart" data-title="按日提交趋势(次数)" class="echart"></div>
34
+ </div>
35
+
36
+ <div class="chart-card">
37
+ <h2>每周趋势(加班占比)</h2>
38
+ <div id="weeklyTrendChart" data-title="每周趋势(加班占比)" class="echart"></div>
39
+ </div>
40
+ <div class="chart-card">
41
+ <h2>每月趋势(加班占比)</h2>
42
+ <div id="monthlyTrendChart" data-title="每月趋势(加班占比)" class="echart"></div>
43
+ </div>
44
+ <div class="chart-card">
45
+ <h2>每日最晚提交时间(小时)</h2>
46
+ <div id="latestHourDailyChart" data-title="每日最晚提交时间(小时)" class="echart"></div>
47
+ </div>
48
+ <div class="chart-card">
49
+ <h2>每日超过下班的小时数</h2>
50
+ <div id="dailySeverityChart" data-title="每日超过下班的小时数" class="echart"></div>
51
+ </div>
52
+ <div class="chart-card">
53
+ <h2>按日提交趋势(次数)</h2>
54
+ <div id="dailyTrendChartDog" data-title="按日提交趋势" class="echart"></div>
55
+ </div>
56
+ <div class="chart-card">
57
+ <div id="mostTiredDay"></div>
58
+ <div id="mostTiredWeek"></div>
59
+ <div id="mostTiredMonth"></div>
60
+ <div id="latestOvertimeDay"></div>
61
+ <div id="latestOvertimeWeek"></div>
62
+ <div id="latestOvertimeMonth"></div>
63
+ </div>
64
+ </section>
65
+
66
+ <div class="chart-card">
67
+ <h2>开发者 Changed 工作量趋势(行数)</h2>
68
+
69
+ <div id="tabs" class="tabs">
70
+ <button data-type="daily" class="active">按日</button>
71
+ <button data-type="weekly">按周</button>
72
+ <button data-type="monthly">按月</button>
73
+ </div>
74
+ <div id="chartAuthorChanges" class="echart"></div>
75
+ </div>
76
+
77
+ <div class="chart-card">
78
+ <h2>开发者 加班趋势(次数)</h2>
79
+ <div class="tabs" id="tabsOvertime">
80
+ <button data-type="daily" class="active">按日</button>
81
+ <button data-type="weekly">按周</button>
82
+ <button data-type="monthly">按月</button>
83
+ </div>
84
+ <div id="chartAuthorOvertime" class="echart"></div>
85
+ <div id="weeklyRiskSummary" class="risk-summary-box"></div>
86
+ <div id="monthlyRiskSummary" class="risk-summary-box"></div>
87
+ <div id="weeklyDurationRankSummary" class="risk-summary-box"></div>
88
+ <div id="weeklyDurationRiskSummary" class="risk-summary-box"></div>
89
+ <div id="monthlyDurationRankSummary" class="risk-summary-box"></div>
90
+ <div id="monthlyDurationRiskSummary" class="risk-summary-box"></div>
91
+ <div id="rolling30DurationRiskSummary" class="risk-summary-box"></div>
92
+ </div>
93
+
94
+ <div class="chart-card">
95
+ <h2>开发者 加班最晚趋势(小时)</h2>
96
+ <div class="tabs" id="tabsLatestOvertime">
97
+ <button data-type="daily" class="active">按日</button>
98
+ <button data-type="weekly">按周</button>
99
+ <button data-type="monthly">按月</button>
100
+ </div>
101
+ <div id="chartAuthorLatestOvertime" class="echart"></div>
102
+ <div id="latestRiskSummary" class="risk-summary-box"></div>
103
+ <div id="latestMonthlyRiskSummary" class="risk-summary-box"></div>
104
+ </div>
105
+
106
+ <div class="chart-card">
107
+ <h2>开发者 午休最晚提交(小时)</h2>
108
+ <div class="tabs" id="tabsLunch">
109
+ <button data-type="daily" class="active">按日</button>
110
+ <button data-type="weekly">按周</button>
111
+ <button data-type="monthly">按月</button>
112
+ </div>
113
+ <div id="chartAuthorLunch" class="echart"></div>
114
+ <div id="lunchWeeklyRankSummary" class="risk-summary-box"></div>
115
+ <div id="lunchWeeklyRiskSummary" class="risk-summary-box"></div>
116
+ <div id="lunchMonthlyRankSummary" class="risk-summary-box"></div>
117
+ <div id="lunchMonthlyRiskSummary" class="risk-summary-box"></div>
118
+ </div>
119
+
120
+ <section class="table-card">
121
+ <h2>提交清单</h2>
122
+ <div id="tableControls">
123
+ <input
124
+ id="searchInput"
125
+ type="search"
126
+ placeholder="搜索作者/信息/Hash/Date"
127
+ />
128
+ <input type="date" id="startDate" />
129
+ <span>~</span>
130
+ <input type="date" id="endDate" />
131
+ <button id="clearDate">清除</button>
132
+ <label for="pageSize">每页显示</label>
133
+ <select id="pageSize">
134
+ <option value="10">10</option>
135
+ <option value="20">20</option>
136
+ <option value="50">50</option>
137
+ <option value="100">100</option>
138
+ </select>
139
+ <label id="commitsTotal"></label>
140
+ <div class="pager">
141
+ <button id="prevPage">上一页</button>
142
+ <span id="pageInfo"></span>
143
+ <button id="nextPage">下一页</button>
144
+ </div>
145
+ </div>
146
+ <table id="commitsTable">
147
+ <thead>
148
+ <tr>
149
+ <th>Hash</th>
150
+ <th>Author</th>
151
+ <th>Email</th>
152
+ <th>Date</th>
153
+ <th>Message</th>
154
+ <th>Changed</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody></tbody>
158
+ </table>
159
+ </section>
160
+ </main>
161
+ <footer>
162
+ <small
163
+ >Local dashboard generated by wukong-gitlog-cli. Data served from
164
+ <code>/data/</code>.</small
165
+ >
166
+ </footer>
167
+ <!-- 通用:右侧滑出的详情侧栏(小时 / 天 / 周 复用同一个 DOM) -->
168
+ <!-- 背景遮罩,点击可关闭侧栏 -->
169
+ <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
170
+ <div id="dayDetailSidebar" class="sidebar">
171
+ <div class="sidebar-header">
172
+ <span id="sidebarDrawerTitle"></span>
173
+ <button id="sidebarClose">×</button>
174
+ </div>
175
+ <div id="sidebarTitle"></div>
176
+ <div id="sidebarContent" class="sidebar-content"></div>
177
+ </div>
178
+
179
+ <script type="module" src="/app.before.js"></script>
180
+ </body>
181
+ </html>
@@ -28,14 +28,27 @@ header h1 {
28
28
  font-weight: 500;
29
29
  }
30
30
 
31
+ /* 采样信息(显示在 header 旁边) */
32
+ .sampling-info {
33
+ margin-left: 16px;
34
+ font-size: 13px;
35
+ color: rgba(255, 255, 255, 0.95);
36
+ font-weight: 400;
37
+ opacity: 0.95;
38
+ }
39
+ .sampling {
40
+ color: #00a76f;
41
+ }
42
+
31
43
  /* 主体布局 -------------------------------------------------- */
32
44
  main {
33
45
  display: flex;
34
46
  flex-direction: column;
35
47
  gap: 20px;
36
48
  padding: 20px;
37
- max-width: 1280px;
49
+ /* max-width: 1280px; */
38
50
  margin: 20px auto;
51
+ overflow-x: hidden;
39
52
  }
40
53
 
41
54
  /* 卡片基础样式(MUI Paper) ----------------------------- */
@@ -58,10 +71,24 @@ main {
58
71
  /* 图表网格 -------------------------------------------------- */
59
72
  #charts {
60
73
  display: grid;
61
- grid-template-columns: repeat(auto-fit, minmax(640px, 1fr));
62
- gap: 16px;
74
+ /* 移动端默认:单列,防止溢出 */
75
+ grid-template-columns: 1fr;
76
+ gap: 20px;
77
+ padding: 16px;
78
+ }
79
+
80
+ @media (min-width: 768px) {
81
+ #charts {
82
+ /* 桌面端:最小宽度 640px,自动填充 */
83
+ grid-template-columns: repeat(auto-fit, minmax(640px, 1fr));
84
+ }
63
85
  }
64
- #chartsHalf {
86
+ /* 让核心趋势图始终占据整行,无论分几列 */
87
+ .full-width-chart {
88
+ grid-column: 1 / -1;
89
+ }
90
+ #chartsHalf,
91
+ .chartsHalf {
65
92
  display: grid;
66
93
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
67
94
  gap: 16px;
@@ -77,6 +104,8 @@ main {
77
104
  height: 340px;
78
105
  }
79
106
 
107
+
108
+
80
109
  /* 表格 -------------------------------------------------- */
81
110
  table {
82
111
  width: 100%;
@@ -97,6 +126,18 @@ td {
97
126
  border-bottom: 1px solid #eee;
98
127
  }
99
128
 
129
+ @media screen and (max-width: 799px) {
130
+ .hash {
131
+ display: none;
132
+ }
133
+ .email {
134
+ display: none;
135
+ }
136
+ .cherryPick {
137
+ display: none;
138
+ }
139
+ }
140
+
100
141
  /* 分页、搜索栏 -------------------------------------------------- */
101
142
  #tableControls {
102
143
  display: flex;
@@ -139,6 +180,45 @@ td {
139
180
  box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
140
181
  outline: none;
141
182
  }
183
+ #tableControls input[type='date'] {
184
+ padding: 9px 12px;
185
+ border-radius: 8px;
186
+ border: 1px solid #ccc;
187
+ background: #fafafa;
188
+ font-size: 13px;
189
+ color: #111827;
190
+ transition:
191
+ border-color 0.2s,
192
+ box-shadow 0.2s;
193
+ }
194
+
195
+ #tableControls input[type='date']:focus {
196
+ border-color: #1976d2;
197
+ box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
198
+ outline: none;
199
+ }
200
+ /* 清除日期按钮 ------------------------------------------ */
201
+ #clearDate {
202
+ padding: 8px 10px;
203
+ border-radius: 8px;
204
+ border: none;
205
+ background: transparent;
206
+ color: #64748b; /* slate-500 */
207
+ font-size: 13px;
208
+ cursor: pointer;
209
+ transition:
210
+ color 0.2s ease,
211
+ background 0.2s ease;
212
+ }
213
+
214
+ #clearDate:hover {
215
+ color: #ef4444; /* red-500 */
216
+ background: rgba(239, 68, 68, 0.08);
217
+ }
218
+
219
+ #clearDate:active {
220
+ background: rgba(239, 68, 68, 0.16);
221
+ }
142
222
 
143
223
  /* 按钮 — MUI Button 风格 */
144
224
  .pager button {
@@ -335,8 +415,9 @@ td {
335
415
  }
336
416
 
337
417
  .sidebar-item-header .time {
338
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
339
- 'Liberation Mono', 'Courier New', monospace;
418
+ font-family:
419
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
420
+ 'Courier New', monospace;
340
421
  }
341
422
 
342
423
  .sidebar-item-message {
@@ -382,6 +463,38 @@ td {
382
463
  box-shadow: 0 3px 10px rgba(79, 70, 229, 0.25);
383
464
  }
384
465
 
466
+ /* 开发者累计加班排名(小圆点样式) --------------------------- */
467
+ .rank-list {
468
+ margin-top: 12px;
469
+ display: flex;
470
+ flex-direction: column;
471
+ gap: 8px;
472
+ padding: 0;
473
+ }
474
+ .rank-list .rank-item {
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 8px;
478
+ font-size: 14px;
479
+ color: #333;
480
+ }
481
+ .rank-list .dot {
482
+ width: 12px;
483
+ height: 12px;
484
+ border-radius: 50%;
485
+ display: inline-block;
486
+ margin-right: 8px;
487
+ /* flex: 0 0 12px; */
488
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.03) inset;
489
+ }
490
+ .rank-list .author {
491
+ flex: 1;
492
+ }
493
+ .rank-list .hours {
494
+ color: #666;
495
+ font-weight: 600;
496
+ }
497
+
385
498
  /* 风险总结卡片 ---------------------------------------------- */
386
499
  /* 风险总结卡片 — MUI风格 ---------------------------------------------- */
387
500
  .risk-summary {
@@ -394,8 +507,9 @@ td {
394
507
  border: 1px solid rgba(244, 199, 199, 0.9);
395
508
 
396
509
  /* MUI 风格的柔和阴影 */
397
- box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.08),
398
- 0px 1px 2px rgba(0, 0, 0, 0.04);
510
+ box-shadow:
511
+ 0px 3px 6px rgba(0, 0, 0, 0.08),
512
+ 0px 1px 2px rgba(0, 0, 0, 0.04);
399
513
 
400
514
  color: #b71c1c; /* 比 #991b1b 更接近 MUI error.dark */
401
515
  font-size: 14px;
@@ -421,7 +535,39 @@ td {
421
535
  margin-bottom: 6px;
422
536
  }
423
537
 
424
-
538
+ .box-flex {
539
+ display: flex;
540
+ justify-content: space-between;
541
+ align-items: center;
542
+ }
543
+ .date {
544
+ display: flex;
545
+ width: 120px;
546
+ }
547
+ .long-txt{
548
+ /* overflow: hidden;
549
+ text-overflow: ellipsis; */
550
+ word-break: break-all;
551
+ /* 或 */
552
+ overflow-wrap: break-word;
553
+ word-wrap: break-word;
554
+ }
555
+ .tooltip-box{
556
+ width:500px;
557
+ }
558
+ .long-txt-break-all{
559
+ word-break: break-all;
560
+ overflow-wrap: break-word;
561
+ word-wrap: break-word;
562
+ text-overflow: ellipsis;
563
+ overflow: hidden;
564
+ max-width: 300px;
565
+ }
566
+ @media (min-width: 768px) {
567
+ .long-txt-break-all{
568
+ max-width: 500px;
569
+ }
570
+ }
425
571
  /* 底部 -------------------------------------------------- */
426
572
  footer {
427
573
  text-align: center;
package/src/git.mjs DELETED
@@ -1,256 +0,0 @@
1
- import { $ } from 'zx'
2
-
3
- import { createAuthorNormalizer } from './utils/authorNormalizer.mjs'
4
-
5
- const normalizer = createAuthorNormalizer()
6
-
7
- export async function getGitLogsSlow(opts) {
8
- const { author, email, since, until, limit, merges } = opts
9
-
10
- const pretty = '%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%B%x1e'
11
- const args = ['log', `--pretty=format:${pretty}`, '--date=iso-local']
12
-
13
- if (author) args.push(`--author=${author}`)
14
- if (email) args.push(`--author=${email}`)
15
- if (since) args.push(`--since=${since}`)
16
- if (until) args.push(`--until=${until}`)
17
- if (merges === false) args.push(`--no-merges`)
18
- if (limit) args.push(`-n`, `${limit}`)
19
-
20
- const { stdout } = await $`git ${args}`.quiet()
21
-
22
- const commits = stdout
23
- .split('\x1e')
24
- .filter(Boolean)
25
- .map((r) => {
26
- const f = r.split('\x1f').map((s) => (s || '').trim())
27
- const hash = f[0]
28
- const authorName = f[1]
29
- const emailAddr = f[2]
30
- const date = f[3]
31
- const subject = f[4]
32
- const body = f[5] || ''
33
-
34
- const [, changeId] = body.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
35
-
36
- return {
37
- hash,
38
- author: normalizer.getAuthor(authorName, emailAddr),
39
- originalAuthor: authorName,
40
- email: emailAddr,
41
- date,
42
- message: subject,
43
- body,
44
- changeId
45
- }
46
- })
47
-
48
- // === 新增:为每个 commit 计算代码增量 ===
49
- for (const c of commits) {
50
- try {
51
- const { stdout: diffOut } =
52
- await $`git show --numstat --format= ${c.hash}`.quiet()
53
- // numstat 格式: "12 5 path/file.js"
54
- const lines = diffOut
55
- .split('\n')
56
- .map((l) => l.trim())
57
- .filter((l) => l && /^\d+\s+\d+/.test(l))
58
-
59
- let added = 0
60
- let deleted = 0
61
-
62
- for (const line of lines) {
63
- const [a, d] = line.split(/\s+/)
64
- added += parseInt(a, 10) || 0
65
- deleted += parseInt(d, 10) || 0
66
- }
67
-
68
- c.added = added
69
- c.deleted = deleted
70
- c.changed = added + deleted
71
- } catch (err) {
72
- // 避免阻塞,异常时改动量设置为0
73
- c.added = 0
74
- c.deleted = 0
75
- c.changed = 0
76
- }
77
- }
78
-
79
- return { commits, authorMap: {} }
80
- }
81
-
82
- export async function getGitLogsQuick(opts) {
83
- const { author, email, since, until, limit, merges } = opts
84
-
85
- const pretty = `${[
86
- '%H', // hash
87
- '%an', // author name
88
- '%ae', // email
89
- '%ad', // date
90
- '%s', // subject
91
- '%B' // body
92
- ].join('%x1f')}%x1e`
93
-
94
- const args = [
95
- 'log',
96
- `--pretty=format:${pretty}`,
97
- '--date=iso-local',
98
- '--numstat'
99
- ]
100
-
101
- if (author) args.push(`--author=${author}`)
102
- if (email) args.push(`--author=${email}`)
103
- if (since) args.push(`--since=${since}`)
104
- if (until) args.push(`--until=${until}`)
105
- if (merges === false) args.push(`--no-merges`)
106
- if (limit) args.push('-n', `${limit}`)
107
-
108
- const { stdout } = await $`git ${args}`.quiet()
109
-
110
- const rawCommits = stdout.split('\x1e').filter(Boolean)
111
- const commits = []
112
-
113
- for (const raw of rawCommits) {
114
- const block = raw.replace(/\r/g, '').trim()
115
- // eslint-disable-next-line no-continue
116
- if (!block) continue
117
-
118
- // header: 6 个字段用 \x1f 分隔
119
- const headerMatch = block.match(
120
- // eslint-disable-next-line no-control-regex
121
- /^([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*)$/
122
- )
123
- // eslint-disable-next-line no-continue
124
- if (!headerMatch) continue
125
-
126
- const [, hash, authorName, emailAddr, date, subject, body] = headerMatch
127
-
128
- const [, changeId] = body.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
129
-
130
- const c = {
131
- hash,
132
- author: normalizer.getAuthor(authorName, emailAddr),
133
- originalAuthor: authorName,
134
- email: emailAddr,
135
- date,
136
- message: subject,
137
- body,
138
- changeId,
139
- added: 0,
140
- deleted: 0,
141
- changed: 0,
142
- files: []
143
- }
144
-
145
- // 匹配所有 numstat 行
146
- const numstatLines = block
147
- .split('\n')
148
- .filter((l) => /^\d+\s+\d+\s+/.test(l))
149
- for (const line of numstatLines) {
150
- const [a, d, file] = line.trim().split(/\s+/)
151
- const added = parseInt(a, 10) || 0
152
- const deleted = parseInt(d, 10) || 0
153
- c.added += added
154
- c.deleted += deleted
155
- c.changed += added + deleted
156
- c.files.push({ file, added, deleted })
157
- }
158
-
159
- commits.push(c)
160
- }
161
-
162
- return {
163
- commits,
164
- authorMap: normalizer.getMap()
165
- }
166
- }
167
-
168
- /**
169
- * 高性能 git log + numstat 获取 commit
170
- */
171
- export async function getGitLogsFast(opts = {}) {
172
- const { author, email, since, until, limit, merges } = opts
173
-
174
- const pretty = `${[
175
- '%H', // hash
176
- '%an', // author name
177
- '%ae', // email
178
- '%ad', // date
179
- '%s', // subject
180
- '%B' // body
181
- ].join('%x1f')}%x1e`
182
-
183
- const args = [
184
- 'log',
185
- `--pretty=format:${pretty}`,
186
- '--date=iso-local',
187
- '--numstat'
188
- ]
189
-
190
- if (author) args.push(`--author=${author}`)
191
- if (email) args.push(`--author=${email}`)
192
- if (since) args.push(`--since=${since}`)
193
- if (until) args.push(`--until=${until}`)
194
- if (merges === false) args.push(`--no-merges`)
195
- if (limit) args.push('-n', `${limit}`)
196
-
197
- const { stdout } = await $`git ${args}`.quiet()
198
- const raw = stdout.replace(/\r/g, '')
199
-
200
- const commits = []
201
-
202
- // 匹配每个 commit header + numstat
203
- const commitRegex =
204
- // eslint-disable-next-line no-control-regex
205
- /([0-9a-f]+)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*?)(?=(?:[0-9a-f]{7,40}\x1f)|\x1e$)/g
206
- let match
207
-
208
- for (const ns of raw.matchAll(commitRegex)) {
209
- const [_, hash, authorName, emailAddr, date, subject, bodyAndNumstat] = ns
210
- const [, changeId] =
211
- bodyAndNumstat.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
212
-
213
- const c = {
214
- hash,
215
- author: normalizer.getAuthor(authorName, emailAddr),
216
- originalAuthor: authorName,
217
- email: emailAddr,
218
- date,
219
- message: subject,
220
- body: bodyAndNumstat,
221
- changeId,
222
- added: 0,
223
- deleted: 0,
224
- changed: 0,
225
- files: []
226
- }
227
-
228
- // 匹配 numstat
229
- const numstatRegex = /^(\d+)\s+(\d+)\s+(.+)$/gm
230
- for (const m of bodyAndNumstat.matchAll(numstatRegex)) {
231
- const added = parseInt(m[1], 10) || 0
232
- const deleted = parseInt(m[2], 10) || 0
233
- const file = m[3]
234
- c.added += added
235
- c.deleted += deleted
236
- c.changed += added + deleted
237
- c.files.push({ file, added, deleted })
238
- }
239
-
240
- commits.push(c)
241
- }
242
-
243
- // 最终统一覆盖 author,保证所有 commit 都使用中文名(如存在)
244
- const finalMap = normalizer.getMap()
245
- for (const c of commits) {
246
- if (c.email && finalMap[c.email]) {
247
- c.author = finalMap[c.email]
248
- }
249
- }
250
-
251
- return {
252
- commits,
253
- authorMap: finalMap,
254
- originalMap: normalizer.getOriginalMap()
255
- }
256
- }