@vladimirshefer/git-stats 0.0.2 → 0.8.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/dist/index.js +309 -633
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
9
13
|
var __copyProps = (to, from, except, desc) => {
|
|
10
14
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
15
|
for (let key of __getOwnPropNames(from))
|
|
@@ -22,542 +26,31 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
26
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
27
|
mod
|
|
24
28
|
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
30
|
|
|
26
31
|
// src/index.ts
|
|
27
|
-
var
|
|
28
|
-
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
dataDir: () => dataDir,
|
|
35
|
+
progress: () => progress,
|
|
36
|
+
runScan1: () => runScan1
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
var fs5 = __toESM(require("fs"));
|
|
40
|
+
var path5 = __toESM(require("path"));
|
|
41
|
+
var readline = __toESM(require("readline"));
|
|
29
42
|
|
|
30
43
|
// src/output/report_template.ts
|
|
31
44
|
var path = __toESM(require("path"));
|
|
32
45
|
var fs = __toESM(require("fs"));
|
|
33
46
|
|
|
34
|
-
//
|
|
35
|
-
var report_template_default = `<!DOCTYPE html>
|
|
36
|
-
<!--suppress TypeScriptMissingConfigOption -->
|
|
37
|
-
<html lang="en">
|
|
38
|
-
<head>
|
|
39
|
-
<meta charset="UTF-8">
|
|
40
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
41
|
-
<title>Git Blame Statistics</title>
|
|
42
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
43
|
-
<script src="https://cdn.plot.ly/plotly-3.3.0.min.js" charset="utf-8"></script>
|
|
44
|
-
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
45
|
-
<!-- Preact UMD build -->
|
|
46
|
-
<script crossorigin src="https://unpkg.com/preact@10/dist/preact.umd.js"></script>
|
|
47
|
-
<script crossorigin src="https://unpkg.com/preact@10/hooks/dist/hooks.umd.js"></script>
|
|
48
|
-
<!-- Babel Standalone for in-browser JSX transform -->
|
|
49
|
-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
50
|
-
</head>
|
|
51
|
-
<body class="font-sans m-0 bg-gray-50 text-gray-900">
|
|
52
|
-
<div id="root"></div>
|
|
53
|
-
</body>
|
|
54
|
-
<script type="text/babel" data-presets="react">
|
|
55
|
-
// Preact compat aliases
|
|
56
|
-
const React = window.preact;
|
|
57
|
-
const ReactDOM = {
|
|
58
|
-
createRoot: (container) => ({
|
|
59
|
-
render: (element) => window.preact.render(element, container)
|
|
60
|
-
})
|
|
61
|
-
};
|
|
62
|
-
React.useRef = window.preactHooks.useRef;
|
|
63
|
-
React.useEffect = window.preactHooks.useEffect;
|
|
64
|
-
React.useState = window.preactHooks.useState;
|
|
65
|
-
React.useMemo = window.preactHooks.useMemo;
|
|
66
|
-
|
|
67
|
-
const RAW_DATASET =
|
|
68
|
-
__DATASET_JSON__
|
|
69
|
-
|| [[1,2,3,4,5,6,7,8,9]];
|
|
70
|
-
// Fixed schema as per pipeline: [author, days_bucket, lang, clusterPath, repoName, count]
|
|
71
|
-
const RAW_DATASET_SCHEMA = ["author", "days_bucket", "lang", "clusterPath", "repoName", "count"];
|
|
72
|
-
const CLUSTER_COLUMN = RAW_DATASET_SCHEMA.indexOf("clusterPath");
|
|
73
|
-
const REPO_COLUMN = RAW_DATASET_SCHEMA.indexOf("repoName");
|
|
74
|
-
// -------- Client-side grouping/filtering engine --------
|
|
75
|
-
const KEY_INDEX = Object.fromEntries(RAW_DATASET_SCHEMA.map((k, i) => [k, i]));
|
|
76
|
-
const COLUMNS_AMOUNT = RAW_DATASET[0].length - 1;
|
|
77
|
-
const COLUMNS_IDX_ARRAY = Array(COLUMNS_AMOUNT).fill(-1).map((_, i) => i);
|
|
78
|
-
const COUNT_IDX_FROM_END = 1; // last element is count
|
|
79
|
-
const COLUMN_COMPARATORS = COLUMNS_IDX_ARRAY.map(idx => {
|
|
80
|
-
let isNumber = typeof RAW_DATASET?.[0]?.[idx] === "number";
|
|
81
|
-
return isNumber ? ((a, b) => (a || 0) - (b || 0)) : (a, b) => String(a).localeCompare(String(b));
|
|
82
|
-
})
|
|
83
|
-
const UNIQUE_VALUES = COLUMNS_IDX_ARRAY.map((idx) => uniqueValues(RAW_DATASET, idx).sort(COLUMN_COMPARATORS[idx]))
|
|
84
|
-
|
|
85
|
-
const TOP_N = 20;
|
|
86
|
-
const BUCKET_COLORS = [
|
|
87
|
-
'rgba(214, 40, 40, 0.7)',
|
|
88
|
-
'rgba(247, 127, 0, 0.7)',
|
|
89
|
-
'rgba(252, 191, 73, 0.7)',
|
|
90
|
-
'rgba(168, 218, 142, 0.7)',
|
|
91
|
-
'rgba(75, 192, 192, 0.7)',
|
|
92
|
-
'rgba(54, 162, 235, 0.7)',
|
|
93
|
-
'rgba(153, 102, 255, 0.7)',
|
|
94
|
-
'rgba(201, 203, 207, 0.7)'
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
function uniqueValues(arr, idx) {
|
|
98
|
-
const set = new Set(arr.map(r => r[idx]));
|
|
99
|
-
return Array.from(set);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function matchesFilters(row, filters) {
|
|
103
|
-
for (let idx = 0; idx < COLUMNS_AMOUNT; idx++) {
|
|
104
|
-
const sel = filters[idx];
|
|
105
|
-
if (sel && !sel.has(String(row[idx]))) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function pivot(
|
|
113
|
-
dataset,
|
|
114
|
-
column1,
|
|
115
|
-
column2
|
|
116
|
-
) {
|
|
117
|
-
const grouped2 = new Map();
|
|
118
|
-
const secValuesSet = new Set();
|
|
119
|
-
for (const row of dataset) {
|
|
120
|
-
const c1 = row[column1];
|
|
121
|
-
const c2 = row[column2];
|
|
122
|
-
const count = Number(row[row.length - COUNT_IDX_FROM_END]) || 0;
|
|
123
|
-
if (!grouped2.has(c1)) grouped2.set(c1, new Map());
|
|
124
|
-
grouped2.get(c1).set(c2, (grouped2.get(c1).get(c2) || 0) + count);
|
|
125
|
-
secValuesSet.add(c2);
|
|
126
|
-
}
|
|
127
|
-
const primaryTotals = new Map();
|
|
128
|
-
for (const [c1, innerMap] of grouped2) {
|
|
129
|
-
let total = 0;
|
|
130
|
-
for (const val of innerMap.values()) total += val;
|
|
131
|
-
primaryTotals.set(c1, total);
|
|
132
|
-
}
|
|
133
|
-
const primaryKeys = Array.from(grouped2.keys()).sort((a, b) => (primaryTotals.get(b) || 0) - (primaryTotals.get(a) || 0));
|
|
134
|
-
const secondaryKeys = Array.from(secValuesSet).sort((a, b) => String(a).localeCompare(String(b)));
|
|
135
|
-
return {grouped2, primaryKeys, secondaryKeys};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function computeColumnTotals(dataset, columnIdx) {
|
|
139
|
-
const value2total = new Map();
|
|
140
|
-
const otherColumnContributions = new Map(); // Map<key, Map<otherColumnIdx, Map<otherValue, count>>>
|
|
141
|
-
|
|
142
|
-
for (const row of dataset) {
|
|
143
|
-
const value = row[columnIdx];
|
|
144
|
-
const count = Number(row[row.length - 1]) || 0;
|
|
145
|
-
value2total.set(value, (value2total.get(value) || 0) + count);
|
|
146
|
-
|
|
147
|
-
// Track contributions from other columns
|
|
148
|
-
if (!otherColumnContributions.has(value)) {
|
|
149
|
-
otherColumnContributions.set(value, new Map());
|
|
150
|
-
}
|
|
151
|
-
const keyContribs = otherColumnContributions.get(value);
|
|
152
|
-
|
|
153
|
-
for (let otherIdx = 0; otherIdx < COLUMNS_AMOUNT; otherIdx++) {
|
|
154
|
-
if (otherIdx === columnIdx) continue;
|
|
155
|
-
|
|
156
|
-
if (!keyContribs.has(otherIdx)) {
|
|
157
|
-
keyContribs.set(otherIdx, new Map());
|
|
158
|
-
}
|
|
159
|
-
const otherColMap = keyContribs.get(otherIdx);
|
|
160
|
-
const otherValue = row[otherIdx];
|
|
161
|
-
otherColMap.set(otherValue, (otherColMap.get(otherValue) || 0) + count);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const sorted = Array.from(value2total.entries()).sort((a, b) => b[1] - a[1]);
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
keys: sorted.map(([k]) => k),
|
|
169
|
-
values: sorted.map(([, v]) => v),
|
|
170
|
-
totals: value2total,
|
|
171
|
-
contributions: otherColumnContributions
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---------- React Components ----------
|
|
176
|
-
function ColumnTotalCard({columnName, columnIdx, keys, values, totals, contributions}) {
|
|
177
|
-
const containerRef = React.useRef(null);
|
|
178
|
-
|
|
179
|
-
const {labels, hoverText} = React.useMemo(() => {
|
|
180
|
-
const labels = [];
|
|
181
|
-
const hoverText = [];
|
|
182
|
-
|
|
183
|
-
keys.forEach((k) => {
|
|
184
|
-
const total = totals.get(k);
|
|
185
|
-
const keyContribs = contributions.get(k);
|
|
186
|
-
const topContribs = [];
|
|
187
|
-
|
|
188
|
-
for (let otherIdx = 0; otherIdx < COLUMNS_AMOUNT; otherIdx++) {
|
|
189
|
-
if (otherIdx === columnIdx) continue;
|
|
190
|
-
const otherColMap = keyContribs?.get(otherIdx);
|
|
191
|
-
if (otherColMap) {
|
|
192
|
-
const top3 = Array.from(otherColMap.entries())
|
|
193
|
-
.sort((a, b) => b[1] - a[1])
|
|
194
|
-
.slice(0, 3)
|
|
195
|
-
.map(([val, cnt]) => \`\${val}(\${(cnt / total * 100.0).toFixed(1)}%)\`)
|
|
196
|
-
.join('<br>-');
|
|
197
|
-
if (top3) {
|
|
198
|
-
topContribs.push(\`\${RAW_DATASET_SCHEMA[otherIdx]}<br>-\` + top3);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const label = \`\${String(k)} (\${total})\`;
|
|
204
|
-
labels.push(label);
|
|
205
|
-
hoverText.push(topContribs.length > 0 ? \`\${label}<br>\${topContribs.join('<br>')}\` : label);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
return {labels, hoverText};
|
|
209
|
-
}, [columnName, columnIdx, keys, totals, contributions]);
|
|
210
|
-
|
|
211
|
-
React.useEffect(() => {
|
|
212
|
-
const el = containerRef.current;
|
|
213
|
-
if (!el) return;
|
|
214
|
-
if (!labels.length) {
|
|
215
|
-
el.innerHTML = '<div class="text-gray-500 py-6 text-center">No data to display</div>';
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const trace = {
|
|
219
|
-
type: 'sunburst',
|
|
220
|
-
labels: labels,
|
|
221
|
-
parents: labels.map(() => ''),
|
|
222
|
-
values: values,
|
|
223
|
-
hovertext: hoverText,
|
|
224
|
-
hovertemplate: '%{hovertext}<extra></extra>',
|
|
225
|
-
branchvalues: 'total'
|
|
226
|
-
};
|
|
227
|
-
const layout = {
|
|
228
|
-
margin: {l: 0, r: 0, t: 10, b: 10},
|
|
229
|
-
sunburstcolorway: ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#06B6D4', '#8B5CF6', '#F43F5E'],
|
|
230
|
-
extendsunburstcolors: true,
|
|
231
|
-
height: 300
|
|
232
|
-
};
|
|
233
|
-
const config = {responsive: true, displayModeBar: false};
|
|
234
|
-
if (window.Plotly && window.Plotly.newPlot) {
|
|
235
|
-
window.Plotly.newPlot(el, [trace], layout, config);
|
|
236
|
-
}
|
|
237
|
-
}, [labels, hoverText, values]);
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
|
241
|
-
<h3 className="text-lg font-semibold mb-3 text-gray-800">{columnName} - Total</h3>
|
|
242
|
-
<div ref={containerRef} className="w-full"/>
|
|
243
|
-
</div>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ---------- Sunburst over repository paths (clusterPath) ----------
|
|
248
|
-
function buildSunburst(dataset, filterState) {
|
|
249
|
-
const filtered = dataset.filter(row => matchesFilters(row, filterState));
|
|
250
|
-
|
|
251
|
-
const PSEUDO_ROOT = "@";
|
|
252
|
-
// Aggregate counts per path and propagate sums up the tree
|
|
253
|
-
const leafSums = new Map(); // fullPath -> sum
|
|
254
|
-
const allPaths = new Set(); // includes all prefixes (no empty)
|
|
255
|
-
|
|
256
|
-
for (const row of filtered) {
|
|
257
|
-
const rawPath = String(row[REPO_COLUMN] + "/" + row[CLUSTER_COLUMN] ?? '').trim();
|
|
258
|
-
const cnt = Number(row[COLUMNS_AMOUNT]) || 0;
|
|
259
|
-
if (!rawPath) continue;
|
|
260
|
-
const segs = rawPath.split('/').filter(s => s && s !== '.');
|
|
261
|
-
if (segs.length === 0) continue;
|
|
262
|
-
const full = segs.join('/');
|
|
263
|
-
leafSums.set(full, (leafSums.get(full) || 0) + cnt);
|
|
264
|
-
// Collect all prefix nodes for structure
|
|
265
|
-
for (let i = 0; i < segs.length; i++) {
|
|
266
|
-
const p = segs.slice(0, i + 1).join('/');
|
|
267
|
-
allPaths.add(p);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (allPaths.size === 0) return {ids: [], labels: [], parents: [], values: []};
|
|
272
|
-
|
|
273
|
-
// Compute total values for every node as sum of its descendant leaves
|
|
274
|
-
const totals = new Map(Array.from(allPaths, p => [p, 0]));
|
|
275
|
-
for (const [leaf, v] of leafSums) {
|
|
276
|
-
const segs = leaf.split('/');
|
|
277
|
-
for (let i = 0; i < segs.length; i++) {
|
|
278
|
-
const p = segs.slice(0, i + 1).join('/');
|
|
279
|
-
totals.set(p, (totals.get(p) || 0) + v);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Build Plotly arrays
|
|
284
|
-
const ids = [];
|
|
285
|
-
const labels = [];
|
|
286
|
-
const parents = [];
|
|
287
|
-
const values = [];
|
|
288
|
-
|
|
289
|
-
// Ensure stable ordering: sort by path length then alphabetically
|
|
290
|
-
const ordered = Array.from(allPaths);
|
|
291
|
-
ordered.sort((a, b) => {
|
|
292
|
-
const da = a.split('/').length, db = b.split('/').length;
|
|
293
|
-
if (da !== db) return da - db;
|
|
294
|
-
return a.localeCompare(b);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
for (const id of ordered) {
|
|
298
|
-
const segs = id.split('/');
|
|
299
|
-
const label = segs[segs.length - 1] || id;
|
|
300
|
-
const parent = segs.length > 1 ? segs.slice(0, -1).join('/') : '';
|
|
301
|
-
ids.push(id);
|
|
302
|
-
labels.push(label);
|
|
303
|
-
parents.push(parent);
|
|
304
|
-
values.push(totals.get(id) || 0);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return {ids, labels, parents, values};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function SunburstPaths({dataset, filters}) {
|
|
311
|
-
const containerRef = React.useRef(null);
|
|
312
|
-
const data = React.useMemo(() => buildSunburst(dataset, filters), [dataset, filters]);
|
|
313
|
-
|
|
314
|
-
React.useEffect(() => {
|
|
315
|
-
const el = containerRef.current;
|
|
316
|
-
if (!el) return;
|
|
317
|
-
if (!data.ids.length) {
|
|
318
|
-
el.innerHTML = '<div class="text-gray-500 py-6 text-center">No path data to display</div>';
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const trace = {
|
|
322
|
-
type: 'sunburst',
|
|
323
|
-
ids: data.ids,
|
|
324
|
-
labels: data.labels,
|
|
325
|
-
parents: data.parents,
|
|
326
|
-
values: data.values,
|
|
327
|
-
branchvalues: 'total',
|
|
328
|
-
maxdepth: 3
|
|
329
|
-
};
|
|
330
|
-
const layout = {
|
|
331
|
-
margin: {l: 0, r: 0, t: 10, b: 10},
|
|
332
|
-
sunburstcolorway: ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#06B6D4', '#8B5CF6', '#F43F5E'],
|
|
333
|
-
extendsunburstcolors: true,
|
|
334
|
-
height: 400
|
|
335
|
-
};
|
|
336
|
-
const config = {responsive: true, displayModeBar: false};
|
|
337
|
-
// Use Plotly from global
|
|
338
|
-
if (window.Plotly && window.Plotly.newPlot) {
|
|
339
|
-
window.Plotly.newPlot(el, [trace], layout, config);
|
|
340
|
-
}
|
|
341
|
-
}, [data]);
|
|
342
|
-
|
|
343
|
-
return <div ref={containerRef} className="w-full"/>;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function MultiSelect({label, values, selectedSet, onChange}) {
|
|
347
|
-
const [isOpen, setIsOpen] = React.useState(false);
|
|
348
|
-
const [searchTerm, setSearchTerm] = React.useState('');
|
|
349
|
-
const dropdownRef = React.useRef(null);
|
|
350
|
-
|
|
351
|
-
React.useEffect(() => {
|
|
352
|
-
function handleClickOutside(event) {
|
|
353
|
-
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
354
|
-
setIsOpen(false);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
359
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
360
|
-
}, []);
|
|
361
|
-
|
|
362
|
-
const filteredValues = React.useMemo(() => {
|
|
363
|
-
if (!searchTerm) return values;
|
|
364
|
-
const lower = searchTerm.toLowerCase();
|
|
365
|
-
return values.filter(v => String(v).toLowerCase().includes(lower));
|
|
366
|
-
}, [values, searchTerm]);
|
|
367
|
-
|
|
368
|
-
const selCount = selectedSet.size;
|
|
369
|
-
const total = values.length;
|
|
370
|
-
|
|
371
|
-
const toggleValue = (val) => {
|
|
372
|
-
const newSet = new Set(selectedSet);
|
|
373
|
-
if (newSet.has(val)) {
|
|
374
|
-
newSet.delete(val);
|
|
375
|
-
} else {
|
|
376
|
-
newSet.add(val);
|
|
377
|
-
}
|
|
378
|
-
onChange(newSet);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
return (
|
|
382
|
-
<div ref={dropdownRef} className="relative">
|
|
383
|
-
<label className="block font-semibold mb-1.5">
|
|
384
|
-
{label}{' '}
|
|
385
|
-
<span className="text-gray-600 font-normal">({selCount}/{total})</span>
|
|
386
|
-
</label>
|
|
387
|
-
<button
|
|
388
|
-
type="button"
|
|
389
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
390
|
-
className="w-full px-3 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
391
|
-
>
|
|
392
|
-
<span className="text-gray-700">
|
|
393
|
-
{selCount === 0 ? 'Select...' : selCount === total ? 'All selected' : \`\${selCount} selected\`}
|
|
394
|
-
</span>
|
|
395
|
-
<span className="float-right">\u25BC</span>
|
|
396
|
-
</button>
|
|
397
|
-
{isOpen && (
|
|
398
|
-
<div
|
|
399
|
-
className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-80 overflow-hidden">
|
|
400
|
-
<div className="p-2 border-b border-gray-200">
|
|
401
|
-
<input
|
|
402
|
-
type="text"
|
|
403
|
-
placeholder="Search..."
|
|
404
|
-
value={searchTerm}
|
|
405
|
-
onChange={(e) => setSearchTerm(e.target.value)}
|
|
406
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
407
|
-
onClick={(e) => e.stopPropagation()}
|
|
408
|
-
/>
|
|
409
|
-
<div className="flex gap-2 mt-2">
|
|
410
|
-
<button
|
|
411
|
-
type="button"
|
|
412
|
-
onClick={() => onChange(new Set(values.map(String)))}
|
|
413
|
-
className="flex-1 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
414
|
-
>
|
|
415
|
-
Select All
|
|
416
|
-
</button>
|
|
417
|
-
<button
|
|
418
|
-
type="button"
|
|
419
|
-
onClick={() => onChange(new Set())}
|
|
420
|
-
className="flex-1 px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
|
421
|
-
>
|
|
422
|
-
Unselect All
|
|
423
|
-
</button>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
<div className="overflow-y-auto max-h-64">
|
|
427
|
-
{filteredValues.length === 0 ? (
|
|
428
|
-
<div className="px-3 py-2 text-gray-500 text-center">No matches found</div>
|
|
429
|
-
) : (
|
|
430
|
-
filteredValues.map(v => {
|
|
431
|
-
const val = String(v);
|
|
432
|
-
const isChecked = selectedSet.has(val);
|
|
433
|
-
return (
|
|
434
|
-
<label
|
|
435
|
-
key={val}
|
|
436
|
-
className="flex items-center px-3 py-2 hover:bg-gray-100 cursor-pointer"
|
|
437
|
-
>
|
|
438
|
-
<input
|
|
439
|
-
type="checkbox"
|
|
440
|
-
checked={isChecked}
|
|
441
|
-
onChange={() => toggleValue(val)}
|
|
442
|
-
className="mr-2 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
443
|
-
/>
|
|
444
|
-
<span className="text-gray-900">{val}</span>
|
|
445
|
-
</label>
|
|
446
|
-
);
|
|
447
|
-
})
|
|
448
|
-
)}
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
)}
|
|
452
|
-
</div>
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function App() {
|
|
457
|
-
const {initialFilters, valueOptions} = React.useMemo(() => {
|
|
458
|
-
const filters = {};
|
|
459
|
-
const options = {};
|
|
460
|
-
for (let idx = 0; idx < COLUMNS_AMOUNT; idx++) {
|
|
461
|
-
filters[idx] = new Set(UNIQUE_VALUES[idx].map(v => String(v)));
|
|
462
|
-
options[idx] = UNIQUE_VALUES[idx];
|
|
463
|
-
}
|
|
464
|
-
return {initialFilters: filters, valueOptions: options};
|
|
465
|
-
}, []);
|
|
466
|
-
const [filters, setFilters] = React.useState(initialFilters);
|
|
467
|
-
const [primaryKeyIndex, setPrimaryKeyIndex] = React.useState(0);
|
|
468
|
-
const [secondaryKeyIndex, setSecondaryKeyIndex] = React.useState(1);
|
|
469
|
-
|
|
470
|
-
const {chartLabels, datasets} = React.useMemo(() => {
|
|
471
|
-
const filteredDataset = RAW_DATASET.filter(row => matchesFilters(row, filters));
|
|
472
|
-
const {
|
|
473
|
-
grouped2: pv,
|
|
474
|
-
primaryKeys,
|
|
475
|
-
secondaryKeys
|
|
476
|
-
} = pivot(filteredDataset, primaryKeyIndex, secondaryKeyIndex);
|
|
477
|
-
const chartPrimaryKeys = primaryKeys.slice(0, TOP_N);
|
|
478
|
-
const ds = secondaryKeys.map((sk, i) => ({
|
|
479
|
-
label: String(sk),
|
|
480
|
-
data: chartPrimaryKeys.map(pk => (pv.get(pk)?.get(sk)) || 0),
|
|
481
|
-
backgroundColor: BUCKET_COLORS[i % BUCKET_COLORS.length]
|
|
482
|
-
}));
|
|
483
|
-
return {chartLabels: chartPrimaryKeys, datasets: ds};
|
|
484
|
-
}, [filters, primaryKeyIndex, secondaryKeyIndex]);
|
|
485
|
-
|
|
486
|
-
return (
|
|
487
|
-
<div className="max-w-4xl mx-auto my-5 p-5 bg-white rounded-lg shadow-sm">
|
|
488
|
-
<h1 className="border-b border-gray-300 pb-2.5">Git Contribution Statistics</h1>
|
|
489
|
-
<div className="controls">
|
|
490
|
-
<h2 className="border-b border-gray-300 pb-2.5">Controls</h2>
|
|
491
|
-
<div className="flex gap-4 flex-wrap items-center">
|
|
492
|
-
<label>
|
|
493
|
-
Primary group:
|
|
494
|
-
<select value={primaryKeyIndex} onChange={e => setPrimaryKeyIndex(Number(e.target.value))}>
|
|
495
|
-
{COLUMNS_IDX_ARRAY.map((__, i) => (
|
|
496
|
-
<option key={i} value={i}>{RAW_DATASET_SCHEMA[i]}</option>
|
|
497
|
-
))}
|
|
498
|
-
</select>
|
|
499
|
-
</label>
|
|
500
|
-
<label>
|
|
501
|
-
Secondary group:
|
|
502
|
-
<select value={secondaryKeyIndex}
|
|
503
|
-
onChange={e => setSecondaryKeyIndex(Number(e.target.value))}>
|
|
504
|
-
{COLUMNS_IDX_ARRAY.map((__, i) => (
|
|
505
|
-
<option key={i} value={i}>{RAW_DATASET_SCHEMA[i]}</option>
|
|
506
|
-
))}
|
|
507
|
-
</select>
|
|
508
|
-
</label>
|
|
509
|
-
</div>
|
|
510
|
-
<div id="filters" className="mt-3 grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3">
|
|
511
|
-
{COLUMNS_IDX_ARRAY.map((__, idx) => (
|
|
512
|
-
<MultiSelect
|
|
513
|
-
key={idx}
|
|
514
|
-
label={RAW_DATASET_SCHEMA[idx]}
|
|
515
|
-
values={valueOptions[idx]}
|
|
516
|
-
selectedSet={filters[idx]}
|
|
517
|
-
onChange={(newSet) => setFilters(prev => ({...prev, [idx]: newSet}))}
|
|
518
|
-
/>
|
|
519
|
-
))}
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
<div className="mt-8">
|
|
523
|
-
<h2 className="border-b border-gray-300 pb-2.5">Column Totals</h2>
|
|
524
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
|
525
|
-
{COLUMNS_IDX_ARRAY.map((__, idx) => {
|
|
526
|
-
const filteredDataset = RAW_DATASET.filter(row => matchesFilters(row, filters));
|
|
527
|
-
const {keys, values, totals, contributions} = computeColumnTotals(filteredDataset, idx);
|
|
528
|
-
return (
|
|
529
|
-
<ColumnTotalCard
|
|
530
|
-
key={idx}
|
|
531
|
-
columnName={RAW_DATASET_SCHEMA[idx]}
|
|
532
|
-
columnIdx={idx}
|
|
533
|
-
keys={keys}
|
|
534
|
-
values={values}
|
|
535
|
-
totals={totals}
|
|
536
|
-
contributions={contributions}
|
|
537
|
-
/>
|
|
538
|
-
);
|
|
539
|
-
})}
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
<div className="mt-8">
|
|
543
|
-
<h2 className="border-b border-gray-300 pb-2.5">Repository Paths Sunburst</h2>
|
|
544
|
-
<p className="text-sm text-gray-600 mt-2 mb-3">Breakdown by folder structure based on <code>clusterPath</code> within current filters.</p>
|
|
545
|
-
<SunburstPaths dataset={RAW_DATASET} filters={filters}/>
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
552
|
-
root.render(<App/>);
|
|
553
|
-
</script>
|
|
554
|
-
</html>
|
|
555
|
-
`;
|
|
47
|
+
// ../html-ui/dist/index.html
|
|
48
|
+
var dist_default = '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <script src="https://cdn.plot.ly/plotly-3.3.0.min.js" charset="utf-8"></script>\n <title>Git Blame Statistics</title>\n <style>/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-4xl:56rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-normal:400;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.z-10{z-index:10}.float-right{float:right}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-8{margin-top:calc(var(--spacing)*8)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1\\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.block{display:block}.flex{display:flex}.grid{display:grid}.h-4{height:calc(var(--spacing)*4)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-80{max-height:calc(var(--spacing)*80)}.w-4{width:calc(var(--spacing)*4)}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\\[repeat\\(auto-fit\\,minmax\\(220px\\,1fr\\)\\)\\]{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.items-center{align-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.px-3{padding-inline:calc(var(--spacing)*3)}.py-1\\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-6{padding-block:calc(var(--spacing)*6)}.pb-2\\.5{padding-bottom:calc(var(--spacing)*2.5)}.text-center{text-align:center}.text-left{text-align:left}.font-sans{font-family:var(--font-sans)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-blue-600{color:var(--color-blue-600)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-white{color:var(--color-white)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){.hover\\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\\:bg-gray-700:hover{background-color:var(--color-gray-700)}}.focus\\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.focus\\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:48rem){.md\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}</style>\n</head>\n<script>\n try {\n window.RAW_DATASET =\n __DATASET_JSON__\n } catch (e) {\n window.RAW_DATASET = [[1, 2, 3, 4, 5, 6, 7, 8, 9]]\n }\n</script>\n<body class="font-sans m-0 bg-gray-50 text-gray-900">\n <div id="root"></div>\n <script>(()=>{var Y,g,he,ze,U,fe,ve,ge,ye,oe,te,ne,Qe,Q={},G=[],Ge=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,J=Array.isArray;function E(t,e){for(var n in e)t[n]=e[n];return t}function _e(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function Xe(t,e,n){var _,s,o,l={};for(o in e)o=="key"?_=e[o]:o=="ref"?s=e[o]:l[o]=e[o];if(arguments.length>2&&(l.children=arguments.length>3?Y.call(arguments,2):n),typeof t=="function"&&t.defaultProps!=null)for(o in t.defaultProps)l[o]===void 0&&(l[o]=t.defaultProps[o]);return q(t,l,_,s,null)}function q(t,e,n,_,s){var o={type:t,props:e,key:n,ref:_,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s==null?++he:s,__i:-1,__u:0};return s==null&&g.vnode!=null&&g.vnode(o),o}function H(t){return t.children}function z(t,e){this.props=t,this.context=e}function F(t,e){if(e==null)return t.__?F(t.__,t.__i+1):null;for(var n;e<t.__k.length;e++)if((n=t.__k[e])!=null&&n.__e!=null)return n.__e;return typeof t.type=="function"?F(t):null}function Ye(t){if(t.__P&&t.__d){var e=t.__v,n=e.__e,_=[],s=[],o=E({},e);o.__v=e.__v+1,g.vnode&&g.vnode(o),se(t.__P,o,e,t.__n,t.__P.namespaceURI,32&e.__u?[n]:null,_,n==null?F(e):n,!!(32&e.__u),s),o.__v=e.__v,o.__.__k[o.__i]=o,ke(_,o,s),e.__e=e.__=null,o.__e!=n&&be(o)}}function be(t){if((t=t.__)!=null&&t.__c!=null)return t.__e=t.__c.base=null,t.__k.some(function(e){if(e!=null&&e.__e!=null)return t.__e=t.__c.base=e.__e}),be(t)}function pe(t){(!t.__d&&(t.__d=!0)&&U.push(t)&&!X.__r++||fe!=g.debounceRendering)&&((fe=g.debounceRendering)||ve)(X)}function X(){for(var t,e=1;U.length;)U.length>e&&U.sort(ge),t=U.shift(),e=U.length,Ye(t);X.__r=0}function xe(t,e,n,_,s,o,l,a,f,i,u){var r,p,c,m,y,b,v,h=_&&_.__k||G,C=e.length;for(f=Je(n,e,h,f,C),r=0;r<C;r++)(c=n.__k[r])!=null&&(p=c.__i!=-1&&h[c.__i]||Q,c.__i=r,b=se(t,c,p,s,o,l,a,f,i,u),m=c.__e,c.ref&&p.ref!=c.ref&&(p.ref&&le(p.ref,null,c),u.push(c.ref,c.__c||m,c)),y==null&&m!=null&&(y=m),(v=!!(4&c.__u))||p.__k===c.__k?f=we(c,f,t,v):typeof c.type=="function"&&b!==void 0?f=b:m&&(f=m.nextSibling),c.__u&=-7);return n.__e=y,f}function Je(t,e,n,_,s){var o,l,a,f,i,u=n.length,r=u,p=0;for(t.__k=new Array(s),o=0;o<s;o++)(l=e[o])!=null&&typeof l!="boolean"&&typeof l!="function"?(typeof l=="string"||typeof l=="number"||typeof l=="bigint"||l.constructor==String?l=t.__k[o]=q(null,l,null,null,null):J(l)?l=t.__k[o]=q(H,{children:l},null,null,null):l.constructor===void 0&&l.__b>0?l=t.__k[o]=q(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[o]=l,f=o+p,l.__=t,l.__b=t.__b+1,a=null,(i=l.__i=Ke(l,n,f,r))!=-1&&(r--,(a=n[i])&&(a.__u|=2)),a==null||a.__v==null?(i==-1&&(s>u?p--:s<u&&p++),typeof l.type!="function"&&(l.__u|=4)):i!=f&&(i==f-1?p--:i==f+1?p++:(i>f?p--:p++,l.__u|=4))):t.__k[o]=null;if(r)for(o=0;o<u;o++)(a=n[o])!=null&&(2&a.__u)==0&&(a.__e==_&&(_=F(a)),Ne(a,a));return _}function we(t,e,n,_){var s,o;if(typeof t.type=="function"){for(s=t.__k,o=0;s&&o<s.length;o++)s[o]&&(s[o].__=t,e=we(s[o],e,n,_));return e}t.__e!=e&&(_&&(e&&t.type&&!e.parentNode&&(e=F(t)),n.insertBefore(t.__e,e||null)),e=t.__e);do e=e&&e.nextSibling;while(e!=null&&e.nodeType==8);return e}function Ke(t,e,n,_){var s,o,l,a=t.key,f=t.type,i=e[n],u=i!=null&&(2&i.__u)==0;if(i===null&&a==null||u&&a==i.key&&f==i.type)return n;if(_>(u?1:0)){for(s=n-1,o=n+1;s>=0||o<e.length;)if((i=e[l=s>=0?s--:o++])!=null&&(2&i.__u)==0&&a==i.key&&f==i.type)return l}return-1}function de(t,e,n){e[0]=="-"?t.setProperty(e,n==null?"":n):t[e]=n==null?"":typeof n!="number"||Ge.test(e)?n:n+"px"}function V(t,e,n,_,s){var o,l;e:if(e=="style")if(typeof n=="string")t.style.cssText=n;else{if(typeof _=="string"&&(t.style.cssText=_=""),_)for(e in _)n&&e in n||de(t.style,e,"");if(n)for(e in n)_&&n[e]==_[e]||de(t.style,e,n[e])}else if(e[0]=="o"&&e[1]=="n")o=e!=(e=e.replace(ye,"$1")),l=e.toLowerCase(),e=l in t||e=="onFocusOut"||e=="onFocusIn"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+o]=n,n?_?n.u=_.u:(n.u=oe,t.addEventListener(e,o?ne:te,o)):t.removeEventListener(e,o?ne:te,o);else{if(s=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in t)try{t[e]=n==null?"":n;break e}catch(a){}typeof n=="function"||(n==null||n===!1&&e[4]!="-"?t.removeAttribute(e):t.setAttribute(e,e=="popover"&&n==1?"":n))}}function me(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e.t==null)e.t=oe++;else if(e.t<n.u)return;return n(g.event?g.event(e):e)}}}function se(t,e,n,_,s,o,l,a,f,i){var u,r,p,c,m,y,b,v,h,C,N,A,D,j,ee,S=e.type;if(e.constructor!==void 0)return null;128&n.__u&&(f=!!(32&n.__u),o=[a=e.__e=n.__e]),(u=g.__b)&&u(e);e:if(typeof S=="function")try{if(v=e.props,h="prototype"in S&&S.prototype.render,C=(u=S.contextType)&&_[u.__c],N=u?C?C.props.value:u.__:_,n.__c?b=(r=e.__c=n.__c).__=r.__E:(h?e.__c=r=new S(v,N):(e.__c=r=new z(v,N),r.constructor=S,r.render=et),C&&C.sub(r),r.state||(r.state={}),r.__n=_,p=r.__d=!0,r.__h=[],r._sb=[]),h&&r.__s==null&&(r.__s=r.state),h&&S.getDerivedStateFromProps!=null&&(r.__s==r.state&&(r.__s=E({},r.__s)),E(r.__s,S.getDerivedStateFromProps(v,r.__s))),c=r.props,m=r.state,r.__v=e,p)h&&S.getDerivedStateFromProps==null&&r.componentWillMount!=null&&r.componentWillMount(),h&&r.componentDidMount!=null&&r.__h.push(r.componentDidMount);else{if(h&&S.getDerivedStateFromProps==null&&v!==c&&r.componentWillReceiveProps!=null&&r.componentWillReceiveProps(v,N),e.__v==n.__v||!r.__e&&r.shouldComponentUpdate!=null&&r.shouldComponentUpdate(v,r.__s,N)===!1){e.__v!=n.__v&&(r.props=v,r.state=r.__s,r.__d=!1),e.__e=n.__e,e.__k=n.__k,e.__k.some(function(L){L&&(L.__=e)}),G.push.apply(r.__h,r._sb),r._sb=[],r.__h.length&&l.push(r);break e}r.componentWillUpdate!=null&&r.componentWillUpdate(v,r.__s,N),h&&r.componentDidUpdate!=null&&r.__h.push(function(){r.componentDidUpdate(c,m,y)})}if(r.context=N,r.props=v,r.__P=t,r.__e=!1,A=g.__r,D=0,h)r.state=r.__s,r.__d=!1,A&&A(e),u=r.render(r.props,r.state,r.context),G.push.apply(r.__h,r._sb),r._sb=[];else do r.__d=!1,A&&A(e),u=r.render(r.props,r.state,r.context),r.state=r.__s;while(r.__d&&++D<25);r.state=r.__s,r.getChildContext!=null&&(_=E(E({},_),r.getChildContext())),h&&!p&&r.getSnapshotBeforeUpdate!=null&&(y=r.getSnapshotBeforeUpdate(c,m)),j=u!=null&&u.type===H&&u.key==null?Ce(u.props.children):u,a=xe(t,J(j)?j:[j],e,n,_,s,o,l,a,f,i),r.base=e.__e,e.__u&=-161,r.__h.length&&l.push(r),b&&(r.__E=r.__=null)}catch(L){if(e.__v=null,f||o!=null)if(L.then){for(e.__u|=f?160:128;a&&a.nodeType==8&&a.nextSibling;)a=a.nextSibling;o[o.indexOf(a)]=null,e.__e=a}else{for(ee=o.length;ee--;)_e(o[ee]);re(e)}else e.__e=n.__e,e.__k=n.__k,L.then||re(e);g.__e(L,e,n)}else o==null&&e.__v==n.__v?(e.__k=n.__k,e.__e=n.__e):a=e.__e=Ze(n.__e,e,n,_,s,o,l,f,i);return(u=g.diffed)&&u(e),128&e.__u?void 0:a}function re(t){t&&(t.__c&&(t.__c.__e=!0),t.__k&&t.__k.some(re))}function ke(t,e,n){for(var _=0;_<n.length;_++)le(n[_],n[++_],n[++_]);g.__c&&g.__c(e,t),t.some(function(s){try{t=s.__h,s.__h=[],t.some(function(o){o.call(s)})}catch(o){g.__e(o,s.__v)}})}function Ce(t){return typeof t!="object"||t==null||t.__b>0?t:J(t)?t.map(Ce):E({},t)}function Ze(t,e,n,_,s,o,l,a,f){var i,u,r,p,c,m,y,b=n.props||Q,v=e.props,h=e.type;if(h=="svg"?s="http://www.w3.org/2000/svg":h=="math"?s="http://www.w3.org/1998/Math/MathML":s||(s="http://www.w3.org/1999/xhtml"),o!=null){for(i=0;i<o.length;i++)if((c=o[i])&&"setAttribute"in c==!!h&&(h?c.localName==h:c.nodeType==3)){t=c,o[i]=null;break}}if(t==null){if(h==null)return document.createTextNode(v);t=document.createElementNS(s,h,v.is&&v),a&&(g.__m&&g.__m(e,o),a=!1),o=null}if(h==null)b===v||a&&t.data==v||(t.data=v);else{if(o=o&&Y.call(t.childNodes),!a&&o!=null)for(b={},i=0;i<t.attributes.length;i++)b[(c=t.attributes[i]).name]=c.value;for(i in b)c=b[i],i=="dangerouslySetInnerHTML"?r=c:i=="children"||i in v||i=="value"&&"defaultValue"in v||i=="checked"&&"defaultChecked"in v||V(t,i,null,c,s);for(i in v)c=v[i],i=="children"?p=c:i=="dangerouslySetInnerHTML"?u=c:i=="value"?m=c:i=="checked"?y=c:a&&typeof c!="function"||b[i]===c||V(t,i,c,b[i],s);if(u)a||r&&(u.__html==r.__html||u.__html==t.innerHTML)||(t.innerHTML=u.__html),e.__k=[];else if(r&&(t.innerHTML=""),xe(e.type=="template"?t.content:t,J(p)?p:[p],e,n,_,h=="foreignObject"?"http://www.w3.org/1999/xhtml":s,o,l,o?o[0]:n.__k&&F(n,0),a,f),o!=null)for(i=o.length;i--;)_e(o[i]);a||(i="value",h=="progress"&&m==null?t.removeAttribute("value"):m!=null&&(m!==t[i]||h=="progress"&&!m||h=="option"&&m!=b[i])&&V(t,i,m,b[i],s),i="checked",y!=null&&y!=t[i]&&V(t,i,y,b[i],s))}return t}function le(t,e,n){try{if(typeof t=="function"){var _=typeof t.__u=="function";_&&t.__u(),_&&e==null||(t.__u=t(e))}else t.current=e}catch(s){g.__e(s,n)}}function Ne(t,e,n){var _,s;if(g.unmount&&g.unmount(t),(_=t.ref)&&(_.current&&_.current!=t.__e||le(_,null,e)),(_=t.__c)!=null){if(_.componentWillUnmount)try{_.componentWillUnmount()}catch(o){g.__e(o,e)}_.base=_.__P=null}if(_=t.__k)for(s=0;s<_.length;s++)_[s]&&Ne(_[s],e,n||typeof t.type!="function");n||_e(t.__e),t.__c=t.__=t.__e=void 0}function et(t,e,n){return this.constructor(t,n)}function Se(t,e,n){var _,s,o,l;e==document&&(e=document.documentElement),g.__&&g.__(t,e),s=(_=typeof n=="function")?null:n&&n.__k||e.__k,o=[],l=[],se(e,t=(!_&&n||e).__k=Xe(H,null,[t]),s||Q,Q,e.namespaceURI,!_&&n?[n]:s?null:e.firstChild?Y.call(e.childNodes):null,o,!_&&n?n:s?s.__e:e.firstChild,_,l),ke(o,t,l)}Y=G.slice,g={__e:function(t,e,n,_){for(var s,o,l;e=e.__;)if((s=e.__c)&&!s.__)try{if((o=s.constructor)&&o.getDerivedStateFromError!=null&&(s.setState(o.getDerivedStateFromError(t)),l=s.__d),s.componentDidCatch!=null&&(s.componentDidCatch(t,_||{}),l=s.__d),l)return s.__E=s}catch(a){t=a}throw t}},he=0,ze=function(t){return t!=null&&t.constructor===void 0},z.prototype.setState=function(t,e){var n;n=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=E({},this.state),typeof t=="function"&&(t=t(E({},n),this.props)),t&&E(n,t),t!=null&&this.__v&&(e&&this._sb.push(e),pe(this))},z.prototype.forceUpdate=function(t){this.__v&&(this.__e=!0,t&&this.__h.push(t),pe(this))},z.prototype.render=H,U=[],ve=typeof Promise=="function"?Promise.prototype.then.bind(Promise.resolve()):setTimeout,ge=function(t,e){return t.__v.__b-e.__v.__b},X.__r=0,ye=/(PointerCapture)$|Capture$/i,oe=0,te=me(!1),ne=me(!0),Qe=0;var I,x,ie,Me,Z=0,Fe=[],w=g,Ae=w.__b,Ee=w.__r,Te=w.diffed,Pe=w.__c,Ue=w.unmount,De=w.__;function ue(t,e){w.__h&&w.__h(x,t,Z||e),Z=0;var n=x.__H||(x.__H={__:[],__h:[]});return t>=n.__.length&&n.__.push({}),n.__[t]}function B(t){return Z=1,tt(Re,t)}function tt(t,e,n){var _=ue(I++,2);if(_.t=t,!_.__c&&(_.__=[n?n(e):Re(void 0,e),function(a){var f=_.__N?_.__N[0]:_.__[0],i=_.t(f,a);f!==i&&(_.__N=[i,_.__[1]],_.__c.setState({}))}],_.__c=x,!x.__f)){var s=function(a,f,i){if(!_.__c.__H)return!0;var u=_.__c.__H.__.filter(function(p){return p.__c});if(u.every(function(p){return!p.__N}))return!o||o.call(this,a,f,i);var r=_.__c.props!==a;return u.some(function(p){if(p.__N){var c=p.__[0];p.__=p.__N,p.__N=void 0,c!==p.__[0]&&(r=!0)}}),o&&o.call(this,a,f,i)||r};x.__f=!0;var o=x.shouldComponentUpdate,l=x.componentWillUpdate;x.componentWillUpdate=function(a,f,i){if(this.__e){var u=o;o=void 0,s(a,f,i),o=u}l&&l.call(this,a,f,i)},x.shouldComponentUpdate=s}return _.__N||_.__}function R(t,e){var n=ue(I++,3);!w.__s&&He(n.__H,e)&&(n.__=t,n.u=e,x.__H.__h.push(n))}function O(t){return Z=5,k(function(){return{current:t}},[])}function k(t,e){var n=ue(I++,7);return He(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function nt(){for(var t;t=Fe.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(K),e.__h.some(ae),e.__h=[]}catch(n){e.__h=[],w.__e(n,t.__v)}}}w.__b=function(t){x=null,Ae&&Ae(t)},w.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),De&&De(t,e)},w.__r=function(t){Ee&&Ee(t),I=0;var e=(x=t.__c).__H;e&&(ie===x?(e.__h=[],x.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(K),e.__h.some(ae),e.__h=[],I=0)),ie=x},w.diffed=function(t){Te&&Te(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Fe.push(e)!==1&&Me===w.requestAnimationFrame||((Me=w.requestAnimationFrame)||rt)(nt)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),ie=x=null},w.__c=function(t,e){e.some(function(n){try{n.__h.some(K),n.__h=n.__h.filter(function(_){return!_.__||ae(_)})}catch(_){e.some(function(s){s.__h&&(s.__h=[])}),e=[],w.__e(_,n.__v)}}),Pe&&Pe(t,e)},w.unmount=function(t){Ue&&Ue(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(_){try{K(_)}catch(s){e=s}}),n.__H=void 0,e&&w.__e(e,n.__v))};var Le=typeof requestAnimationFrame=="function";function rt(t){var e,n=function(){clearTimeout(_),Le&&cancelAnimationFrame(e),setTimeout(t)},_=setTimeout(n,35);Le&&(e=requestAnimationFrame(n))}function K(t){var e=x,n=t.__c;typeof n=="function"&&(t.__c=void 0,n()),x=e}function ae(t){var e=x;t.__c=t.__(),x=e}function He(t,e){return!t||t.length!==e.length||e.some(function(n,_){return n!==t[_]})}function Re(t,e){return typeof e=="function"?e(t):e}var T=window==null?void 0:window.RAW_DATASET,P=["author","days_bucket","lang","clusterPath","repoName","count"],M=T[0].length-1,W=Array(M).fill(-1).map((t,e)=>e),ot=W.map(t=>{var n;return typeof((n=T==null?void 0:T[0])==null?void 0:n[t])=="number"?(_,s)=>(_||0)-(s||0):(_,s)=>String(_).localeCompare(String(s))}),ce=W.map(t=>_t(T,t).sort(ot[t]));function _t(t,e){let n=new Set(t.map(_=>_[e]));return Array.from(n)}var Oe=P.indexOf("clusterPath"),Ie=P.indexOf("repoName");var st=0,dt=Array.isArray;function d(t,e,n,_,s,o){e||(e={});var l,a,f=e;if("ref"in f)for(a in f={},e)a=="ref"?l=e[a]:f[a]=e[a];var i={type:t,props:f,key:n,ref:l,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--st,__i:-1,__u:0,__source:s,__self:o};if(typeof t=="function"&&(l=t.defaultProps))for(a in l)f[a]===void 0&&(f[a]=l[a]);return g.vnode&&g.vnode(i),i}function Be({label:t,values:e,selectedSet:n,onChange:_}){let[s,o]=B(!1),[l,a]=B(""),f=O(null);R(()=>{function c(m){f.current&&!f.current.contains(m.target)&&o(!1)}return document.addEventListener("mousedown",c),()=>document.removeEventListener("mousedown",c)},[]);let i=k(()=>{if(!l)return e;let c=l.toLowerCase();return e.filter(m=>String(m).toLowerCase().includes(c))},[e,l]),u=n.size,r=e.length,p=c=>{let m=new Set(n);m.has(c)?m.delete(c):m.add(c),_(m)};return d("div",{ref:f,className:"relative",children:[d("label",{className:"block font-semibold mb-1.5",children:[t," ",d("span",{className:"text-gray-600 font-normal",children:["(",u,"/",r,")"]})]}),d("button",{type:"button",onClick:()=>o(!s),className:"w-full px-3 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[d("span",{className:"text-gray-700",children:u===0?"Select...":u===r?"All selected":`${u} selected`}),d("span",{className:"float-right",children:"\\u25BC"})]}),s&&d("div",{className:"absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-80 overflow-hidden",children:[d("div",{className:"p-2 border-b border-gray-200",children:[d("input",{type:"text",placeholder:"Search...",value:l,onChange:c=>a(c.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",onClick:c=>c.stopPropagation()}),d("div",{className:"flex gap-2 mt-2",children:[d("button",{type:"button",onClick:()=>_(new Set(e.map(String))),className:"flex-1 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500",children:"Select All"}),d("button",{type:"button",onClick:()=>_(new Set),className:"flex-1 px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500",children:"Unselect All"})]})]}),d("div",{className:"overflow-y-auto max-h-64",children:i.length===0?d("div",{className:"px-3 py-2 text-gray-500 text-center",children:"No matches found"}):i.map(c=>{let m=String(c),y=n.has(m);return d("label",{className:"flex items-center px-3 py-2 hover:bg-gray-100 cursor-pointer",children:[d("input",{type:"checkbox",checked:y,onChange:()=>p(m),className:"mr-2 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"}),d("span",{className:"text-gray-900",children:m})]},m)})})]})]})}function $({data:t,layout:e,config:n}){let _=O(null);return R(()=>{let s=_.current;s&&Plotly.newPlot(s,t,e,n)},[t,e,n]),d("div",{ref:_,className:"w-full"})}function We({columnName:t,columnIdx:e,keys:n,values:_,totals:s,contributions:o}){let{labels:l,hoverText:a}=k(()=>{let r=[],p=[];return n.forEach(c=>{let m=s.get(c)||0,y=o.get(c),b=[];for(let h=0;h<M;h++){if(h===e)continue;let C=y==null?void 0:y.get(h);if(C){let N=Array.from(C.entries()).sort((A,D)=>D[1]-A[1]).slice(0,3).map(([A,D])=>`${A}(${(D/m*100).toFixed(1)}%)`).join("<br>-");N&&b.push(`${P[h]}<br>-`+N)}}let v=`${String(c)} (${m})`;r.push(v),p.push(b.length>0?`${v}<br>${b.join("<br>")}`:v)}),{labels:r,hoverText:p}},[t,e,n,s,o]),f=k(()=>[{type:"sunburst",labels:l,parents:l.map(()=>""),values:_,hovertext:a,hovertemplate:"%{hovertext}<extra></extra>",branchvalues:"total"}],[l,a,_]),i=k(()=>({margin:{l:0,r:0,t:10,b:10},sunburstcolorway:["#4F46E5","#10B981","#F59E0B","#EF4444","#06B6D4","#8B5CF6","#F43F5E"],extendsunburstcolors:!0,height:300}),[]),u=k(()=>({responsive:!0,displayModeBar:!1}),[]);return d("div",{className:"bg-white border border-gray-200 rounded-lg p-4 shadow-sm",children:[d("h3",{className:"text-lg font-semibold mb-3 text-gray-800",children:[t," - Total"]}),l.length?d($,{data:f,layout:i,config:u}):d("div",{className:"text-gray-500 py-6 text-center",children:"No data to display"})]})}function $e({dataset:t}){let e=O(null),n=k(()=>lt(t),[t]);return R(()=>{let _=e.current;if(!_)return;if(!n.ids.length){_.innerHTML=\'<div class="text-gray-500 py-6 text-center">No path data to display</div>\';return}let s={type:"sunburst",ids:n.ids,labels:n.labels,parents:n.parents,values:n.values,branchvalues:"total",maxdepth:3},o={margin:{l:0,r:0,t:10,b:10},sunburstcolorway:["#4F46E5","#10B981","#F59E0B","#EF4444","#06B6D4","#8B5CF6","#F43F5E"],extendsunburstcolors:!0,height:400},l={responsive:!0,displayModeBar:!1};Plotly.newPlot(_,[s],o,l)},[n]),d("div",{ref:e,className:"w-full"})}function lt(t){let e=t,n=new Map,_=new Set;for(let u of e){let r=(u[Ie]+"/"+u[Oe]).trim(),p=Number(u[M])||0;if(!r)continue;let c=r.split("/").filter(y=>y&&y!==".");if(c.length===0)continue;let m=c.join("/");n.set(m,(n.get(m)||0)+p);for(let y=0;y<c.length;y++){let b=c.slice(0,y+1).join("/");_.add(b)}}if(_.size===0)return{ids:[],labels:[],parents:[],values:[]};let s=new Map(Array.from(_,u=>[u,0]));for(let[u,r]of n){let p=u.split("/");for(let c=0;c<p.length;c++){let m=p.slice(0,c+1).join("/");s.set(m,(s.get(m)||0)+r)}}let o=[],l=[],a=[],f=[],i=Array.from(_);i.sort((u,r)=>{let p=u.split("/").length,c=r.split("/").length;return p!==c?p-c:u.localeCompare(r)});for(let u of i){let r=u.split("/"),p=r[r.length-1]||u,c=r.length>1?r.slice(0,-1).join("/"):"";o.push(u),l.push(p),a.push(c),f.push(s.get(u)||0)}return{ids:o,labels:l,parents:a,values:f}}function Ve(){let{initialFilters:t,valueOptions:e}=k(()=>{let l={},a={};for(let f=0;f<M;f++)l[f]=new Set(ce[f].map(i=>String(i))),a[f]=ce[f];return{initialFilters:l,valueOptions:a}},[]),[n,_]=B(t),s=k(()=>T.filter(l=>je(l,n)),[n]),o=k(()=>{let l=new Map,a=P.indexOf("days_bucket"),f=null,i=null;s.forEach(u=>{let r=u[a],p=Number(u[M])||0;l.set(r,(l.get(r)||0)+p),console.error("ADD",r),(!f||r<f)&&(f=r),(!i||r>i)&&(i=r)});for(let u=f;u<i;u++)u%10>0&&u%10<=4&&!l.has(u)&&(l.set(u,0),console.error("ADD",u));return Array.from(l.entries()).sort((u,r)=>u[0]-r[0])},[s]);return d("div",{className:"max-w-4xl mx-auto bg-white rounded-lg shadow-sm p-4",children:[d("h1",{className:"border-gray-300 text-xl",children:"Git Contribution Statistics"}),d("div",{id:"filters",className:"grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3",children:W.map((l,a)=>d(Be,{label:P[a],values:e[a],selectedSet:n[a],onChange:f=>_(i=>({...i,[a]:f}))},a))}),d("div",{children:d($,{data:[{x:o.map(l=>l[0]),y:o.map(l=>l[1]),type:"scatter",line:{shape:"spline",smoothing:1.3}}],layout:{xaxis:{visible:!1,type:"category"},yaxis:{visible:!1},height:150,margin:{l:0,r:0,t:0,b:0,pad:0},bargap:0,bargroupgap:0,selectdirection:"h",zoomdirection:"x"},config:{displayModeBar:!1,displaylogo:!1}})}),d("div",{children:[d("h2",{className:"border-b border-gray-300",children:"Column Totals"}),d("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4 mt-4",children:W.map((l,a)=>{let f=T.filter(c=>je(c,n)),{keys:i,values:u,totals:r,contributions:p}=it(f,a);return d(We,{columnName:P[a],columnIdx:a,keys:i,values:u,totals:r,contributions:p},a)})})]}),d("div",{className:"mt-8",children:[d("h2",{className:"border-b border-gray-300 pb-2.5",children:"Repository Paths Sunburst"}),d("p",{className:"text-sm text-gray-600 mt-2 mb-3",children:["Breakdown by folder structure based on ",d("code",{children:"clusterPath"})," within current filters."]}),d($e,{dataset:s})]})]})}function je(t,e){for(let n=0;n<M;n++){let _=e[n];if(_&&!_.has(String(t[n])))return!1}return!0}function it(t,e){let n=new Map,_=new Map;for(let o of t){let l=o[e],a=Number(o[o.length-1])||0;n.set(l,(n.get(l)||0)+a),_.has(l)||_.set(l,new Map);let f=_.get(l);for(let i=0;i<M;i++){if(i===e)continue;f.has(i)||f.set(i,new Map);let u=f.get(i),r=o[i];u.set(r,(u.get(r)||0)+a)}}let s=Array.from(n.entries()).sort((o,l)=>l[1]-o[1]);return{keys:s.map(([o])=>o),values:s.map(([,o])=>o),totals:n,contributions:_}}var qe=document.getElementById("root");qe&&Se(d(Ve,{}),qe);})();</script>\n</body>\n</html>\nw';
|
|
556
49
|
|
|
557
50
|
// src/output/report_template.ts
|
|
558
51
|
function generateHtmlReport(data, outputFile) {
|
|
559
52
|
const finalOutputPath = path.join(outputFile);
|
|
560
|
-
let htmlContent =
|
|
53
|
+
let htmlContent = dist_default.split("__DATASET_JSON__");
|
|
561
54
|
fs.writeFileSync(finalOutputPath, htmlContent[0]);
|
|
562
55
|
fs.appendFileSync(finalOutputPath, "\n[\n");
|
|
563
56
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -577,7 +70,7 @@ function execAsync(command, args = [], options = {}) {
|
|
|
577
70
|
return new Promise((resolve2, reject) => {
|
|
578
71
|
const child = (0, import_child_process.spawn)(command, args, { ...options });
|
|
579
72
|
let stdout = [];
|
|
580
|
-
let
|
|
73
|
+
let stderr2 = [];
|
|
581
74
|
let stdoutBuffer = "";
|
|
582
75
|
let stderrBuffer = "";
|
|
583
76
|
child.stdout.on("data", (data) => {
|
|
@@ -592,7 +85,7 @@ function execAsync(command, args = [], options = {}) {
|
|
|
592
85
|
stderrBuffer += data.toString();
|
|
593
86
|
const lines = stderrBuffer.split("\n");
|
|
594
87
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
595
|
-
|
|
88
|
+
stderr2.push(lines[i]);
|
|
596
89
|
}
|
|
597
90
|
stderrBuffer = lines[lines.length - 1];
|
|
598
91
|
});
|
|
@@ -604,13 +97,13 @@ function execAsync(command, args = [], options = {}) {
|
|
|
604
97
|
stdout.push(stdoutBuffer);
|
|
605
98
|
}
|
|
606
99
|
if (stderrBuffer.length > 0) {
|
|
607
|
-
|
|
100
|
+
stderr2.push(stderrBuffer);
|
|
608
101
|
}
|
|
609
102
|
if (code === 0) {
|
|
610
|
-
resolve2({ stdout, stderr });
|
|
103
|
+
resolve2({ stdout, stderr: stderr2 });
|
|
611
104
|
} else {
|
|
612
105
|
reject(new Error(`Command ${command} ${JSON.stringify(args)} failed with code ${code}
|
|
613
|
-
${
|
|
106
|
+
${stderr2.join("\n")}`));
|
|
614
107
|
}
|
|
615
108
|
});
|
|
616
109
|
});
|
|
@@ -638,18 +131,18 @@ function parsePorcelain(blameOutput, fields) {
|
|
|
638
131
|
const commiterTimePos = fields.indexOf("committer-time");
|
|
639
132
|
const boundaryPos = fields.indexOf("boundary");
|
|
640
133
|
const commitPos = fields.indexOf("commit");
|
|
641
|
-
let emptyRow =
|
|
134
|
+
let emptyRow = {};
|
|
642
135
|
if (commiterTimePos >= 0) {
|
|
643
136
|
emptyRow[commiterTimePos] = 0;
|
|
644
137
|
}
|
|
645
138
|
if (boundaryPos >= 0) {
|
|
646
139
|
emptyRow[boundaryPos] = 0;
|
|
647
140
|
}
|
|
648
|
-
let nextRow =
|
|
141
|
+
let nextRow = { ...emptyRow };
|
|
649
142
|
const result = [];
|
|
650
143
|
for (const line of blameOutput) {
|
|
651
144
|
if (line.startsWith(" ")) {
|
|
652
|
-
result.push(
|
|
145
|
+
result.push({ ...nextRow });
|
|
653
146
|
continue;
|
|
654
147
|
}
|
|
655
148
|
if (commitPos >= 0) {
|
|
@@ -657,22 +150,22 @@ function parsePorcelain(blameOutput, fields) {
|
|
|
657
150
|
if (firstSpace === 40) {
|
|
658
151
|
const possibleHash = line.substring(0, firstSpace);
|
|
659
152
|
if (/^\^?[0-9a-f]{40}$/i.test(possibleHash)) {
|
|
660
|
-
nextRow =
|
|
661
|
-
nextRow
|
|
153
|
+
nextRow = { ...emptyRow };
|
|
154
|
+
nextRow.commit = possibleHash.replace(/^\^/, "");
|
|
662
155
|
continue;
|
|
663
156
|
}
|
|
664
157
|
}
|
|
665
158
|
}
|
|
666
159
|
if (userPos >= 0 && line.startsWith("author ")) {
|
|
667
|
-
nextRow
|
|
160
|
+
nextRow.author = line.substring("author ".length).replace(/^<|>$/g, "");
|
|
668
161
|
continue;
|
|
669
162
|
}
|
|
670
163
|
if (commiterTimePos >= 0 && line.startsWith("committer-time ")) {
|
|
671
|
-
nextRow
|
|
164
|
+
nextRow.time = parseInt(line.substring("committer-time ".length), 10);
|
|
672
165
|
continue;
|
|
673
166
|
}
|
|
674
167
|
if (boundaryPos >= 0 && line.startsWith("boundary")) {
|
|
675
|
-
nextRow
|
|
168
|
+
nextRow.boundary = 1;
|
|
676
169
|
}
|
|
677
170
|
}
|
|
678
171
|
return result;
|
|
@@ -756,6 +249,22 @@ var AsyncIteratorWrapperImpl = class {
|
|
|
756
249
|
constructor(source) {
|
|
757
250
|
this.source = source;
|
|
758
251
|
}
|
|
252
|
+
chunked(size) {
|
|
253
|
+
return streamOf(this.__chunked(size));
|
|
254
|
+
}
|
|
255
|
+
async *__chunked(size) {
|
|
256
|
+
let chunk = [];
|
|
257
|
+
for await (const item of this.source) {
|
|
258
|
+
chunk.push(item);
|
|
259
|
+
if (chunk.length === size) {
|
|
260
|
+
yield chunk;
|
|
261
|
+
chunk = [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (chunk.length > 0) {
|
|
265
|
+
yield chunk;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
759
268
|
get() {
|
|
760
269
|
return this.source;
|
|
761
270
|
}
|
|
@@ -848,7 +357,6 @@ var graph;
|
|
|
848
357
|
child.value = [];
|
|
849
358
|
if (child.size === 0) {
|
|
850
359
|
delete graphNode.children[k];
|
|
851
|
-
console.error("delete", graphNode.path, k);
|
|
852
360
|
result = true;
|
|
853
361
|
}
|
|
854
362
|
}
|
|
@@ -868,7 +376,6 @@ var graph;
|
|
|
868
376
|
graphNode.value.push(...flatten(child));
|
|
869
377
|
graphNode.size++;
|
|
870
378
|
delete graphNode.children[k];
|
|
871
|
-
console.error("delete", graphNode.path, k);
|
|
872
379
|
return true;
|
|
873
380
|
}
|
|
874
381
|
return false;
|
|
@@ -911,10 +418,10 @@ function clusterFiles(files, clusterMaxSize, clusterMinSize) {
|
|
|
911
418
|
}));
|
|
912
419
|
return subclusters.map((cluster) => {
|
|
913
420
|
let files2 = cluster.files;
|
|
914
|
-
let
|
|
421
|
+
let path6 = cluster.path;
|
|
915
422
|
let files1 = files2.map((it) => it.str);
|
|
916
423
|
return {
|
|
917
|
-
path:
|
|
424
|
+
path: path6.join("/"),
|
|
918
425
|
files: files1.sort(),
|
|
919
426
|
weight: files1.length,
|
|
920
427
|
isLeftovers: cluster.isLeftovers
|
|
@@ -927,38 +434,178 @@ async function* distinctCount(source) {
|
|
|
927
434
|
const map = /* @__PURE__ */ new Map();
|
|
928
435
|
for await (const row of source) {
|
|
929
436
|
const key = JSON.stringify(row);
|
|
437
|
+
let count = row?.count ?? 1;
|
|
930
438
|
if (map.has(key)) {
|
|
931
|
-
map.get(key).count +=
|
|
439
|
+
map.get(key).count += count;
|
|
932
440
|
} else {
|
|
933
|
-
map.set(key, { row, count
|
|
441
|
+
map.set(key, { row, count });
|
|
934
442
|
}
|
|
935
443
|
}
|
|
936
444
|
for (const { row, count } of map.values()) {
|
|
937
|
-
yield [
|
|
445
|
+
yield [row, count];
|
|
938
446
|
}
|
|
939
447
|
}
|
|
940
448
|
|
|
941
|
-
// src/
|
|
942
|
-
var
|
|
449
|
+
// src/discovery.ts
|
|
450
|
+
var import_fs2 = __toESM(require("fs"));
|
|
451
|
+
var import_path2 = __toESM(require("path"));
|
|
452
|
+
|
|
453
|
+
// src/util/util.ts
|
|
454
|
+
var util;
|
|
455
|
+
((util2) => {
|
|
456
|
+
function distinct(arr) {
|
|
457
|
+
return [...new Set(arr)];
|
|
458
|
+
}
|
|
459
|
+
util2.distinct = distinct;
|
|
460
|
+
function daysAgo(epoch) {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
const diff = now - epoch * 1e3;
|
|
463
|
+
return Math.floor(diff / (1e3 * 60 * 60 * 24));
|
|
464
|
+
}
|
|
465
|
+
util2.daysAgo = daysAgo;
|
|
466
|
+
function bucket(n, buckets) {
|
|
467
|
+
for (let i = 1; i < buckets.length; i++) {
|
|
468
|
+
if (n > buckets[i - 1] && n < buckets[i]) return buckets[i - 1];
|
|
469
|
+
}
|
|
470
|
+
return -1;
|
|
471
|
+
}
|
|
472
|
+
util2.bucket = bucket;
|
|
473
|
+
function yyyyMM(epoch) {
|
|
474
|
+
const date = new Date(epoch * 1e3);
|
|
475
|
+
let yyyyStr = date.getFullYear().toString();
|
|
476
|
+
let MMStr = (date.getMonth() + 1 / 4).toString().padStart(1, "0");
|
|
477
|
+
return parseInt(yyyyStr) * 10 + parseInt(MMStr);
|
|
478
|
+
}
|
|
479
|
+
util2.yyyyMM = yyyyMM;
|
|
480
|
+
})(util || (util = {}));
|
|
481
|
+
|
|
482
|
+
// src/discovery.ts
|
|
943
483
|
function getDirectories(absoluteDirPath) {
|
|
944
|
-
if (!
|
|
484
|
+
if (!import_fs2.default.existsSync(absoluteDirPath) || !import_fs2.default.statSync(absoluteDirPath).isDirectory()) return [];
|
|
945
485
|
const ignoredDirs = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
946
486
|
try {
|
|
947
|
-
return
|
|
487
|
+
return import_fs2.default.readdirSync(absoluteDirPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory() && !ignoredDirs.has(dirent.name)).map((dirent) => import_path2.default.join(absoluteDirPath, dirent.name));
|
|
948
488
|
} catch (error) {
|
|
949
489
|
console.error(`Could not read directory: ${absoluteDirPath}`);
|
|
950
490
|
return [];
|
|
951
491
|
}
|
|
952
492
|
}
|
|
953
|
-
|
|
493
|
+
function findRepositories(absolutePath, depth) {
|
|
494
|
+
if (depth <= 0) return [];
|
|
495
|
+
if (!import_path2.default.isAbsolute(absolutePath)) throw new Error(`Path must be absolute: ${absolutePath}`);
|
|
496
|
+
if (!import_fs2.default.existsSync(absolutePath)) throw new Error(`Path does not exist: ${absolutePath}`);
|
|
497
|
+
if (!import_fs2.default.statSync(absolutePath).isDirectory()) throw new Error(`Path is not a directory: ${absolutePath}`);
|
|
498
|
+
if (isGitRepo(absolutePath)) return [absolutePath];
|
|
499
|
+
let result = getDirectories(absolutePath).flatMap((dir) => findRepositories(dir, depth - 1));
|
|
500
|
+
return util.distinct(result).sort();
|
|
501
|
+
}
|
|
502
|
+
function getRepoPathsToProcess(inputPaths) {
|
|
503
|
+
let repos = inputPaths.map((it) => import_path2.default.resolve(it)).flatMap((it) => findRepositories(it, 3));
|
|
504
|
+
return util.distinct(repos).sort();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/progress.ts
|
|
508
|
+
var process2 = __toESM(require("node:process"));
|
|
509
|
+
var Progress = class {
|
|
510
|
+
constructor() {
|
|
511
|
+
this.progress = {};
|
|
512
|
+
this.messages = {};
|
|
513
|
+
this.startTime = {};
|
|
514
|
+
this.currentInterval = void 0;
|
|
515
|
+
}
|
|
516
|
+
setProgress(name, current, max = void 0) {
|
|
517
|
+
this.progress[name] = [current, max ?? this.progress[name]?.[1] ?? void 0];
|
|
518
|
+
if (!this.startTime[name]) this.startTime[name] = Date.now();
|
|
519
|
+
}
|
|
520
|
+
setMessage(name, message) {
|
|
521
|
+
this.messages[name] = message;
|
|
522
|
+
}
|
|
523
|
+
stop(name) {
|
|
524
|
+
delete this.progress[name];
|
|
525
|
+
delete this.messages[name];
|
|
526
|
+
delete this.startTime[name];
|
|
527
|
+
}
|
|
528
|
+
destroy() {
|
|
529
|
+
Object.keys(this.progress).forEach((key) => this.stop(key));
|
|
530
|
+
clearInterval(this.currentInterval);
|
|
531
|
+
}
|
|
532
|
+
showProgress(period) {
|
|
533
|
+
this.currentInterval = setInterval(() => {
|
|
534
|
+
const now = Date.now();
|
|
535
|
+
const progress2 = Object.entries(this.progress).map(([key, [value, max]]) => {
|
|
536
|
+
let eta = "?";
|
|
537
|
+
const startTime = this.startTime[key];
|
|
538
|
+
if (startTime && max !== void 0) {
|
|
539
|
+
const elapsed = (now - startTime) / 1e3;
|
|
540
|
+
const rate = value / elapsed;
|
|
541
|
+
const remaining = max - value;
|
|
542
|
+
const etaSeconds = Math.round(remaining / rate);
|
|
543
|
+
const days = Math.floor(etaSeconds / 86400);
|
|
544
|
+
const hours = Math.floor(etaSeconds % 86400 / 3600);
|
|
545
|
+
const minutes = Math.floor(etaSeconds % 3600 / 60);
|
|
546
|
+
const seconds = etaSeconds % 60;
|
|
547
|
+
const parts = [];
|
|
548
|
+
if (days > 0) parts.push(`${days}d`);
|
|
549
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
550
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
551
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
552
|
+
eta = ` ETA: ${parts.join("")}`;
|
|
553
|
+
}
|
|
554
|
+
return `${key}: [${value}/${max ?? "?"}]${eta} ${this.messages[key] ?? ""}`;
|
|
555
|
+
}).join(", ");
|
|
556
|
+
if (progress2.length === 0) return;
|
|
557
|
+
process2.stderr.clearLine(0, () => {
|
|
558
|
+
process2.stderr.write("\r" + progress2);
|
|
559
|
+
});
|
|
560
|
+
}, period);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/vfs.ts
|
|
565
|
+
var import_fs3 = __toESM(require("fs"));
|
|
566
|
+
var import_path3 = __toESM(require("path"));
|
|
567
|
+
var RealFileSystemImpl = class {
|
|
568
|
+
constructor(basePath) {
|
|
569
|
+
this.basePath = import_path3.default.normalize(basePath);
|
|
570
|
+
}
|
|
571
|
+
async read(filePath) {
|
|
572
|
+
return import_fs3.default.readFileSync(this.resolve(filePath), "utf8");
|
|
573
|
+
}
|
|
574
|
+
async write(filePath, content) {
|
|
575
|
+
let path1 = import_path3.default.resolve(this.resolve(filePath), "..");
|
|
576
|
+
import_fs3.default.mkdirSync(path1, { recursive: true });
|
|
577
|
+
import_fs3.default.writeFileSync(this.resolve(filePath), content);
|
|
578
|
+
}
|
|
579
|
+
async append(filePath, content) {
|
|
580
|
+
return new Promise((resolve2, reject) => {
|
|
581
|
+
import_fs3.default.appendFile(this.resolve(filePath), content, (err) => {
|
|
582
|
+
if (!err) {
|
|
583
|
+
resolve2(void 0);
|
|
584
|
+
} else {
|
|
585
|
+
reject(err);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
resolve(filePath) {
|
|
591
|
+
return import_path3.default.normalize(`${this.basePath}/${filePath}`);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/index.ts
|
|
596
|
+
var sigintCaught = false;
|
|
597
|
+
var progress = null;
|
|
598
|
+
var dataDir = new RealFileSystemImpl("./.git-stats");
|
|
599
|
+
async function* getRepositoryFiles(repoRelativePath) {
|
|
954
600
|
console.error(`
|
|
955
601
|
Processing repository: ${repoRelativePath || "."}`);
|
|
956
|
-
const absoluteRepoPath =
|
|
957
|
-
const repoName =
|
|
958
|
-
let revisionBoundary = await findRevision(absoluteRepoPath,
|
|
602
|
+
const absoluteRepoPath = path5.resolve(process.cwd(), repoRelativePath);
|
|
603
|
+
const repoName = path5.basename(absoluteRepoPath);
|
|
604
|
+
let revisionBoundary = await findRevision(absoluteRepoPath, 1e3);
|
|
959
605
|
const files = await git_ls_files(absoluteRepoPath, ".");
|
|
960
606
|
let minClusterSize = Math.floor(Math.max(2, files.length / 100));
|
|
961
607
|
let maxClusterSize = Math.round(Math.max(20, files.length / 30));
|
|
608
|
+
console.error(`Found ${files.length} files to analyze in '${repoName}'...`);
|
|
962
609
|
console.error(`Clustering ${files.length} into ${minClusterSize}..${maxClusterSize}+ sized chunks`);
|
|
963
610
|
const filesClustered = clusterFiles(
|
|
964
611
|
files,
|
|
@@ -966,69 +613,72 @@ Processing repository: ${repoRelativePath || "."}`);
|
|
|
966
613
|
minClusterSize
|
|
967
614
|
);
|
|
968
615
|
console.error(filesClustered.map((it) => `${it.path} (${it.weight})`));
|
|
969
|
-
let
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
616
|
+
let filesShuffled = filesClustered.flatMap(
|
|
617
|
+
(cluster) => cluster.files.flatMap((file) => ({
|
|
618
|
+
file,
|
|
619
|
+
cluster: cluster.path
|
|
620
|
+
}))
|
|
621
|
+
);
|
|
622
|
+
for (let i = filesShuffled.length - 1; i > 0; i--) {
|
|
623
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
624
|
+
[filesShuffled[i], filesShuffled[j]] = [filesShuffled[j], filesShuffled[i]];
|
|
625
|
+
}
|
|
626
|
+
for (let i = 0; i < filesShuffled.length; i++) {
|
|
973
627
|
if (sigintCaught) break;
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
628
|
+
const currentFile = filesShuffled[i];
|
|
629
|
+
progress?.setProgress("File", i + 1, files.length);
|
|
630
|
+
progress?.setMessage("File", currentFile.file);
|
|
631
|
+
yield {
|
|
632
|
+
repo: path5.basename(absoluteRepoPath),
|
|
633
|
+
file: currentFile.file,
|
|
634
|
+
rev: revisionBoundary,
|
|
635
|
+
cluster: currentFile.cluster
|
|
636
|
+
};
|
|
983
637
|
}
|
|
984
638
|
process.stderr.write(" ".repeat(process.stderr.columns || 80) + "\r");
|
|
985
639
|
console.error(`Analysis complete for '${repoName}'.`);
|
|
986
640
|
}
|
|
987
641
|
async function doProcessFile(absoluteRepoRoot, repoRelativeFilePath, revisionBoundary) {
|
|
988
642
|
if (!repoRelativeFilePath) return [];
|
|
989
|
-
const absoluteFilePath =
|
|
643
|
+
const absoluteFilePath = path5.join(absoluteRepoRoot, repoRelativeFilePath);
|
|
990
644
|
let stat = null;
|
|
991
645
|
try {
|
|
992
|
-
stat =
|
|
646
|
+
stat = fs5.statSync(absoluteFilePath);
|
|
993
647
|
} catch (e) {
|
|
994
648
|
console.error(`Fail get stats for file ${absoluteFilePath}`, e.stack || e.message || e);
|
|
995
649
|
}
|
|
996
650
|
if (!stat || !stat.isFile() || stat.size === 0) return [];
|
|
997
651
|
const result = [];
|
|
998
652
|
for (const item of await git_blame_porcelain(repoRelativeFilePath, absoluteRepoRoot, ["author", "committer-time", "commit"], !!revisionBoundary ? revisionBoundary + "..HEAD" : void 0)) {
|
|
999
|
-
if (revisionBoundary === item
|
|
1000
|
-
item
|
|
1001
|
-
item
|
|
1002
|
-
item
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
653
|
+
if (revisionBoundary === item.commit) {
|
|
654
|
+
item.author = "Legacy";
|
|
655
|
+
item.time = 0;
|
|
656
|
+
item.commit = "0".repeat(40);
|
|
657
|
+
}
|
|
658
|
+
result.push({
|
|
659
|
+
repo: absoluteRepoRoot,
|
|
660
|
+
file: path5.basename(repoRelativeFilePath),
|
|
661
|
+
author: item.author,
|
|
662
|
+
commit: item.commit,
|
|
663
|
+
time: item.time,
|
|
664
|
+
year: new Date(item.time * 1e3).getFullYear(),
|
|
665
|
+
month: new Date(item.time * 1e3).getMonth() + 1,
|
|
666
|
+
lang: path5.extname(repoRelativeFilePath) || "Other"
|
|
667
|
+
});
|
|
1009
668
|
}
|
|
1010
669
|
return result;
|
|
1011
670
|
}
|
|
1012
|
-
function
|
|
1013
|
-
let repoPathsToProcess = inputPaths.flatMap((it) => findRepositories(it, 3));
|
|
1014
|
-
repoPathsToProcess = util.distinct(repoPathsToProcess).sort();
|
|
1015
|
-
if (repoPathsToProcess.length === 0) {
|
|
1016
|
-
throw new Error("No git repositories found to analyze.");
|
|
1017
|
-
}
|
|
1018
|
-
console.error(`Found ${repoPathsToProcess.length} repositories to analyze:`);
|
|
1019
|
-
repoPathsToProcess.forEach((p) => console.error(`- ${p ?? "."}`));
|
|
1020
|
-
return repoPathsToProcess;
|
|
1021
|
-
}
|
|
1022
|
-
function runScan1(args) {
|
|
671
|
+
async function runScan1(args) {
|
|
1023
672
|
const inputPaths = args && args.length > 0 ? args : ["."];
|
|
1024
673
|
let repoPathsToProcess = getRepoPathsToProcess(inputPaths);
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
return linesInfo.map((it) => it.concat(fileInfo[3])).get();
|
|
1028
|
-
}).map((it) => [it[0], it[1], it[2], it[5], path3.basename(it[4])]).get();
|
|
674
|
+
await dataDir.write("known_repositories.txt", repoPathsToProcess.join("\n") + "\n");
|
|
675
|
+
let dataSet = streamOf(AsyncGeneratorUtil.of(repoPathsToProcess)).flatMap((repoRelativePath) => getRepositoryFiles(repoRelativePath)).flatMap((fileInfo) => stream.ofArrayPromise(doProcessFile(fileInfo.repo, fileInfo.file, fileInfo.rev)).get()).map((it) => [it.author, it.time, it.lang, it.cluster, it.repo]).get();
|
|
1029
676
|
return distinctCount(dataSet);
|
|
1030
677
|
}
|
|
1031
678
|
async function runScan(args) {
|
|
679
|
+
let [keys, paths] = extractArgKeys(args);
|
|
680
|
+
progress = new Progress();
|
|
681
|
+
progress.showProgress(300);
|
|
1032
682
|
process.on("SIGINT", () => {
|
|
1033
683
|
if (sigintCaught) {
|
|
1034
684
|
console.error("\nForcing exit.");
|
|
@@ -1037,19 +687,24 @@ async function runScan(args) {
|
|
|
1037
687
|
sigintCaught = true;
|
|
1038
688
|
console.error("\nSignal received. Finishing current file then stopping. Press Ctrl+C again to exit immediately.");
|
|
1039
689
|
});
|
|
1040
|
-
let aggregatedData1 = runScan1(
|
|
690
|
+
let aggregatedData1 = await runScan1(paths);
|
|
1041
691
|
let aggregatedData = await AsyncGeneratorUtil.collect(aggregatedData1);
|
|
1042
|
-
|
|
692
|
+
progress.destroy();
|
|
693
|
+
if (keys.includes("stdout")) {
|
|
694
|
+
aggregatedData.forEach((it) => console.log(JSON.stringify(it)));
|
|
695
|
+
} else {
|
|
696
|
+
aggregatedData.forEach((it) => dataDir.append("data.jsonl", JSON.stringify(it) + "\n"));
|
|
697
|
+
}
|
|
1043
698
|
}
|
|
1044
699
|
async function runHtml(args) {
|
|
1045
|
-
const absoluteInputPath = args[0] ||
|
|
1046
|
-
const absoluteOutHtml =
|
|
1047
|
-
if (!
|
|
700
|
+
const absoluteInputPath = args[0] || path5.resolve("./.git-stats/data.jsonl");
|
|
701
|
+
const absoluteOutHtml = path5.resolve("./.git-stats/report.html");
|
|
702
|
+
if (!fs5.existsSync(absoluteInputPath)) {
|
|
1048
703
|
console.error(`Input data file not found: ${absoluteInputPath}`);
|
|
1049
704
|
process.exitCode = 1;
|
|
1050
705
|
return;
|
|
1051
706
|
}
|
|
1052
|
-
const lines =
|
|
707
|
+
const lines = fs5.readFileSync(absoluteInputPath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
1053
708
|
const aggregatedData = lines.map((line) => {
|
|
1054
709
|
try {
|
|
1055
710
|
return JSON.parse(line);
|
|
@@ -1060,13 +715,51 @@ async function runHtml(args) {
|
|
|
1060
715
|
generateHtmlReport(aggregatedData, absoluteOutHtml);
|
|
1061
716
|
console.error(`HTML report generated: ${absoluteOutHtml}`);
|
|
1062
717
|
}
|
|
1063
|
-
function
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
718
|
+
async function* forEachStdinLine(consumer) {
|
|
719
|
+
const rl = readline.createInterface({
|
|
720
|
+
input: process.stdin,
|
|
721
|
+
output: process.stdout,
|
|
722
|
+
terminal: false
|
|
723
|
+
});
|
|
724
|
+
for await (const line of rl) {
|
|
725
|
+
if (!line.trim()) continue;
|
|
726
|
+
try {
|
|
727
|
+
consumer(line);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
console.error(`Error parsing line: ${line}`, error);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
yield null;
|
|
733
|
+
}
|
|
734
|
+
async function runSlice(args) {
|
|
735
|
+
let cols = args.map((it) => parseInt(it));
|
|
736
|
+
let result = {};
|
|
737
|
+
await forEachStdinLine((it) => {
|
|
738
|
+
const data = JSON.parse(it);
|
|
739
|
+
console.log(data);
|
|
740
|
+
let n = result;
|
|
741
|
+
for (let col of cols) {
|
|
742
|
+
n[data[col]] = n?.[data[col]] ?? {};
|
|
743
|
+
n = n[data[col]];
|
|
744
|
+
n.count = (n?.count ?? 0) + data[data.length - 1];
|
|
745
|
+
n.values = n?.values ?? {};
|
|
746
|
+
n = n.values;
|
|
747
|
+
}
|
|
748
|
+
}).next();
|
|
749
|
+
console.log(JSON.stringify(result, null, 2));
|
|
750
|
+
}
|
|
751
|
+
function extractArgKeys(args) {
|
|
752
|
+
const keys = [];
|
|
753
|
+
const values = [];
|
|
754
|
+
for (let i = 0; i < args.length; i++) {
|
|
755
|
+
const arg = args[i];
|
|
756
|
+
if (arg.startsWith("--")) {
|
|
757
|
+
keys.push(arg.substring(2));
|
|
758
|
+
} else {
|
|
759
|
+
values.push(arg);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return [keys, values];
|
|
1070
763
|
}
|
|
1071
764
|
async function main() {
|
|
1072
765
|
const argv = process.argv.slice(2);
|
|
@@ -1089,6 +782,10 @@ async function main() {
|
|
|
1089
782
|
await runHtml(argv.slice(1));
|
|
1090
783
|
return;
|
|
1091
784
|
}
|
|
785
|
+
if (subcommand === "slice") {
|
|
786
|
+
await runSlice(argv.slice(1));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
1092
789
|
console.error(`Usage: git-stats <subcommand> [args]
|
|
1093
790
|
|
|
1094
791
|
Available subcommands:`);
|
|
@@ -1097,31 +794,10 @@ Available subcommands:`);
|
|
|
1097
794
|
Usage: ${usage}`);
|
|
1098
795
|
}
|
|
1099
796
|
}
|
|
1100
|
-
var util;
|
|
1101
|
-
((util2) => {
|
|
1102
|
-
function distinct(arr) {
|
|
1103
|
-
return [...new Set(arr)];
|
|
1104
|
-
}
|
|
1105
|
-
util2.distinct = distinct;
|
|
1106
|
-
function daysAgo(epoch) {
|
|
1107
|
-
const now = Date.now();
|
|
1108
|
-
const diff = now - epoch * 1e3;
|
|
1109
|
-
return Math.floor(diff / (1e3 * 60 * 60 * 24));
|
|
1110
|
-
}
|
|
1111
|
-
util2.daysAgo = daysAgo;
|
|
1112
|
-
function bucket(n, buckets) {
|
|
1113
|
-
for (let i = 1; i < buckets.length; i++) {
|
|
1114
|
-
if (n > buckets[i - 1] && n < buckets[i]) return buckets[i - 1];
|
|
1115
|
-
}
|
|
1116
|
-
return -1;
|
|
1117
|
-
}
|
|
1118
|
-
util2.bucket = bucket;
|
|
1119
|
-
function yyyyMM(epoch) {
|
|
1120
|
-
const date = new Date(epoch * 1e3);
|
|
1121
|
-
let yyyyStr = date.getFullYear().toString();
|
|
1122
|
-
let MMStr = (date.getMonth() + 1 / 4).toString().padStart(1, "0");
|
|
1123
|
-
return parseInt(yyyyStr) * 10 + parseInt(MMStr);
|
|
1124
|
-
}
|
|
1125
|
-
util2.yyyyMM = yyyyMM;
|
|
1126
|
-
})(util || (util = {}));
|
|
1127
797
|
main().catch(console.error);
|
|
798
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
799
|
+
0 && (module.exports = {
|
|
800
|
+
dataDir,
|
|
801
|
+
progress,
|
|
802
|
+
runScan1
|
|
803
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vladimirshefer/git-stats",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A CLI to generate git blame stats for a repository",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^20.0.0",
|
|
24
|
+
"@vladimirshefer/git-stats--html-ui": "*",
|
|
24
25
|
"vitest": "^1.6.0",
|
|
25
|
-
"esbuild": "0.25.
|
|
26
|
+
"esbuild": "0.25.2",
|
|
26
27
|
"typescript": "^5.0.0"
|
|
27
28
|
}
|
|
28
29
|
}
|