@d3lm/pr-stats 0.1.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 +51 -0
- package/dist/index.mjs +1760 -0
- package/package.json +41 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1760 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw mod = 0, e;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
// ../../node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js
|
|
33
|
+
var require_asciichart = __commonJS({
|
|
34
|
+
"../../node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js"(exports) {
|
|
35
|
+
"use strict";
|
|
36
|
+
(function(exports2) {
|
|
37
|
+
exports2.black = "\x1B[30m";
|
|
38
|
+
exports2.red = "\x1B[31m";
|
|
39
|
+
exports2.green = "\x1B[32m";
|
|
40
|
+
exports2.yellow = "\x1B[33m";
|
|
41
|
+
exports2.blue = "\x1B[34m";
|
|
42
|
+
exports2.magenta = "\x1B[35m";
|
|
43
|
+
exports2.cyan = "\x1B[36m";
|
|
44
|
+
exports2.lightgray = "\x1B[37m";
|
|
45
|
+
exports2.default = "\x1B[39m";
|
|
46
|
+
exports2.darkgray = "\x1B[90m";
|
|
47
|
+
exports2.lightred = "\x1B[91m";
|
|
48
|
+
exports2.lightgreen = "\x1B[92m";
|
|
49
|
+
exports2.lightyellow = "\x1B[93m";
|
|
50
|
+
exports2.lightblue = "\x1B[94m";
|
|
51
|
+
exports2.lightmagenta = "\x1B[95m";
|
|
52
|
+
exports2.lightcyan = "\x1B[96m";
|
|
53
|
+
exports2.white = "\x1B[97m";
|
|
54
|
+
exports2.reset = "\x1B[0m";
|
|
55
|
+
function colored(char, color) {
|
|
56
|
+
return color === void 0 ? char : color + char + exports2.reset;
|
|
57
|
+
}
|
|
58
|
+
exports2.colored = colored;
|
|
59
|
+
exports2.plot = function(series, cfg = void 0) {
|
|
60
|
+
if (typeof series[0] == "number") {
|
|
61
|
+
series = [series];
|
|
62
|
+
}
|
|
63
|
+
cfg = typeof cfg !== "undefined" ? cfg : {};
|
|
64
|
+
let min = typeof cfg.min !== "undefined" ? cfg.min : series[0][0];
|
|
65
|
+
let max = typeof cfg.max !== "undefined" ? cfg.max : series[0][0];
|
|
66
|
+
for (let j = 0; j < series.length; j++) {
|
|
67
|
+
for (let i = 0; i < series[j].length; i++) {
|
|
68
|
+
min = Math.min(min, series[j][i]);
|
|
69
|
+
max = Math.max(max, series[j][i]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let defaultSymbols = ["\u253C", "\u2524", "\u2576", "\u2574", "\u2500", "\u2570", "\u256D", "\u256E", "\u256F", "\u2502"];
|
|
73
|
+
let range = Math.abs(max - min);
|
|
74
|
+
let offset = typeof cfg.offset !== "undefined" ? cfg.offset : 3;
|
|
75
|
+
let padding = typeof cfg.padding !== "undefined" ? cfg.padding : " ";
|
|
76
|
+
let height = typeof cfg.height !== "undefined" ? cfg.height : range;
|
|
77
|
+
let colors = typeof cfg.colors !== "undefined" ? cfg.colors : [];
|
|
78
|
+
let ratio = range !== 0 ? height / range : 1;
|
|
79
|
+
let min2 = Math.round(min * ratio);
|
|
80
|
+
let max2 = Math.round(max * ratio);
|
|
81
|
+
let rows = Math.abs(max2 - min2);
|
|
82
|
+
let width = 0;
|
|
83
|
+
for (let i = 0; i < series.length; i++) {
|
|
84
|
+
width = Math.max(width, series[i].length);
|
|
85
|
+
}
|
|
86
|
+
width = width + offset;
|
|
87
|
+
let symbols = typeof cfg.symbols !== "undefined" ? cfg.symbols : defaultSymbols;
|
|
88
|
+
let format = typeof cfg.format !== "undefined" ? cfg.format : function(x) {
|
|
89
|
+
return (padding + x.toFixed(2)).slice(-padding.length);
|
|
90
|
+
};
|
|
91
|
+
let result = new Array(rows + 1);
|
|
92
|
+
for (let i = 0; i <= rows; i++) {
|
|
93
|
+
result[i] = new Array(width);
|
|
94
|
+
for (let j = 0; j < width; j++) {
|
|
95
|
+
result[i][j] = " ";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (let y = min2; y <= max2; ++y) {
|
|
99
|
+
let label = format(rows > 0 ? max - (y - min2) * range / rows : y, y - min2);
|
|
100
|
+
result[y - min2][Math.max(offset - label.length, 0)] = label;
|
|
101
|
+
result[y - min2][offset - 1] = y == 0 ? symbols[0] : symbols[1];
|
|
102
|
+
}
|
|
103
|
+
for (let j = 0; j < series.length; j++) {
|
|
104
|
+
let currentColor = colors[j % colors.length];
|
|
105
|
+
let y0 = Math.round(series[j][0] * ratio) - min2;
|
|
106
|
+
result[rows - y0][offset - 1] = colored(symbols[0], currentColor);
|
|
107
|
+
for (let x = 0; x < series[j].length - 1; x++) {
|
|
108
|
+
let y02 = Math.round(series[j][x + 0] * ratio) - min2;
|
|
109
|
+
let y1 = Math.round(series[j][x + 1] * ratio) - min2;
|
|
110
|
+
if (y02 == y1) {
|
|
111
|
+
result[rows - y02][x + offset] = colored(symbols[4], currentColor);
|
|
112
|
+
} else {
|
|
113
|
+
result[rows - y1][x + offset] = colored(y02 > y1 ? symbols[5] : symbols[6], currentColor);
|
|
114
|
+
result[rows - y02][x + offset] = colored(y02 > y1 ? symbols[7] : symbols[8], currentColor);
|
|
115
|
+
let from = Math.min(y02, y1);
|
|
116
|
+
let to = Math.max(y02, y1);
|
|
117
|
+
for (let y = from + 1; y < to; y++) {
|
|
118
|
+
result[rows - y][x + offset] = colored(symbols[9], currentColor);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result.map(function(x) {
|
|
124
|
+
return x.join("");
|
|
125
|
+
}).join("\n");
|
|
126
|
+
};
|
|
127
|
+
})(typeof exports === "undefined" ? (
|
|
128
|
+
/* istanbul ignore next */
|
|
129
|
+
exports["asciichart"] = {}
|
|
130
|
+
) : exports);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// github.mjs
|
|
135
|
+
import { execFile } from "node:child_process";
|
|
136
|
+
import { promisify } from "node:util";
|
|
137
|
+
|
|
138
|
+
// utils.mjs
|
|
139
|
+
function fail(message) {
|
|
140
|
+
console.error(`Error: ${message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
function formatMinutesOfDay(minutes) {
|
|
144
|
+
return `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}`;
|
|
145
|
+
}
|
|
146
|
+
function percentile(sorted, percent) {
|
|
147
|
+
const index = Math.min(sorted.length - 1, Math.ceil(percent / 100 * sorted.length) - 1);
|
|
148
|
+
return sorted[Math.max(0, index)];
|
|
149
|
+
}
|
|
150
|
+
function link(text, url) {
|
|
151
|
+
if (!process.stdout.isTTY) {
|
|
152
|
+
return text;
|
|
153
|
+
}
|
|
154
|
+
return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// github.mjs
|
|
158
|
+
var execFileAsync = promisify(execFile);
|
|
159
|
+
var API_BASE = "https://api.github.com";
|
|
160
|
+
var token;
|
|
161
|
+
function configureAuth(cliToken) {
|
|
162
|
+
token = cliToken ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
163
|
+
}
|
|
164
|
+
async function gh(args) {
|
|
165
|
+
try {
|
|
166
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
167
|
+
maxBuffer: 64 * 1024 * 1024
|
|
168
|
+
});
|
|
169
|
+
return stdout;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === "ENOENT") {
|
|
172
|
+
fail("the gh CLI is not installed, install it or provide a token via --token or GITHUB_TOKEN");
|
|
173
|
+
}
|
|
174
|
+
const stderr = error.stderr?.toString().trim();
|
|
175
|
+
fail(`gh ${args.slice(0, 2).join(" ")} failed${stderr ? `
|
|
176
|
+
${stderr}` : ""}`);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function api(path, { method = "GET", body } = {}) {
|
|
181
|
+
let response;
|
|
182
|
+
try {
|
|
183
|
+
response = await fetch(`${API_BASE}${path}`, {
|
|
184
|
+
method,
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `Bearer ${token}`,
|
|
187
|
+
Accept: "application/vnd.github+json",
|
|
188
|
+
"User-Agent": "pr-stats",
|
|
189
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
190
|
+
},
|
|
191
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
fail(`cannot reach ${API_BASE} (${error.cause?.message ?? error.message})`);
|
|
195
|
+
}
|
|
196
|
+
const payload = await response.json().catch(() => null);
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const message = payload?.message ?? "";
|
|
199
|
+
const endpoint = path.split("?")[0];
|
|
200
|
+
fail(`GitHub API ${method} ${endpoint} failed with ${response.status}${message ? ` (${message})` : ""}`);
|
|
201
|
+
}
|
|
202
|
+
return payload;
|
|
203
|
+
}
|
|
204
|
+
async function runGraphql(query) {
|
|
205
|
+
if (token) {
|
|
206
|
+
const result = await api("/graphql", { method: "POST", body: { query } });
|
|
207
|
+
if (!result?.data) {
|
|
208
|
+
const message = result?.errors?.[0]?.message;
|
|
209
|
+
fail(`GitHub GraphQL query failed${message ? ` (${message})` : ""}`);
|
|
210
|
+
}
|
|
211
|
+
return result.data;
|
|
212
|
+
}
|
|
213
|
+
const stdout = await gh(["api", "graphql", "-f", `query=${query}`]);
|
|
214
|
+
return JSON.parse(stdout).data;
|
|
215
|
+
}
|
|
216
|
+
async function fetchCurrentUser() {
|
|
217
|
+
if (token) {
|
|
218
|
+
const user = await api("/user");
|
|
219
|
+
return user.login;
|
|
220
|
+
}
|
|
221
|
+
const stdout = await gh(["api", "user", "--jq", ".login"]);
|
|
222
|
+
return stdout.trim();
|
|
223
|
+
}
|
|
224
|
+
async function fetchDefaultOwner() {
|
|
225
|
+
if (!token) {
|
|
226
|
+
const stdout = await gh(["repo", "view", "--json", "owner", "--jq", ".owner.login"]);
|
|
227
|
+
return stdout.trim();
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"]);
|
|
231
|
+
const match = /github\.com[/:]([^/]+)\//.exec(stdout);
|
|
232
|
+
return match?.[1] ?? "";
|
|
233
|
+
} catch {
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function resolveRepos(repos) {
|
|
238
|
+
let defaultOwner;
|
|
239
|
+
const resolved = [];
|
|
240
|
+
for (const repo of repos) {
|
|
241
|
+
if (repo.includes("/")) {
|
|
242
|
+
resolved.push(repo);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!defaultOwner) {
|
|
246
|
+
defaultOwner = await fetchDefaultOwner();
|
|
247
|
+
if (!defaultOwner) {
|
|
248
|
+
fail(`cannot resolve owner for "--repo ${repo}", use the owner/name form`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
resolved.push(`${defaultOwner}/${repo}`);
|
|
252
|
+
}
|
|
253
|
+
return resolved;
|
|
254
|
+
}
|
|
255
|
+
async function searchPrsViaApi({ user, sinceIso, repos, includeDrafts, mode }) {
|
|
256
|
+
const qualifier = {
|
|
257
|
+
requested: "review-requested",
|
|
258
|
+
reviewed: "reviewed-by",
|
|
259
|
+
authored: "author"
|
|
260
|
+
}[mode];
|
|
261
|
+
const terms = ["type:pr", `${qualifier}:${user}`, `created:>=${sinceIso}`];
|
|
262
|
+
if (!includeDrafts) {
|
|
263
|
+
terms.push("draft:false");
|
|
264
|
+
}
|
|
265
|
+
for (const repo of repos) {
|
|
266
|
+
terms.push(`repo:${repo}`);
|
|
267
|
+
}
|
|
268
|
+
const query = encodeURIComponent(terms.join(" "));
|
|
269
|
+
const items = [];
|
|
270
|
+
for (let page = 1; page <= 10; page++) {
|
|
271
|
+
const result = await api(`/search/issues?q=${query}&per_page=100&page=${page}`);
|
|
272
|
+
items.push(...result.items);
|
|
273
|
+
if (result.items.length === 0 || items.length >= result.total_count) {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return items.map((item) => {
|
|
278
|
+
return {
|
|
279
|
+
number: item.number,
|
|
280
|
+
repository: { nameWithOwner: item.repository_url.replace(`${API_BASE}/repos/`, "") },
|
|
281
|
+
title: item.title,
|
|
282
|
+
url: item.html_url,
|
|
283
|
+
createdAt: item.created_at,
|
|
284
|
+
isDraft: item.draft,
|
|
285
|
+
state: item.state
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async function searchPrs({ user, sinceIso, repos, includeDrafts, mode }) {
|
|
290
|
+
if (token) {
|
|
291
|
+
return searchPrsViaApi({ user, sinceIso, repos, includeDrafts, mode });
|
|
292
|
+
}
|
|
293
|
+
const modeFlag = {
|
|
294
|
+
requested: "--review-requested",
|
|
295
|
+
reviewed: "--reviewed-by",
|
|
296
|
+
authored: "--author"
|
|
297
|
+
}[mode];
|
|
298
|
+
const args = [
|
|
299
|
+
"search",
|
|
300
|
+
"prs",
|
|
301
|
+
modeFlag,
|
|
302
|
+
user,
|
|
303
|
+
"--created",
|
|
304
|
+
`>=${sinceIso}`,
|
|
305
|
+
"--limit",
|
|
306
|
+
"1000",
|
|
307
|
+
"--json",
|
|
308
|
+
"number,repository,title,url,createdAt,isDraft,state"
|
|
309
|
+
];
|
|
310
|
+
if (!includeDrafts) {
|
|
311
|
+
args.push("--draft=false");
|
|
312
|
+
}
|
|
313
|
+
for (const repo of repos) {
|
|
314
|
+
args.push("--repo", repo);
|
|
315
|
+
}
|
|
316
|
+
return JSON.parse(await gh(args));
|
|
317
|
+
}
|
|
318
|
+
async function fetchPrDetails(prs) {
|
|
319
|
+
const parts = prs.map((pr, i) => {
|
|
320
|
+
const [owner, name] = pr.repo.split("/");
|
|
321
|
+
return `
|
|
322
|
+
pr${i}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) {
|
|
323
|
+
pullRequest(number: ${pr.number}) {
|
|
324
|
+
timelineItems(itemTypes: [REVIEW_REQUESTED_EVENT], first: 100) {
|
|
325
|
+
nodes {
|
|
326
|
+
... on ReviewRequestedEvent {
|
|
327
|
+
createdAt
|
|
328
|
+
requestedReviewer {
|
|
329
|
+
... on User { login }
|
|
330
|
+
... on Team { slug }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
reviews(first: 100) {
|
|
336
|
+
nodes {
|
|
337
|
+
author { login }
|
|
338
|
+
submittedAt
|
|
339
|
+
state
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}`;
|
|
344
|
+
});
|
|
345
|
+
const query = `query {${parts.join("\n")}}`;
|
|
346
|
+
const data = await runGraphql(query);
|
|
347
|
+
return prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
|
|
348
|
+
}
|
|
349
|
+
async function fetchPrSizes(prs) {
|
|
350
|
+
const parts = prs.map((pr, i) => {
|
|
351
|
+
const [owner, name] = pr.repo.split("/");
|
|
352
|
+
return `
|
|
353
|
+
pr${i}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) {
|
|
354
|
+
pullRequest(number: ${pr.number}) {
|
|
355
|
+
additions
|
|
356
|
+
deletions
|
|
357
|
+
changedFiles
|
|
358
|
+
}
|
|
359
|
+
}`;
|
|
360
|
+
});
|
|
361
|
+
const query = `query {${parts.join("\n")}}`;
|
|
362
|
+
const data = await runGraphql(query);
|
|
363
|
+
return prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// report.mjs
|
|
367
|
+
var import_asciichart = __toESM(require_asciichart(), 1);
|
|
368
|
+
|
|
369
|
+
// time.mjs
|
|
370
|
+
var timeMode = {
|
|
371
|
+
business: true,
|
|
372
|
+
workWindows: [{ startMin: 0, endMin: 24 * 60 }],
|
|
373
|
+
dayHours: 24,
|
|
374
|
+
formatter: void 0
|
|
375
|
+
};
|
|
376
|
+
function configureTimeMode({ business, workWindows, tz }) {
|
|
377
|
+
const workMinutesPerDay = workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
|
|
378
|
+
timeMode.business = business;
|
|
379
|
+
timeMode.workWindows = workWindows;
|
|
380
|
+
timeMode.dayHours = business ? workMinutesPerDay / 60 : 24;
|
|
381
|
+
timeMode.formatter = new Intl.DateTimeFormat("en-US", {
|
|
382
|
+
timeZone: tz,
|
|
383
|
+
hourCycle: "h23",
|
|
384
|
+
year: "numeric",
|
|
385
|
+
month: "2-digit",
|
|
386
|
+
day: "2-digit",
|
|
387
|
+
hour: "2-digit",
|
|
388
|
+
minute: "2-digit",
|
|
389
|
+
second: "2-digit"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function isFullDayMode() {
|
|
393
|
+
return timeMode.business && timeMode.dayHours === 24;
|
|
394
|
+
}
|
|
395
|
+
function wallParts(instantMs) {
|
|
396
|
+
const parts = Object.fromEntries(timeMode.formatter.formatToParts(instantMs).map((part) => [part.type, part.value]));
|
|
397
|
+
return {
|
|
398
|
+
year: Number(parts.year),
|
|
399
|
+
month: Number(parts.month),
|
|
400
|
+
day: Number(parts.day),
|
|
401
|
+
hour: Number(parts.hour),
|
|
402
|
+
minute: Number(parts.minute),
|
|
403
|
+
second: Number(parts.second)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function utcFromWall(wallTargetMs) {
|
|
407
|
+
let guess = wallTargetMs;
|
|
408
|
+
for (let i = 0; i < 2; i++) {
|
|
409
|
+
const parts = wallParts(guess);
|
|
410
|
+
const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
411
|
+
guess = wallTargetMs - (wall - guess);
|
|
412
|
+
}
|
|
413
|
+
return guess;
|
|
414
|
+
}
|
|
415
|
+
function businessMsBetween(start, end) {
|
|
416
|
+
const startMs = start.getTime();
|
|
417
|
+
const endMs = end.getTime();
|
|
418
|
+
if (endMs <= startMs) {
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
let total = 0;
|
|
422
|
+
const parts = wallParts(startMs);
|
|
423
|
+
let localDay = Date.UTC(parts.year, parts.month - 1, parts.day);
|
|
424
|
+
while (utcFromWall(localDay) <= endMs) {
|
|
425
|
+
const weekday = new Date(localDay).getUTCDay();
|
|
426
|
+
if (weekday !== 0 && weekday !== 6) {
|
|
427
|
+
for (const window of timeMode.workWindows) {
|
|
428
|
+
const windowStart = utcFromWall(localDay + window.startMin * 6e4);
|
|
429
|
+
const windowEnd = utcFromWall(localDay + window.endMin * 6e4);
|
|
430
|
+
const overlapStart = Math.max(windowStart, startMs);
|
|
431
|
+
const overlapEnd = Math.min(windowEnd, endMs);
|
|
432
|
+
if (overlapEnd > overlapStart) {
|
|
433
|
+
total += overlapEnd - overlapStart;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
localDay += 864e5;
|
|
438
|
+
}
|
|
439
|
+
return total;
|
|
440
|
+
}
|
|
441
|
+
function durationHours(start, end) {
|
|
442
|
+
if (!timeMode.business) {
|
|
443
|
+
return (end - start) / 36e5;
|
|
444
|
+
}
|
|
445
|
+
return businessMsBetween(start, end) / 36e5;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// report.mjs
|
|
449
|
+
var BUCKETS = [];
|
|
450
|
+
function initBuckets() {
|
|
451
|
+
BUCKETS = makeBuckets();
|
|
452
|
+
}
|
|
453
|
+
function makeBuckets() {
|
|
454
|
+
if (!timeMode.business || isFullDayMode()) {
|
|
455
|
+
return [
|
|
456
|
+
{ label: "< 1h", max: 1 },
|
|
457
|
+
{ label: "1-4h", max: 4 },
|
|
458
|
+
{ label: "4-8h", max: 8 },
|
|
459
|
+
{ label: "8-24h", max: 24 },
|
|
460
|
+
{ label: "1-2d", max: 48 },
|
|
461
|
+
{ label: "2-4d", max: 96 },
|
|
462
|
+
{ label: "4-7d", max: 168 },
|
|
463
|
+
{ label: "> 7d", max: Infinity }
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
const wd = timeMode.dayHours;
|
|
467
|
+
return [
|
|
468
|
+
{ label: "< 1h", max: 1 },
|
|
469
|
+
{ label: "1-4h", max: 4 },
|
|
470
|
+
{ label: "4h-1wd", max: wd },
|
|
471
|
+
{ label: "1-2wd", max: 2 * wd },
|
|
472
|
+
{ label: "2-3wd", max: 3 * wd },
|
|
473
|
+
{ label: "3-5wd", max: 5 * wd },
|
|
474
|
+
{ label: "5-10wd", max: 10 * wd },
|
|
475
|
+
{ label: "> 10wd", max: Infinity }
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
var LINE_BUCKETS = [
|
|
479
|
+
{ label: "< 50", max: 50 },
|
|
480
|
+
{ label: "50-100", max: 100 },
|
|
481
|
+
{ label: "100-250", max: 250 },
|
|
482
|
+
{ label: "250-500", max: 500 },
|
|
483
|
+
{ label: "500-1k", max: 1e3 },
|
|
484
|
+
{ label: "1k-2.5k", max: 2500 },
|
|
485
|
+
{ label: "2.5k-5k", max: 5e3 },
|
|
486
|
+
{ label: "> 5k", max: Infinity }
|
|
487
|
+
];
|
|
488
|
+
var FILE_BUCKETS = [
|
|
489
|
+
{ label: "1-2", max: 3 },
|
|
490
|
+
{ label: "3-5", max: 6 },
|
|
491
|
+
{ label: "6-10", max: 11 },
|
|
492
|
+
{ label: "11-20", max: 21 },
|
|
493
|
+
{ label: "21-50", max: 51 },
|
|
494
|
+
{ label: "> 50", max: Infinity }
|
|
495
|
+
];
|
|
496
|
+
function formatHours(hours) {
|
|
497
|
+
if (hours < 1) {
|
|
498
|
+
return `${Math.round(hours * 60)}m`;
|
|
499
|
+
}
|
|
500
|
+
if (hours < 2 * timeMode.dayHours) {
|
|
501
|
+
return `${hours.toFixed(1)}h`;
|
|
502
|
+
}
|
|
503
|
+
return `${(hours / timeMode.dayHours).toFixed(1)}${timeMode.business && !isFullDayMode() ? "wd" : "d"}`;
|
|
504
|
+
}
|
|
505
|
+
function formatHoursOnly(hours) {
|
|
506
|
+
if (hours < 1) {
|
|
507
|
+
return `${Math.round(hours * 60)}m`;
|
|
508
|
+
}
|
|
509
|
+
return `${hours.toFixed(1)}h`;
|
|
510
|
+
}
|
|
511
|
+
function weeksSuffix(hours) {
|
|
512
|
+
const weekHours = timeMode.business ? 5 * timeMode.dayHours : 7 * 24;
|
|
513
|
+
if (hours < weekHours) {
|
|
514
|
+
return "";
|
|
515
|
+
}
|
|
516
|
+
return ` (${(hours / weekHours).toFixed(1)} weeks)`;
|
|
517
|
+
}
|
|
518
|
+
function printHistogram(title, values, buckets = BUCKETS) {
|
|
519
|
+
const counts = buckets.map(() => 0);
|
|
520
|
+
for (const value2 of values) {
|
|
521
|
+
counts[buckets.findIndex((bucket) => value2 < bucket.max)] += 1;
|
|
522
|
+
}
|
|
523
|
+
const maxCount = Math.max(...counts, 1);
|
|
524
|
+
const labelWidth = Math.max(...buckets.map((bucket) => bucket.label.length));
|
|
525
|
+
console.info(`
|
|
526
|
+
${title}`);
|
|
527
|
+
for (const [i, bucket] of buckets.entries()) {
|
|
528
|
+
const bar = "\u2588".repeat(Math.round(counts[i] / maxCount * 40)) || (counts[i] > 0 ? "\u258F" : "");
|
|
529
|
+
const pct = values.length > 0 ? Math.round(counts[i] / values.length * 100) : 0;
|
|
530
|
+
console.info(` ${bucket.label.padEnd(labelWidth)} ${bar.padEnd(40)} ${String(counts[i]).padStart(4)} (${pct}%)`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function printStats(hoursList, targetHours, targetLabel, pendingHours = []) {
|
|
534
|
+
const sorted = [...hoursList].toSorted((a, b) => a - b);
|
|
535
|
+
const mean = sorted.reduce((sum, hours) => sum + hours, 0) / sorted.length;
|
|
536
|
+
console.info(
|
|
537
|
+
`
|
|
538
|
+
min ${formatHours(sorted[0])} | mean ${formatHours(mean)} | p50 ${formatHours(percentile(sorted, 50))} | p90 ${formatHours(percentile(sorted, 90))} | p99 ${formatHours(percentile(sorted, 99))} | max ${formatHours(sorted.at(-1))}`
|
|
539
|
+
);
|
|
540
|
+
if (targetHours !== void 0) {
|
|
541
|
+
const overdue = pendingHours.filter((hours) => hours > targetHours).length;
|
|
542
|
+
const met = sorted.filter((hours) => hours <= targetHours).length;
|
|
543
|
+
const total = sorted.length + overdue;
|
|
544
|
+
const pct = met / total * 100;
|
|
545
|
+
const filled = Math.round(pct / 100 * 30);
|
|
546
|
+
const gauge = "\u2588".repeat(filled) + "\u2591".repeat(30 - filled);
|
|
547
|
+
console.info(
|
|
548
|
+
` target <= ${targetLabel} ${gauge} ${pct.toFixed(0)}% met (${met}/${total}${overdue > 0 ? `, ${overdue} awaiting review and already over` : ""})`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function printSizeStats(title, metrics) {
|
|
553
|
+
const rows = metrics.map(({ label, values }) => {
|
|
554
|
+
const sorted = [...values].toSorted((a, b) => a - b);
|
|
555
|
+
const mean = sorted.reduce((sum, value2) => sum + value2, 0) / sorted.length;
|
|
556
|
+
return {
|
|
557
|
+
label,
|
|
558
|
+
cells: [
|
|
559
|
+
["min", String(sorted[0])],
|
|
560
|
+
["mean", String(Math.round(mean))],
|
|
561
|
+
["p50", String(percentile(sorted, 50))],
|
|
562
|
+
["p90", String(percentile(sorted, 90))],
|
|
563
|
+
["p99", String(percentile(sorted, 99))],
|
|
564
|
+
["max", String(sorted.at(-1))]
|
|
565
|
+
]
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
569
|
+
const columnWidths = rows[0].cells.map((_, i) => Math.max(...rows.map((row) => row.cells[i][1].length)));
|
|
570
|
+
console.info(`
|
|
571
|
+
${title}`);
|
|
572
|
+
for (const row of rows) {
|
|
573
|
+
const cells = row.cells.map(([name, value2], i) => `${name} ${value2.padStart(columnWidths[i])}`).join(" | ");
|
|
574
|
+
console.info(` ${row.label.padEnd(labelWidth)} ${cells}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function printQuantileStrips(title, metrics, width = 40) {
|
|
578
|
+
const rows = metrics.map(({ label, values }) => {
|
|
579
|
+
const sorted = [...values].toSorted((a, b) => a - b);
|
|
580
|
+
const min = sorted[0];
|
|
581
|
+
const max = sorted.at(-1);
|
|
582
|
+
const span = Math.log1p(max) - Math.log1p(min);
|
|
583
|
+
const position = (value2) => {
|
|
584
|
+
if (span === 0) {
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
return Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (width - 1));
|
|
588
|
+
};
|
|
589
|
+
const cells = Array.from({ length: width }, () => "\u2500");
|
|
590
|
+
cells[0] = "\u251C";
|
|
591
|
+
cells[width - 1] = "\u2524";
|
|
592
|
+
for (let i = position(percentile(sorted, 25)); i <= position(percentile(sorted, 75)); i++) {
|
|
593
|
+
cells[i] = "\u2592";
|
|
594
|
+
}
|
|
595
|
+
cells[position(percentile(sorted, 50))] = "\u2588";
|
|
596
|
+
return { label, strip: cells.join(""), min: String(min), max: String(max) };
|
|
597
|
+
});
|
|
598
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
599
|
+
const minWidth = Math.max(...rows.map((row) => row.min.length));
|
|
600
|
+
console.info(`
|
|
601
|
+
${title}`);
|
|
602
|
+
for (const row of rows) {
|
|
603
|
+
console.info(` ${row.label.padEnd(labelWidth)} ${row.min.padStart(minWidth)} ${row.strip} ${row.max}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function formatCount(value2) {
|
|
607
|
+
if (value2 < 1e3) {
|
|
608
|
+
return String(value2);
|
|
609
|
+
}
|
|
610
|
+
const scaled = value2 / 1e3;
|
|
611
|
+
return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
|
|
612
|
+
}
|
|
613
|
+
function printSizeTimeline(values) {
|
|
614
|
+
const maxPoints = Math.max(40, Math.min((process.stdout.columns ?? 90) - 12, 100));
|
|
615
|
+
let series = values;
|
|
616
|
+
let chunk = 1;
|
|
617
|
+
if (values.length > maxPoints) {
|
|
618
|
+
chunk = Math.ceil(values.length / maxPoints);
|
|
619
|
+
series = [];
|
|
620
|
+
for (let offset = 0; offset < values.length; offset += chunk) {
|
|
621
|
+
const slice = values.slice(offset, offset + chunk);
|
|
622
|
+
series.push(slice.reduce((sum, value2) => sum + value2, 0) / slice.length);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const stretch = Math.floor(maxPoints / series.length);
|
|
626
|
+
if (stretch > 1) {
|
|
627
|
+
series = series.flatMap((value2) => Array.from({ length: stretch }, () => value2));
|
|
628
|
+
}
|
|
629
|
+
const chart = import_asciichart.default.plot(
|
|
630
|
+
series.map((value2) => Math.log10(value2 + 1)),
|
|
631
|
+
{
|
|
632
|
+
height: 10,
|
|
633
|
+
format: (x) => formatCount(Math.round(10 ** x - 1)).padStart(7)
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
console.info(
|
|
637
|
+
`
|
|
638
|
+
Lines changed per authored PR over time (n=${values.length}, log scale${chunk > 1 ? `, each point averages ${chunk} PRs` : ""})`
|
|
639
|
+
);
|
|
640
|
+
console.info(
|
|
641
|
+
chart.split("\n").map((line) => ` ${line}`).join("\n")
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// analyze.mjs
|
|
646
|
+
function analyzePr(pr, details, user) {
|
|
647
|
+
if (!details) {
|
|
648
|
+
return { kind: "inaccessible", pr };
|
|
649
|
+
}
|
|
650
|
+
const requests = details.timelineItems.nodes.filter((node) => node?.requestedReviewer?.login === user).map((node) => new Date(node.createdAt)).toSorted((a, b) => a - b);
|
|
651
|
+
const reviews = details.reviews.nodes.filter((node) => node?.author?.login === user && node.submittedAt).map((node) => new Date(node.submittedAt)).toSorted((a, b) => a - b);
|
|
652
|
+
if (requests.length === 0) {
|
|
653
|
+
return reviews.length > 0 ? { kind: "unrequested", pr } : { kind: "inaccessible", pr };
|
|
654
|
+
}
|
|
655
|
+
const firstRequest = requests[0];
|
|
656
|
+
const firstReviewAfterRequest = reviews.find((review) => review >= firstRequest);
|
|
657
|
+
if (!firstReviewAfterRequest) {
|
|
658
|
+
return { kind: "pending", pr, requestedAt: firstRequest };
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
kind: "reviewed",
|
|
662
|
+
pr,
|
|
663
|
+
hours: durationHours(firstRequest, firstReviewAfterRequest)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async function analyzeReviewTimes({ requested, reviewed, user, targetHours, targetLabel }) {
|
|
667
|
+
const prByKey = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const item of [...requested, ...reviewed]) {
|
|
669
|
+
const repo = item.repository.nameWithOwner;
|
|
670
|
+
prByKey.set(`${repo}#${item.number}`, {
|
|
671
|
+
repo,
|
|
672
|
+
number: item.number,
|
|
673
|
+
title: item.title,
|
|
674
|
+
url: item.url,
|
|
675
|
+
state: item.state
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const prs = [...prByKey.values()];
|
|
679
|
+
if (prs.length === 0) {
|
|
680
|
+
console.info("No reviewed or review-requested PRs found.");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (requested.length >= 1e3 || reviewed.length >= 1e3) {
|
|
684
|
+
console.warn("Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow --since or --repo.");
|
|
685
|
+
}
|
|
686
|
+
console.info(`Found ${prs.length} PRs, fetching review timelines...`);
|
|
687
|
+
const results = [];
|
|
688
|
+
const batchSize = 25;
|
|
689
|
+
for (let offset = 0; offset < prs.length; offset += batchSize) {
|
|
690
|
+
const batch = prs.slice(offset, offset + batchSize);
|
|
691
|
+
const detailsList = await fetchPrDetails(batch);
|
|
692
|
+
for (const [i, element] of batch.entries()) {
|
|
693
|
+
results.push(analyzePr(element, detailsList[i], user));
|
|
694
|
+
}
|
|
695
|
+
process.stdout.write(`\r ${Math.min(offset + batchSize, prs.length)}/${prs.length}`);
|
|
696
|
+
}
|
|
697
|
+
process.stdout.write("\n");
|
|
698
|
+
const reviewedResults = results.filter((result) => result.kind === "reviewed");
|
|
699
|
+
const pending = results.filter((result) => result.kind === "pending" && result.pr.state === "open");
|
|
700
|
+
const now = /* @__PURE__ */ new Date();
|
|
701
|
+
for (const result of pending) {
|
|
702
|
+
result.hours = durationHours(result.requestedAt, now);
|
|
703
|
+
}
|
|
704
|
+
const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
|
|
705
|
+
const unrequested = results.filter((result) => result.kind === "unrequested");
|
|
706
|
+
console.info(
|
|
707
|
+
`
|
|
708
|
+
${reviewedResults.length} reviewed after a direct request, ${pending.length} open and awaiting your review, ${expired.length} closed or merged without your review, ${unrequested.length} reviewed without a direct request (excluded)`
|
|
709
|
+
);
|
|
710
|
+
if (reviewedResults.length === 0) {
|
|
711
|
+
console.info("Nothing to chart.");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const allHours = reviewedResults.map((result) => result.hours);
|
|
715
|
+
const pendingHours = pending.map((result) => result.hours);
|
|
716
|
+
printHistogram(`Time to review, all repos (n=${allHours.length})`, allHours);
|
|
717
|
+
printStats(allHours, targetHours, targetLabel, pendingHours);
|
|
718
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
719
|
+
for (const result of reviewedResults) {
|
|
720
|
+
if (!byRepo.has(result.pr.repo)) {
|
|
721
|
+
byRepo.set(result.pr.repo, []);
|
|
722
|
+
}
|
|
723
|
+
byRepo.get(result.pr.repo).push(result.hours);
|
|
724
|
+
}
|
|
725
|
+
if (byRepo.size > 1) {
|
|
726
|
+
const sortedByRepo = [...byRepo.entries()].toSorted((a, b) => {
|
|
727
|
+
return b[1].length - a[1].length;
|
|
728
|
+
});
|
|
729
|
+
for (const [repo, hoursList] of sortedByRepo) {
|
|
730
|
+
printHistogram(`Time to review, ${repo} (n=${hoursList.length})`, hoursList);
|
|
731
|
+
const repoPendingHours = pending.filter((result) => result.pr.repo === repo).map((result) => result.hours);
|
|
732
|
+
printStats(hoursList, targetHours, targetLabel, repoPendingHours);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (targetHours !== void 0) {
|
|
736
|
+
const misses = reviewedResults.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
|
|
737
|
+
if (misses.length > 0) {
|
|
738
|
+
console.info(`
|
|
739
|
+
Reviews that missed the <= ${targetLabel} target`);
|
|
740
|
+
const rows = misses.map((result) => {
|
|
741
|
+
return {
|
|
742
|
+
result,
|
|
743
|
+
duration: `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`
|
|
744
|
+
};
|
|
745
|
+
});
|
|
746
|
+
const width = Math.max(...rows.map((row) => row.duration.length));
|
|
747
|
+
for (const { result, duration } of rows) {
|
|
748
|
+
const ref = link(`${result.pr.repo}#${result.pr.number}`, result.pr.url);
|
|
749
|
+
console.info(` ${duration.padEnd(width)} ${ref} ${result.pr.title}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (pending.length > 0) {
|
|
754
|
+
console.info("\nOpen and awaiting your review");
|
|
755
|
+
const rows = pending.toSorted((a, b) => a.requestedAt - b.requestedAt).map((result) => {
|
|
756
|
+
return {
|
|
757
|
+
result,
|
|
758
|
+
duration: `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
const width = Math.max(...rows.map((row) => row.duration.length));
|
|
762
|
+
for (const { result, duration } of rows) {
|
|
763
|
+
const ref = link(`${result.pr.repo}#${result.pr.number}`, result.pr.url);
|
|
764
|
+
console.info(` ${duration.padEnd(width)} ${ref} ${result.pr.title}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function analyzeAuthoredSizes({ authored, sizeTarget, charts }) {
|
|
769
|
+
const prs = authored.map((item) => {
|
|
770
|
+
return {
|
|
771
|
+
repo: item.repository.nameWithOwner,
|
|
772
|
+
number: item.number,
|
|
773
|
+
title: item.title,
|
|
774
|
+
url: item.url,
|
|
775
|
+
createdAt: new Date(item.createdAt)
|
|
776
|
+
};
|
|
777
|
+
});
|
|
778
|
+
if (prs.length === 0) {
|
|
779
|
+
console.info("\nNo authored PRs found.");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (authored.length >= 1e3) {
|
|
783
|
+
console.warn("Warning, the authored search hit the 1000 result cap, so data may be incomplete.");
|
|
784
|
+
}
|
|
785
|
+
console.info(`
|
|
786
|
+
Found ${prs.length} authored PRs, fetching sizes...`);
|
|
787
|
+
const sizes = [];
|
|
788
|
+
const batchSize = 25;
|
|
789
|
+
for (let offset = 0; offset < prs.length; offset += batchSize) {
|
|
790
|
+
const batch = prs.slice(offset, offset + batchSize);
|
|
791
|
+
const detailsList = await fetchPrSizes(batch);
|
|
792
|
+
for (const [i, pr] of batch.entries()) {
|
|
793
|
+
const details = detailsList[i];
|
|
794
|
+
if (details) {
|
|
795
|
+
sizes.push({
|
|
796
|
+
pr,
|
|
797
|
+
files: details.changedFiles,
|
|
798
|
+
additions: details.additions,
|
|
799
|
+
deletions: details.deletions,
|
|
800
|
+
total: details.additions + details.deletions
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
process.stdout.write(`\r ${Math.min(offset + batchSize, prs.length)}/${prs.length}`);
|
|
805
|
+
}
|
|
806
|
+
process.stdout.write("\n");
|
|
807
|
+
if (sizes.length === 0) {
|
|
808
|
+
console.info("No accessible authored PRs to analyze.");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (sizes.length < prs.length) {
|
|
812
|
+
console.info(`${prs.length - sizes.length} authored PRs were inaccessible and are excluded.`);
|
|
813
|
+
}
|
|
814
|
+
const metrics = [
|
|
815
|
+
{ label: "files changed", values: sizes.map((size) => size.files) },
|
|
816
|
+
{ label: "lines added", values: sizes.map((size) => size.additions) },
|
|
817
|
+
{ label: "lines removed", values: sizes.map((size) => size.deletions) },
|
|
818
|
+
{ label: "lines total", values: sizes.map((size) => size.total) }
|
|
819
|
+
];
|
|
820
|
+
printSizeStats(`Authored PR sizes (n=${sizes.length})`, metrics);
|
|
821
|
+
const meetsTarget = (size) => (sizeTarget.lines === void 0 || size.total <= sizeTarget.lines) && (sizeTarget.files === void 0 || size.files <= sizeTarget.files);
|
|
822
|
+
if (sizeTarget !== void 0) {
|
|
823
|
+
const targetLabel = [
|
|
824
|
+
...sizeTarget.lines === void 0 ? [] : [`<= ${sizeTarget.lines} lines`],
|
|
825
|
+
...sizeTarget.files === void 0 ? [] : [`<= ${sizeTarget.files} files`]
|
|
826
|
+
].join(", ");
|
|
827
|
+
const met = sizes.filter((size) => meetsTarget(size)).length;
|
|
828
|
+
const pct = met / sizes.length * 100;
|
|
829
|
+
const filled = Math.round(pct / 100 * 30);
|
|
830
|
+
const gauge = "\u2588".repeat(filled) + "\u2591".repeat(30 - filled);
|
|
831
|
+
console.info(` target ${targetLabel} ${gauge} ${pct.toFixed(0)}% met (${met}/${sizes.length})`);
|
|
832
|
+
}
|
|
833
|
+
if (charts.has("histogram")) {
|
|
834
|
+
printHistogram(
|
|
835
|
+
`Authored PR sizes, lines total (n=${sizes.length})`,
|
|
836
|
+
sizes.map((size) => size.total),
|
|
837
|
+
LINE_BUCKETS
|
|
838
|
+
);
|
|
839
|
+
printHistogram(
|
|
840
|
+
`Authored PR sizes, files changed (n=${sizes.length})`,
|
|
841
|
+
sizes.map((size) => size.files),
|
|
842
|
+
FILE_BUCKETS
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (charts.has("strip")) {
|
|
846
|
+
printQuantileStrips("Authored PR size spread, log scale (\u2592 p25-p75, \u2588 p50)", metrics);
|
|
847
|
+
}
|
|
848
|
+
if (charts.has("spark")) {
|
|
849
|
+
const chronological = [...sizes].toSorted((a, b) => a.pr.createdAt - b.pr.createdAt);
|
|
850
|
+
printSizeTimeline(chronological.map((size) => size.total));
|
|
851
|
+
}
|
|
852
|
+
if (sizeTarget === void 0) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
|
|
856
|
+
if (misses.length === 0) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
console.info("\nAuthored PRs over the size target");
|
|
860
|
+
const rows = misses.map((size) => {
|
|
861
|
+
return {
|
|
862
|
+
size,
|
|
863
|
+
desc: `+${size.additions}/-${size.deletions}, ${size.files} files`
|
|
864
|
+
};
|
|
865
|
+
});
|
|
866
|
+
const width = Math.max(...rows.map((row) => row.desc.length));
|
|
867
|
+
for (const { size, desc } of rows) {
|
|
868
|
+
const ref = link(`${size.pr.repo}#${size.pr.number}`, size.pr.url);
|
|
869
|
+
console.info(` ${desc.padEnd(width)} ${ref} ${size.pr.title}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
|
|
874
|
+
function stringReplaceAll(string, substring, postfix) {
|
|
875
|
+
let index = string.indexOf(substring);
|
|
876
|
+
if (index === -1) {
|
|
877
|
+
return string;
|
|
878
|
+
}
|
|
879
|
+
const substringLength = substring.length;
|
|
880
|
+
let endIndex = 0;
|
|
881
|
+
let returnValue = "";
|
|
882
|
+
do {
|
|
883
|
+
returnValue += string.slice(endIndex, index) + substring + postfix;
|
|
884
|
+
endIndex = index + substringLength;
|
|
885
|
+
index = string.indexOf(substring, endIndex);
|
|
886
|
+
} while (index !== -1);
|
|
887
|
+
returnValue += string.slice(endIndex);
|
|
888
|
+
return returnValue;
|
|
889
|
+
}
|
|
890
|
+
function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
891
|
+
let endIndex = 0;
|
|
892
|
+
let returnValue = "";
|
|
893
|
+
do {
|
|
894
|
+
const isGotCR = string[index - 1] === "\r";
|
|
895
|
+
returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? "\r\n" : "\n") + postfix;
|
|
896
|
+
endIndex = index + 1;
|
|
897
|
+
index = string.indexOf("\n", endIndex);
|
|
898
|
+
} while (index !== -1);
|
|
899
|
+
returnValue += string.slice(endIndex);
|
|
900
|
+
return returnValue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
904
|
+
var ANSI_BACKGROUND_OFFSET = 10;
|
|
905
|
+
var ANSI_UNDERLINE_OFFSET = 20;
|
|
906
|
+
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
907
|
+
var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
|
|
908
|
+
var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
|
|
909
|
+
var wrapUnderlineAnsi = (code) => `\x1B[58;5;${code < 90 ? code - 30 : code - 90 + 8}m`;
|
|
910
|
+
var styles = {
|
|
911
|
+
modifier: {
|
|
912
|
+
reset: [0, 0],
|
|
913
|
+
// 21 isn't widely supported and 22 does the same thing
|
|
914
|
+
bold: [1, 22],
|
|
915
|
+
dim: [2, 22],
|
|
916
|
+
italic: [3, 23],
|
|
917
|
+
underline: [4, 24],
|
|
918
|
+
// Extended underline styles (`SGR 4:x` sub-parameters). Not in upstream `ansi-styles`.
|
|
919
|
+
underlineDouble: ["4:2", 24],
|
|
920
|
+
underlineCurly: ["4:3", 24],
|
|
921
|
+
underlineDotted: ["4:4", 24],
|
|
922
|
+
underlineDashed: ["4:5", 24],
|
|
923
|
+
overline: [53, 55],
|
|
924
|
+
inverse: [7, 27],
|
|
925
|
+
hidden: [8, 28],
|
|
926
|
+
strikethrough: [9, 29]
|
|
927
|
+
},
|
|
928
|
+
color: {
|
|
929
|
+
black: [30, 39],
|
|
930
|
+
red: [31, 39],
|
|
931
|
+
green: [32, 39],
|
|
932
|
+
yellow: [33, 39],
|
|
933
|
+
blue: [34, 39],
|
|
934
|
+
magenta: [35, 39],
|
|
935
|
+
cyan: [36, 39],
|
|
936
|
+
white: [37, 39],
|
|
937
|
+
// Bright color
|
|
938
|
+
blackBright: [90, 39],
|
|
939
|
+
gray: [90, 39],
|
|
940
|
+
// Alias of `blackBright`
|
|
941
|
+
grey: [90, 39],
|
|
942
|
+
// Alias of `blackBright`
|
|
943
|
+
redBright: [91, 39],
|
|
944
|
+
greenBright: [92, 39],
|
|
945
|
+
yellowBright: [93, 39],
|
|
946
|
+
blueBright: [94, 39],
|
|
947
|
+
magentaBright: [95, 39],
|
|
948
|
+
cyanBright: [96, 39],
|
|
949
|
+
whiteBright: [97, 39]
|
|
950
|
+
},
|
|
951
|
+
bgColor: {
|
|
952
|
+
bgBlack: [40, 49],
|
|
953
|
+
bgRed: [41, 49],
|
|
954
|
+
bgGreen: [42, 49],
|
|
955
|
+
bgYellow: [43, 49],
|
|
956
|
+
bgBlue: [44, 49],
|
|
957
|
+
bgMagenta: [45, 49],
|
|
958
|
+
bgCyan: [46, 49],
|
|
959
|
+
bgWhite: [47, 49],
|
|
960
|
+
// Bright color
|
|
961
|
+
bgBlackBright: [100, 49],
|
|
962
|
+
bgGray: [100, 49],
|
|
963
|
+
// Alias of `bgBlackBright`
|
|
964
|
+
bgGrey: [100, 49],
|
|
965
|
+
// Alias of `bgBlackBright`
|
|
966
|
+
bgRedBright: [101, 49],
|
|
967
|
+
bgGreenBright: [102, 49],
|
|
968
|
+
bgYellowBright: [103, 49],
|
|
969
|
+
bgBlueBright: [104, 49],
|
|
970
|
+
bgMagentaBright: [105, 49],
|
|
971
|
+
bgCyanBright: [106, 49],
|
|
972
|
+
bgWhiteBright: [107, 49]
|
|
973
|
+
},
|
|
974
|
+
// Underline color (`SGR 58`/`59`). Not in upstream `ansi-styles`.
|
|
975
|
+
underlineColor: {
|
|
976
|
+
underlineBlack: ["58;5;0", 59],
|
|
977
|
+
underlineRed: ["58;5;1", 59],
|
|
978
|
+
underlineGreen: ["58;5;2", 59],
|
|
979
|
+
underlineYellow: ["58;5;3", 59],
|
|
980
|
+
underlineBlue: ["58;5;4", 59],
|
|
981
|
+
underlineMagenta: ["58;5;5", 59],
|
|
982
|
+
underlineCyan: ["58;5;6", 59],
|
|
983
|
+
underlineWhite: ["58;5;7", 59],
|
|
984
|
+
// Bright color
|
|
985
|
+
underlineBlackBright: ["58;5;8", 59],
|
|
986
|
+
underlineGray: ["58;5;8", 59],
|
|
987
|
+
// Alias of `underlineBlackBright`
|
|
988
|
+
underlineGrey: ["58;5;8", 59],
|
|
989
|
+
// Alias of `underlineBlackBright`
|
|
990
|
+
underlineRedBright: ["58;5;9", 59],
|
|
991
|
+
underlineGreenBright: ["58;5;10", 59],
|
|
992
|
+
underlineYellowBright: ["58;5;11", 59],
|
|
993
|
+
underlineBlueBright: ["58;5;12", 59],
|
|
994
|
+
underlineMagentaBright: ["58;5;13", 59],
|
|
995
|
+
underlineCyanBright: ["58;5;14", 59],
|
|
996
|
+
underlineWhiteBright: ["58;5;15", 59]
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
var modifierNames = Object.keys(styles.modifier);
|
|
1000
|
+
var foregroundColorNames = Object.keys(styles.color);
|
|
1001
|
+
var backgroundColorNames = Object.keys(styles.bgColor);
|
|
1002
|
+
var underlineColorNames = Object.keys(styles.underlineColor);
|
|
1003
|
+
var colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
|
1004
|
+
function assembleStyles() {
|
|
1005
|
+
const codes = /* @__PURE__ */ new Map();
|
|
1006
|
+
for (const [groupName, group] of Object.entries(styles)) {
|
|
1007
|
+
for (const [styleName, style] of Object.entries(group)) {
|
|
1008
|
+
styles[styleName] = {
|
|
1009
|
+
open: `\x1B[${style[0]}m`,
|
|
1010
|
+
close: `\x1B[${style[1]}m`
|
|
1011
|
+
};
|
|
1012
|
+
group[styleName] = styles[styleName];
|
|
1013
|
+
codes.set(Number.parseInt(style[0], 10), style[1]);
|
|
1014
|
+
}
|
|
1015
|
+
Object.defineProperty(styles, groupName, {
|
|
1016
|
+
value: group,
|
|
1017
|
+
enumerable: false
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
Object.defineProperty(styles, "codes", {
|
|
1021
|
+
value: codes,
|
|
1022
|
+
enumerable: false
|
|
1023
|
+
});
|
|
1024
|
+
styles.color.close = "\x1B[39m";
|
|
1025
|
+
styles.bgColor.close = "\x1B[49m";
|
|
1026
|
+
styles.underlineColor.close = "\x1B[59m";
|
|
1027
|
+
styles.color.ansi = wrapAnsi16();
|
|
1028
|
+
styles.color.ansi256 = wrapAnsi256();
|
|
1029
|
+
styles.color.ansi16m = wrapAnsi16m();
|
|
1030
|
+
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
|
1031
|
+
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
|
1032
|
+
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
|
1033
|
+
styles.underlineColor.ansi = wrapUnderlineAnsi;
|
|
1034
|
+
styles.underlineColor.ansi256 = wrapAnsi256(ANSI_UNDERLINE_OFFSET);
|
|
1035
|
+
styles.underlineColor.ansi16m = wrapAnsi16m(ANSI_UNDERLINE_OFFSET);
|
|
1036
|
+
Object.defineProperties(styles, {
|
|
1037
|
+
rgbToAnsi256: {
|
|
1038
|
+
value(red, green, blue) {
|
|
1039
|
+
if (red === green && green === blue) {
|
|
1040
|
+
if (red < 8) {
|
|
1041
|
+
return 16;
|
|
1042
|
+
}
|
|
1043
|
+
if (red > 248) {
|
|
1044
|
+
return 231;
|
|
1045
|
+
}
|
|
1046
|
+
return Math.round((red - 8) / 247 * 24) + 232;
|
|
1047
|
+
}
|
|
1048
|
+
return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
|
|
1049
|
+
},
|
|
1050
|
+
enumerable: false
|
|
1051
|
+
},
|
|
1052
|
+
hexToRgb: {
|
|
1053
|
+
value(hex) {
|
|
1054
|
+
const matches = /[\da-f]{6}|[\da-f]{3}/i.exec(hex.toString(16));
|
|
1055
|
+
if (!matches) {
|
|
1056
|
+
return [0, 0, 0];
|
|
1057
|
+
}
|
|
1058
|
+
let [colorString] = matches;
|
|
1059
|
+
if (colorString.length === 3) {
|
|
1060
|
+
colorString = [...colorString].map((character) => character + character).join("");
|
|
1061
|
+
}
|
|
1062
|
+
const integer = Number.parseInt(colorString, 16);
|
|
1063
|
+
return [
|
|
1064
|
+
/* eslint-disable no-bitwise -- We need the speed */
|
|
1065
|
+
integer >> 16 & 255,
|
|
1066
|
+
integer >> 8 & 255,
|
|
1067
|
+
integer & 255
|
|
1068
|
+
/* eslint-enable no-bitwise */
|
|
1069
|
+
];
|
|
1070
|
+
},
|
|
1071
|
+
enumerable: false
|
|
1072
|
+
},
|
|
1073
|
+
hexToAnsi256: {
|
|
1074
|
+
value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
|
1075
|
+
enumerable: false
|
|
1076
|
+
},
|
|
1077
|
+
ansi256ToAnsi: {
|
|
1078
|
+
value(code) {
|
|
1079
|
+
if (code < 8) {
|
|
1080
|
+
return 30 + code;
|
|
1081
|
+
}
|
|
1082
|
+
if (code < 16) {
|
|
1083
|
+
return 90 + (code - 8);
|
|
1084
|
+
}
|
|
1085
|
+
let red;
|
|
1086
|
+
let green;
|
|
1087
|
+
let blue;
|
|
1088
|
+
if (code >= 232) {
|
|
1089
|
+
red = ((code - 232) * 10 + 8) / 255;
|
|
1090
|
+
green = red;
|
|
1091
|
+
blue = red;
|
|
1092
|
+
} else {
|
|
1093
|
+
code -= 16;
|
|
1094
|
+
const remainder = code % 36;
|
|
1095
|
+
red = Math.floor(code / 36) / 5;
|
|
1096
|
+
green = Math.floor(remainder / 6) / 5;
|
|
1097
|
+
blue = remainder % 6 / 5;
|
|
1098
|
+
}
|
|
1099
|
+
const value2 = Math.max(red, green, blue) * 2;
|
|
1100
|
+
if (value2 === 0) {
|
|
1101
|
+
return 30;
|
|
1102
|
+
}
|
|
1103
|
+
let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
|
|
1104
|
+
if (value2 === 2) {
|
|
1105
|
+
result += 60;
|
|
1106
|
+
}
|
|
1107
|
+
return result;
|
|
1108
|
+
},
|
|
1109
|
+
enumerable: false
|
|
1110
|
+
},
|
|
1111
|
+
rgbToAnsi: {
|
|
1112
|
+
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
|
1113
|
+
enumerable: false
|
|
1114
|
+
},
|
|
1115
|
+
hexToAnsi: {
|
|
1116
|
+
value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
|
1117
|
+
enumerable: false
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
return styles;
|
|
1121
|
+
}
|
|
1122
|
+
var ansiStyles = assembleStyles();
|
|
1123
|
+
var ansi_styles_default = ansiStyles;
|
|
1124
|
+
|
|
1125
|
+
// ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/supports-color/index.js
|
|
1126
|
+
import process2 from "node:process";
|
|
1127
|
+
import os from "node:os";
|
|
1128
|
+
import tty from "node:tty";
|
|
1129
|
+
function hasFlag(flag2, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
|
|
1130
|
+
const prefix = flag2.startsWith("-") ? "" : flag2.length === 1 ? "-" : "--";
|
|
1131
|
+
const position = argv.indexOf(prefix + flag2);
|
|
1132
|
+
const terminatorPosition = argv.indexOf("--");
|
|
1133
|
+
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
|
1134
|
+
}
|
|
1135
|
+
var { env } = process2;
|
|
1136
|
+
var flagForceColor;
|
|
1137
|
+
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
|
|
1138
|
+
flagForceColor = 0;
|
|
1139
|
+
} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
|
|
1140
|
+
flagForceColor = 1;
|
|
1141
|
+
}
|
|
1142
|
+
function hasNumericForceColor() {
|
|
1143
|
+
return /^\d+$/.test(env.FORCE_COLOR);
|
|
1144
|
+
}
|
|
1145
|
+
function envForceColor() {
|
|
1146
|
+
if (!("FORCE_COLOR" in env)) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (env.FORCE_COLOR === "false") {
|
|
1150
|
+
return 0;
|
|
1151
|
+
}
|
|
1152
|
+
if (env.FORCE_COLOR === "true" || env.FORCE_COLOR.length === 0) {
|
|
1153
|
+
return 1;
|
|
1154
|
+
}
|
|
1155
|
+
if (!hasNumericForceColor()) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
return Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
|
1159
|
+
}
|
|
1160
|
+
function translateLevel(level) {
|
|
1161
|
+
if (level === 0) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
level,
|
|
1166
|
+
hasBasic: true,
|
|
1167
|
+
has256: level >= 2,
|
|
1168
|
+
has16m: level >= 3
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
1172
|
+
const noFlagForceColor = envForceColor();
|
|
1173
|
+
if (noFlagForceColor !== void 0) {
|
|
1174
|
+
flagForceColor = noFlagForceColor;
|
|
1175
|
+
}
|
|
1176
|
+
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
|
1177
|
+
if (forceColor === 0) {
|
|
1178
|
+
return 0;
|
|
1179
|
+
}
|
|
1180
|
+
if (sniffFlags) {
|
|
1181
|
+
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
|
|
1182
|
+
return 3;
|
|
1183
|
+
}
|
|
1184
|
+
if (hasFlag("color=256")) {
|
|
1185
|
+
return 2;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (forceColor !== void 0 && hasNumericForceColor()) {
|
|
1189
|
+
return forceColor;
|
|
1190
|
+
}
|
|
1191
|
+
if ("TF_BUILD" in env && "AGENT_NAME" in env) {
|
|
1192
|
+
return 1;
|
|
1193
|
+
}
|
|
1194
|
+
if (haveStream && !streamIsTTY && forceColor === void 0) {
|
|
1195
|
+
return 0;
|
|
1196
|
+
}
|
|
1197
|
+
const min = forceColor || 0;
|
|
1198
|
+
if (env.TERM === "dumb") {
|
|
1199
|
+
return min;
|
|
1200
|
+
}
|
|
1201
|
+
if (process2.platform === "win32") {
|
|
1202
|
+
const osRelease = os.release().split(".");
|
|
1203
|
+
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
1204
|
+
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
1205
|
+
}
|
|
1206
|
+
return 1;
|
|
1207
|
+
}
|
|
1208
|
+
if ("CI" in env) {
|
|
1209
|
+
if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
|
|
1210
|
+
return 3;
|
|
1211
|
+
}
|
|
1212
|
+
if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
|
|
1213
|
+
return 1;
|
|
1214
|
+
}
|
|
1215
|
+
return min;
|
|
1216
|
+
}
|
|
1217
|
+
if ("TEAMCITY_VERSION" in env) {
|
|
1218
|
+
return /^(?:9\.0*[1-9]\d*\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
1219
|
+
}
|
|
1220
|
+
if (env.COLORTERM === "truecolor") {
|
|
1221
|
+
return 3;
|
|
1222
|
+
}
|
|
1223
|
+
if (env.TERM === "xterm-kitty") {
|
|
1224
|
+
return 3;
|
|
1225
|
+
}
|
|
1226
|
+
if (env.TERM === "xterm-ghostty") {
|
|
1227
|
+
return 3;
|
|
1228
|
+
}
|
|
1229
|
+
if (env.TERM === "wezterm") {
|
|
1230
|
+
return 3;
|
|
1231
|
+
}
|
|
1232
|
+
if ("TERM_PROGRAM" in env) {
|
|
1233
|
+
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".", 1)[0], 10);
|
|
1234
|
+
switch (env.TERM_PROGRAM) {
|
|
1235
|
+
case "iTerm.app": {
|
|
1236
|
+
return version >= 3 ? 3 : 2;
|
|
1237
|
+
}
|
|
1238
|
+
case "Apple_Terminal": {
|
|
1239
|
+
return 2;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (/-256(?:color)?$/i.test(env.TERM)) {
|
|
1244
|
+
return 2;
|
|
1245
|
+
}
|
|
1246
|
+
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
|
1247
|
+
return 1;
|
|
1248
|
+
}
|
|
1249
|
+
if ("COLORTERM" in env) {
|
|
1250
|
+
return 1;
|
|
1251
|
+
}
|
|
1252
|
+
return min;
|
|
1253
|
+
}
|
|
1254
|
+
function createSupportsColor(stream, options = {}) {
|
|
1255
|
+
const level = _supportsColor(stream, {
|
|
1256
|
+
streamIsTTY: stream && stream.isTTY,
|
|
1257
|
+
...options
|
|
1258
|
+
});
|
|
1259
|
+
return translateLevel(level);
|
|
1260
|
+
}
|
|
1261
|
+
var supportsColor = {
|
|
1262
|
+
stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
|
|
1263
|
+
stderr: createSupportsColor({ isTTY: tty.isatty(2) })
|
|
1264
|
+
};
|
|
1265
|
+
var supports_color_default = supportsColor;
|
|
1266
|
+
|
|
1267
|
+
// ../../node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/index.js
|
|
1268
|
+
var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
|
|
1269
|
+
var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
|
|
1270
|
+
var STYLER = /* @__PURE__ */ Symbol("STYLER");
|
|
1271
|
+
var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
|
|
1272
|
+
var LEVEL = /* @__PURE__ */ Symbol("LEVEL");
|
|
1273
|
+
var styles2 = /* @__PURE__ */ Object.create(null);
|
|
1274
|
+
var assertValidLevel = (level) => {
|
|
1275
|
+
if (!Number.isSafeInteger(level) || level < 0 || level > 3) {
|
|
1276
|
+
throw new Error("The `level` should be an integer from 0 to 3");
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
var levelDescriptor = {
|
|
1280
|
+
enumerable: true,
|
|
1281
|
+
get() {
|
|
1282
|
+
return this[LEVEL];
|
|
1283
|
+
},
|
|
1284
|
+
set(level) {
|
|
1285
|
+
assertValidLevel(level);
|
|
1286
|
+
this[LEVEL] = level;
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
var applyOptions = (object, options = {}) => {
|
|
1290
|
+
if (options.level !== void 0) {
|
|
1291
|
+
assertValidLevel(options.level);
|
|
1292
|
+
}
|
|
1293
|
+
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
|
1294
|
+
object[LEVEL] = options.level === void 0 ? colorLevel : options.level;
|
|
1295
|
+
};
|
|
1296
|
+
var chalkFactory = (options) => {
|
|
1297
|
+
const chalk2 = (...strings) => strings.join(" ");
|
|
1298
|
+
applyOptions(chalk2, options);
|
|
1299
|
+
Object.setPrototypeOf(chalk2, createChalk.prototype);
|
|
1300
|
+
return chalk2;
|
|
1301
|
+
};
|
|
1302
|
+
function createChalk(options) {
|
|
1303
|
+
return chalkFactory(options);
|
|
1304
|
+
}
|
|
1305
|
+
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
|
1306
|
+
for (const [styleName, style] of Object.entries(ansi_styles_default)) {
|
|
1307
|
+
styles2[styleName] = {
|
|
1308
|
+
get() {
|
|
1309
|
+
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
|
|
1310
|
+
Object.defineProperty(this, styleName, { value: builder });
|
|
1311
|
+
return builder;
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
styles2.visible = {
|
|
1316
|
+
get() {
|
|
1317
|
+
const builder = createBuilder(this, this[STYLER], true);
|
|
1318
|
+
Object.defineProperty(this, "visible", { value: builder });
|
|
1319
|
+
return builder;
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
var createModelConverters = (model, type) => {
|
|
1323
|
+
const style = ansi_styles_default[type];
|
|
1324
|
+
if (model === "rgb") {
|
|
1325
|
+
const ansi2 = (red, green, blue) => style.ansi(ansi_styles_default.rgbToAnsi(red, green, blue));
|
|
1326
|
+
const ansi256 = (red, green, blue) => style.ansi256(ansi_styles_default.rgbToAnsi256(red, green, blue));
|
|
1327
|
+
return [ansi2, ansi2, ansi256, style.ansi16m];
|
|
1328
|
+
}
|
|
1329
|
+
if (model === "hex") {
|
|
1330
|
+
const ansi2 = (hex) => style.ansi(ansi_styles_default.hexToAnsi(hex));
|
|
1331
|
+
const ansi256 = (hex) => style.ansi256(ansi_styles_default.hexToAnsi256(hex));
|
|
1332
|
+
return [ansi2, ansi2, ansi256, (hex) => style.ansi16m(...ansi_styles_default.hexToRgb(hex))];
|
|
1333
|
+
}
|
|
1334
|
+
const ansi = (code) => style.ansi(ansi_styles_default.ansi256ToAnsi(code));
|
|
1335
|
+
return [ansi, ansi, style.ansi256, style.ansi256];
|
|
1336
|
+
};
|
|
1337
|
+
var usedModels = ["rgb", "hex", "ansi256"];
|
|
1338
|
+
for (const model of usedModels) {
|
|
1339
|
+
const capitalizedModel = model[0].toUpperCase() + model.slice(1);
|
|
1340
|
+
for (const [styleName, type] of [
|
|
1341
|
+
[model, "color"],
|
|
1342
|
+
["bg" + capitalizedModel, "bgColor"],
|
|
1343
|
+
["underline" + capitalizedModel, "underlineColor"]
|
|
1344
|
+
]) {
|
|
1345
|
+
const { close } = ansi_styles_default[type];
|
|
1346
|
+
const converters = createModelConverters(model, type);
|
|
1347
|
+
styles2[styleName] = {
|
|
1348
|
+
get() {
|
|
1349
|
+
const styleFunction = function(first, second, third) {
|
|
1350
|
+
const open = converters[this.level](first, second, third);
|
|
1351
|
+
return createBuilder(this, createStyler(open, close, this[STYLER]), this[IS_EMPTY]);
|
|
1352
|
+
};
|
|
1353
|
+
Object.defineProperty(this, styleName, { value: styleFunction });
|
|
1354
|
+
return styleFunction;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
var proto = Object.defineProperties(
|
|
1360
|
+
() => {
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
...styles2,
|
|
1364
|
+
level: {
|
|
1365
|
+
enumerable: true,
|
|
1366
|
+
get() {
|
|
1367
|
+
return this[GENERATOR].level;
|
|
1368
|
+
},
|
|
1369
|
+
set(level) {
|
|
1370
|
+
this[GENERATOR].level = level;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
);
|
|
1375
|
+
var createStyler = (open, close, parent) => {
|
|
1376
|
+
let openAll;
|
|
1377
|
+
let closeAll;
|
|
1378
|
+
if (parent === void 0) {
|
|
1379
|
+
openAll = open;
|
|
1380
|
+
closeAll = close;
|
|
1381
|
+
} else {
|
|
1382
|
+
openAll = parent.openAll + open;
|
|
1383
|
+
closeAll = close + parent.closeAll;
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
open,
|
|
1387
|
+
close,
|
|
1388
|
+
openAll,
|
|
1389
|
+
closeAll,
|
|
1390
|
+
parent
|
|
1391
|
+
};
|
|
1392
|
+
};
|
|
1393
|
+
var createBuilder = (self, _styler, _isEmpty) => {
|
|
1394
|
+
const builder = (...arguments_) => {
|
|
1395
|
+
if (arguments_.length === 1) {
|
|
1396
|
+
return applyStyle(builder, "" + arguments_[0]);
|
|
1397
|
+
}
|
|
1398
|
+
if (arguments_.length === 2) {
|
|
1399
|
+
return applyStyle(builder, arguments_[0] + " " + arguments_[1]);
|
|
1400
|
+
}
|
|
1401
|
+
return applyStyle(builder, arguments_.join(" "));
|
|
1402
|
+
};
|
|
1403
|
+
Object.setPrototypeOf(builder, proto);
|
|
1404
|
+
builder[GENERATOR] = self[GENERATOR] ?? self;
|
|
1405
|
+
builder[STYLER] = _styler;
|
|
1406
|
+
builder[IS_EMPTY] = _isEmpty;
|
|
1407
|
+
return builder;
|
|
1408
|
+
};
|
|
1409
|
+
var applyStyle = (self, string) => {
|
|
1410
|
+
if (self[GENERATOR][LEVEL] <= 0 || !string) {
|
|
1411
|
+
return self[IS_EMPTY] ? "" : string;
|
|
1412
|
+
}
|
|
1413
|
+
let styler = self[STYLER];
|
|
1414
|
+
if (styler === void 0) {
|
|
1415
|
+
return string;
|
|
1416
|
+
}
|
|
1417
|
+
const { openAll, closeAll } = styler;
|
|
1418
|
+
if (string.includes("\x1B")) {
|
|
1419
|
+
while (styler !== void 0) {
|
|
1420
|
+
string = stringReplaceAll(string, styler.close, styler.open);
|
|
1421
|
+
styler = styler.parent;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const lfIndex = string.indexOf("\n");
|
|
1425
|
+
if (lfIndex !== -1) {
|
|
1426
|
+
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
|
|
1427
|
+
}
|
|
1428
|
+
return openAll + string + closeAll;
|
|
1429
|
+
};
|
|
1430
|
+
Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
|
|
1431
|
+
var chalk = createChalk();
|
|
1432
|
+
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
1433
|
+
var source_default = chalk;
|
|
1434
|
+
|
|
1435
|
+
// cli.mjs
|
|
1436
|
+
import { parseArgs } from "node:util";
|
|
1437
|
+
var flag = source_default.green;
|
|
1438
|
+
var value = source_default.cyan;
|
|
1439
|
+
var dim = source_default.dim;
|
|
1440
|
+
var OPTIONS = [
|
|
1441
|
+
{
|
|
1442
|
+
name: "since",
|
|
1443
|
+
short: "s",
|
|
1444
|
+
type: "string",
|
|
1445
|
+
default: "90d",
|
|
1446
|
+
placeholder: "<value>",
|
|
1447
|
+
help: "Only include PRs created after this point. Accepts an ISO date (`2026-01-01`) or a relative value (`30d`, `8w`, `6m`, `1y`). The default is `90d`."
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
name: "repo",
|
|
1451
|
+
short: "r",
|
|
1452
|
+
type: "string",
|
|
1453
|
+
multiple: true,
|
|
1454
|
+
default: [],
|
|
1455
|
+
placeholder: "<name>",
|
|
1456
|
+
help: 'Restrict the search to a repository. Repeat the flag for multiple repositories. Accepts "owner/name" or a bare name, which resolves against the owner of the repo in the current directory. Without this flag, all repositories you have access to are searched.'
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
name: "user",
|
|
1460
|
+
short: "u",
|
|
1461
|
+
type: "string",
|
|
1462
|
+
placeholder: "<login>",
|
|
1463
|
+
help: "Compute stats for this user. Defaults to the authenticated user."
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
name: "token",
|
|
1467
|
+
type: "string",
|
|
1468
|
+
placeholder: "<token>",
|
|
1469
|
+
help: "GitHub access token. When set, the tool calls the GitHub API directly instead of going through the gh CLI. The `GITHUB_TOKEN` and `GH_TOKEN` environment variables work as well."
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
name: "reports",
|
|
1473
|
+
type: "string",
|
|
1474
|
+
default: "review-time,size",
|
|
1475
|
+
placeholder: "<list>",
|
|
1476
|
+
help: "Choose which reports to print as a comma-separated list. Accepts `review-time` (how fast you finish review requests) and `size` (how large your authored PRs are). Both print by default."
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
name: "target",
|
|
1480
|
+
short: "t",
|
|
1481
|
+
type: "string",
|
|
1482
|
+
placeholder: "<value>",
|
|
1483
|
+
help: "Report how many reviews finished within this time. Accepts hours (`24h` or plain `24`), minutes (`90m`), or days (`2d`). A day means 24 counted hours, or one working day when --work-hours is set. Open PRs that have already waited longer than the target count as misses."
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
name: "size-target",
|
|
1487
|
+
type: "string",
|
|
1488
|
+
placeholder: "<v>",
|
|
1489
|
+
help: "Report how many authored PRs fit within this size. Accepts a line budget (`400` or `400l`), a file budget (`20f`), or both (`400l,20f`). Lines count additions plus deletions."
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
name: "charts",
|
|
1493
|
+
short: "c",
|
|
1494
|
+
type: "string",
|
|
1495
|
+
default: "histogram,strip,spark",
|
|
1496
|
+
placeholder: "<list>",
|
|
1497
|
+
help: "Choose which size charts to print as a comma-separated list. Accepts `histogram` (size distribution bars), `strip` (a quantile strip per metric), and `spark` (a size line chart over time). All three print by default. Pass `none` to print no charts."
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
name: "tz",
|
|
1501
|
+
type: "string",
|
|
1502
|
+
placeholder: "<zone>",
|
|
1503
|
+
help: "IANA timezone for the weekend and working-hours math, for example `Europe/Berlin`. Defaults to your system timezone."
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
name: "work-hours",
|
|
1507
|
+
short: "w",
|
|
1508
|
+
type: "string",
|
|
1509
|
+
default: "0-24",
|
|
1510
|
+
placeholder: "<v>",
|
|
1511
|
+
help: "Count only these working hours instead of full weekdays. Accepts one range like `9-17` or `8:30-16:30`, or several comma-separated ranges like `9-18,19:30-20:30`. Without this flag, every hour Mon-Fri counts."
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: "wall-clock",
|
|
1515
|
+
type: "boolean",
|
|
1516
|
+
default: false,
|
|
1517
|
+
help: "Measure raw elapsed time, including weekends."
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
name: "include-drafts",
|
|
1521
|
+
type: "boolean",
|
|
1522
|
+
default: false,
|
|
1523
|
+
help: "Include PRs that are currently drafts. Excluded by default."
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
name: "help",
|
|
1527
|
+
short: "h",
|
|
1528
|
+
type: "boolean",
|
|
1529
|
+
default: false,
|
|
1530
|
+
help: "Show this help."
|
|
1531
|
+
}
|
|
1532
|
+
];
|
|
1533
|
+
var HELP_WIDTH = Math.min(process.stdout.columns ?? 80, 120);
|
|
1534
|
+
function colorizeHelp(text) {
|
|
1535
|
+
return text.replaceAll(/`([^`]+)`/g, (match, span) => value(span)).replaceAll(/"[^"]*"/g, (span) => source_default.yellow(span)).replaceAll(/--[a-z][a-z-]*/g, (span) => flag(span));
|
|
1536
|
+
}
|
|
1537
|
+
function wrapMarkup(text, width) {
|
|
1538
|
+
const lines = [];
|
|
1539
|
+
let line = "";
|
|
1540
|
+
for (const word of text.split(" ")) {
|
|
1541
|
+
const candidate = line === "" ? word : `${line} ${word}`;
|
|
1542
|
+
if (line !== "" && candidate.replaceAll("`", "").length > width) {
|
|
1543
|
+
lines.push(line);
|
|
1544
|
+
line = word;
|
|
1545
|
+
} else {
|
|
1546
|
+
line = candidate;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (line !== "") {
|
|
1550
|
+
lines.push(line);
|
|
1551
|
+
}
|
|
1552
|
+
return lines;
|
|
1553
|
+
}
|
|
1554
|
+
function renderHelp() {
|
|
1555
|
+
const labels = OPTIONS.map((option) => {
|
|
1556
|
+
const short = option.short ? `-${option.short}, ` : "";
|
|
1557
|
+
const long = option.placeholder ? `--${option.name} ${option.placeholder}` : `--${option.name}`;
|
|
1558
|
+
return short + long;
|
|
1559
|
+
});
|
|
1560
|
+
const column = Math.max(...labels.map((label) => label.length)) + 4;
|
|
1561
|
+
const lines = [`${source_default.bold("Usage:")} ${source_default.bold("pr-stats")} ${dim("[options]")}`, "", source_default.bold("Options:")];
|
|
1562
|
+
for (const [index, option] of OPTIONS.entries()) {
|
|
1563
|
+
const short = option.short ? `${flag(`-${option.short}`)}, ` : "";
|
|
1564
|
+
const long = option.placeholder ? `${flag(`--${option.name}`)} ${dim(option.placeholder)}` : flag(`--${option.name}`);
|
|
1565
|
+
const body = wrapMarkup(option.help, HELP_WIDTH - column).map((line) => colorizeHelp(line));
|
|
1566
|
+
lines.push(` ${short}${long}${" ".repeat(column - 2 - labels[index].length)}${body[0]}`);
|
|
1567
|
+
for (const overflow of body.slice(1)) {
|
|
1568
|
+
lines.push(" ".repeat(column) + overflow);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return lines.join("\n");
|
|
1572
|
+
}
|
|
1573
|
+
var HELP = renderHelp();
|
|
1574
|
+
function parseCliArgs() {
|
|
1575
|
+
const options = {};
|
|
1576
|
+
for (const option of OPTIONS) {
|
|
1577
|
+
options[option.name] = { type: option.type };
|
|
1578
|
+
if (option.short) {
|
|
1579
|
+
options[option.name].short = option.short;
|
|
1580
|
+
}
|
|
1581
|
+
if (option.multiple) {
|
|
1582
|
+
options[option.name].multiple = true;
|
|
1583
|
+
}
|
|
1584
|
+
if (option.default !== void 0) {
|
|
1585
|
+
options[option.name].default = option.default;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
const { values } = parseArgs({ options });
|
|
1589
|
+
return values;
|
|
1590
|
+
}
|
|
1591
|
+
function parseSince(value2) {
|
|
1592
|
+
const relative = /^(\d+)([dwmy])$/.exec(value2);
|
|
1593
|
+
if (relative) {
|
|
1594
|
+
const amount = Number(relative[1]);
|
|
1595
|
+
const date2 = /* @__PURE__ */ new Date();
|
|
1596
|
+
if (relative[2] === "d") {
|
|
1597
|
+
date2.setDate(date2.getDate() - amount);
|
|
1598
|
+
}
|
|
1599
|
+
if (relative[2] === "w") {
|
|
1600
|
+
date2.setDate(date2.getDate() - amount * 7);
|
|
1601
|
+
}
|
|
1602
|
+
if (relative[2] === "m") {
|
|
1603
|
+
date2.setMonth(date2.getMonth() - amount);
|
|
1604
|
+
}
|
|
1605
|
+
if (relative[2] === "y") {
|
|
1606
|
+
date2.setFullYear(date2.getFullYear() - amount);
|
|
1607
|
+
}
|
|
1608
|
+
return date2;
|
|
1609
|
+
}
|
|
1610
|
+
const date = new Date(value2);
|
|
1611
|
+
if (Number.isNaN(date.getTime())) {
|
|
1612
|
+
fail(`invalid --since value "${value2}", use an ISO date or 30d/8w/6m/1y`);
|
|
1613
|
+
}
|
|
1614
|
+
return date;
|
|
1615
|
+
}
|
|
1616
|
+
function parseTarget(value2) {
|
|
1617
|
+
const match = /^(\d+(?:\.\d+)?)([hdm]?)$/.exec(value2);
|
|
1618
|
+
if (!match) {
|
|
1619
|
+
fail(`invalid --target value "${value2}", use 24h, 2d, or 90m`);
|
|
1620
|
+
}
|
|
1621
|
+
const amount = Number(match[1]);
|
|
1622
|
+
if (match[2] === "d") {
|
|
1623
|
+
return amount * timeMode.dayHours;
|
|
1624
|
+
}
|
|
1625
|
+
if (match[2] === "m") {
|
|
1626
|
+
return amount / 60;
|
|
1627
|
+
}
|
|
1628
|
+
return amount;
|
|
1629
|
+
}
|
|
1630
|
+
function parseSizeTarget(value2) {
|
|
1631
|
+
const target = {};
|
|
1632
|
+
for (const part of value2.split(",")) {
|
|
1633
|
+
const match = /^(\d+)([lf]?)$/.exec(part.trim());
|
|
1634
|
+
if (!match) {
|
|
1635
|
+
fail(`invalid --size-target value "${value2}", use 400, 400l, 20f, or 400l,20f`);
|
|
1636
|
+
}
|
|
1637
|
+
const key = match[2] === "f" ? "files" : "lines";
|
|
1638
|
+
if (target[key] !== void 0) {
|
|
1639
|
+
fail(`--size-target sets the ${key} budget twice`);
|
|
1640
|
+
}
|
|
1641
|
+
target[key] = Number(match[1]);
|
|
1642
|
+
}
|
|
1643
|
+
return target;
|
|
1644
|
+
}
|
|
1645
|
+
function parseReports(value2) {
|
|
1646
|
+
const names = /* @__PURE__ */ new Set(["review-time", "size"]);
|
|
1647
|
+
const parts = value2.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
1648
|
+
const reports = /* @__PURE__ */ new Set();
|
|
1649
|
+
for (const part of parts) {
|
|
1650
|
+
if (!names.has(part)) {
|
|
1651
|
+
fail(
|
|
1652
|
+
`invalid ${flag("--reports")} value "${part}", use a comma-separated list of ${value2("review-time")} and ${value2("size")}`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
reports.add(part);
|
|
1656
|
+
}
|
|
1657
|
+
if (reports.size === 0) {
|
|
1658
|
+
fail(`--reports needs at least one of ${value2("review-time")} and ${value2("size")}`);
|
|
1659
|
+
}
|
|
1660
|
+
return reports;
|
|
1661
|
+
}
|
|
1662
|
+
function parseCharts(value2) {
|
|
1663
|
+
const aliases = {
|
|
1664
|
+
histogram: "histogram",
|
|
1665
|
+
hist: "histogram",
|
|
1666
|
+
strip: "strip",
|
|
1667
|
+
spark: "spark",
|
|
1668
|
+
sparkline: "spark"
|
|
1669
|
+
};
|
|
1670
|
+
const parts = value2.split(",").map((part) => part.trim().toLowerCase()).filter((part) => part !== "");
|
|
1671
|
+
if (parts.length === 1 && parts[0] === "none") {
|
|
1672
|
+
return /* @__PURE__ */ new Set();
|
|
1673
|
+
}
|
|
1674
|
+
const charts = /* @__PURE__ */ new Set();
|
|
1675
|
+
for (const part of parts) {
|
|
1676
|
+
const name = aliases[part];
|
|
1677
|
+
if (name === void 0) {
|
|
1678
|
+
fail(`invalid --charts value "${part}", use a comma-separated list of histogram, strip, spark, or none`);
|
|
1679
|
+
}
|
|
1680
|
+
charts.add(name);
|
|
1681
|
+
}
|
|
1682
|
+
return charts;
|
|
1683
|
+
}
|
|
1684
|
+
function parseWorkHours(value2) {
|
|
1685
|
+
const windows = value2.split(",").map((range) => {
|
|
1686
|
+
const match = /^(\d{1,2})(?::(\d{2}))?-(\d{1,2})(?::(\d{2}))?$/.exec(range.trim());
|
|
1687
|
+
if (!match) {
|
|
1688
|
+
fail(`invalid --work-hours range "${range}", use ranges like 9-17, 8:30-16:30, or 9-18,19:30-20:30`);
|
|
1689
|
+
}
|
|
1690
|
+
const startMin = Number(match[1]) * 60 + Number(match[2] ?? 0);
|
|
1691
|
+
const endMin = Number(match[3]) * 60 + Number(match[4] ?? 0);
|
|
1692
|
+
if (endMin <= startMin || endMin > 24 * 60 || Number(match[2] ?? 0) > 59 || Number(match[4] ?? 0) > 59) {
|
|
1693
|
+
fail(`invalid --work-hours range "${range}"`);
|
|
1694
|
+
}
|
|
1695
|
+
return { startMin, endMin };
|
|
1696
|
+
});
|
|
1697
|
+
windows.sort((a, b) => a.startMin - b.startMin);
|
|
1698
|
+
for (let i = 1; i < windows.length; i++) {
|
|
1699
|
+
if (windows[i].startMin < windows[i - 1].endMin) {
|
|
1700
|
+
fail(`--work-hours ranges overlap around ${formatMinutesOfDay(windows[i].startMin)}`);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return windows;
|
|
1704
|
+
}
|
|
1705
|
+
function resolveTimezone(value2) {
|
|
1706
|
+
const tz = value2 ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1707
|
+
try {
|
|
1708
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
1709
|
+
} catch {
|
|
1710
|
+
fail(`invalid --tz value "${tz}", use an IANA zone like Europe/Berlin`);
|
|
1711
|
+
}
|
|
1712
|
+
return tz;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// index.mjs
|
|
1716
|
+
async function main() {
|
|
1717
|
+
const values = parseCliArgs();
|
|
1718
|
+
if (values.help) {
|
|
1719
|
+
console.info(HELP);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
configureAuth(values.token);
|
|
1723
|
+
const since = parseSince(values.since);
|
|
1724
|
+
const sinceIso = since.toISOString().slice(0, 10);
|
|
1725
|
+
const tz = resolveTimezone(values.tz);
|
|
1726
|
+
const workWindows = parseWorkHours(values["work-hours"]);
|
|
1727
|
+
configureTimeMode({ business: !values["wall-clock"], workWindows, tz });
|
|
1728
|
+
initBuckets();
|
|
1729
|
+
const targetHours = values.target !== void 0 ? parseTarget(values.target) : void 0;
|
|
1730
|
+
const targetLabel = values.target !== void 0 ? values.target + (/^[\d.]+$/.test(values.target) ? "h" : "") : void 0;
|
|
1731
|
+
const sizeTarget = values["size-target"] !== void 0 ? parseSizeTarget(values["size-target"]) : void 0;
|
|
1732
|
+
const charts = parseCharts(values.charts);
|
|
1733
|
+
const reports = parseReports(values.reports);
|
|
1734
|
+
const user = values.user ?? await fetchCurrentUser();
|
|
1735
|
+
const repos = await resolveRepos(values.repo);
|
|
1736
|
+
const includeDrafts = values["include-drafts"];
|
|
1737
|
+
console.info(
|
|
1738
|
+
`Searching PRs since ${sinceIso} for @${user}` + (repos.length > 0 ? ` in ${repos.join(", ")}` : " across all accessible repos") + (includeDrafts ? " (drafts included)" : "")
|
|
1739
|
+
);
|
|
1740
|
+
if (isFullDayMode()) {
|
|
1741
|
+
console.info(`Durations count all hours Mon-Fri and skip weekends in ${tz}.`);
|
|
1742
|
+
} else if (timeMode.business) {
|
|
1743
|
+
const windowsText = workWindows.map((window) => `${formatMinutesOfDay(window.startMin)}-${formatMinutesOfDay(window.endMin)}`).join(", ");
|
|
1744
|
+
console.info(`Durations count working hours only, Mon-Fri ${windowsText} in ${tz} (1wd = ${timeMode.dayHours}h)`);
|
|
1745
|
+
} else {
|
|
1746
|
+
console.info("Durations are wall-clock time.");
|
|
1747
|
+
}
|
|
1748
|
+
const [requested, reviewed, authored] = await Promise.all([
|
|
1749
|
+
reports.has("review-time") ? searchPrs({ user, sinceIso, repos, includeDrafts, mode: "requested" }) : [],
|
|
1750
|
+
reports.has("review-time") ? searchPrs({ user, sinceIso, repos, includeDrafts, mode: "reviewed" }) : [],
|
|
1751
|
+
reports.has("size") ? searchPrs({ user, sinceIso, repos, includeDrafts, mode: "authored" }) : []
|
|
1752
|
+
]);
|
|
1753
|
+
if (reports.has("review-time")) {
|
|
1754
|
+
await analyzeReviewTimes({ requested, reviewed, user, targetHours, targetLabel });
|
|
1755
|
+
}
|
|
1756
|
+
if (reports.has("size")) {
|
|
1757
|
+
await analyzeAuthoredSizes({ authored, sizeTarget, charts });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
await main();
|