git-truck 0.0.0-de7c4e6 → 0.0.0-e01892a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +44 -90
  3. package/build/client/assets/GitTruckInfo-un_3Kok4.js +1 -0
  4. package/build/client/assets/LoadingIndicator-SOpPzvbS.js +1 -0
  5. package/build/client/assets/RevisionSelect-SrpO6JPf.js +1 -0
  6. package/build/client/assets/Tooltip-D_S4TjsA.js +6 -0
  7. package/build/client/assets/_index-CUlJmeRb.js +1 -0
  8. package/build/client/assets/_repo._-1fXsj0hR.js +18 -0
  9. package/build/client/assets/_repository-CZYQSN1t.js +0 -0
  10. package/build/client/assets/authordist-BooTyDMX.js +0 -0
  11. package/build/client/assets/clear-cache-Ckh4wcip.js +1 -0
  12. package/build/client/assets/clear-cache-D0olKoRl.js +1 -0
  13. package/build/client/assets/commitcount-B_3CeIvr.js +0 -0
  14. package/build/client/assets/commits-DNZ_hn-X.js +0 -0
  15. package/build/client/assets/entry.client-D1dRnGqd.js +9 -0
  16. package/build/client/assets/jsx-runtime-DFd4lFBE.js +26 -0
  17. package/build/client/assets/manifest-4723abac.js +1 -0
  18. package/build/client/assets/progress-DMGX1aEW.js +0 -0
  19. package/build/client/assets/root-DKtEsrVf.js +1 -0
  20. package/build/client/assets/root-igHuhpOR.css +1 -0
  21. package/build/client/assets/ui-C5C1Jexe.js +1 -0
  22. package/build/client/favicon.ico +0 -0
  23. package/build/server/assets/server-build-DEjTAloe.js +12749 -0
  24. package/build/server/assets/util.server-DIX86N1-.js +1822 -0
  25. package/build/server/index.js +193 -0
  26. package/cli.js +35820 -220690
  27. package/package.json +178 -121
  28. package/build/_assets/App-G6V2GHO3.css +0 -3
  29. package/build/_assets/Chart-C7HOIFWY.css +0 -26
  30. package/build/_assets/index-K5E2NP2H.css +0 -52
  31. package/build/_assets/vars-TECAP2OK.css +0 -25
  32. package/build/index.js +0 -190549
  33. package/public/build/_assets/App-G6V2GHO3.css +0 -3
  34. package/public/build/_assets/Chart-C7HOIFWY.css +0 -26
  35. package/public/build/_assets/index-K5E2NP2H.css +0 -52
  36. package/public/build/_assets/truck-7F5JWBYT.gif +0 -0
  37. package/public/build/_assets/vars-TECAP2OK.css +0 -25
  38. package/public/build/_shared/chunk-2BPXJFRU.js +0 -67
  39. package/public/build/_shared/chunk-2EHCKJST.js +0 -1
  40. package/public/build/_shared/chunk-FMS45555.js +0 -1
  41. package/public/build/_shared/chunk-XMAWJ27N.js +0 -26
  42. package/public/build/_shared/chunk-Y7QOL3BF.js +0 -238
  43. package/public/build/entry.client-YPWZXPQ4.js +0 -1
  44. package/public/build/manifest-B9DFC470.js +0 -1
  45. package/public/build/root-4FO5NL4Q.js +0 -1
  46. package/public/build/routes/$repo.$-KLZOSV6H.js +0 -350
  47. package/public/build/routes/index-AJLEUMMK.js +0 -61
  48. /package/build/{_assets/truck-7F5JWBYT.gif → client/assets/truck-BgAoc4Gr.gif} +0 -0
@@ -0,0 +1,1822 @@
1
+ import c from "ansi-colors";
2
+ import { createSpinner } from "nanospinner";
3
+ import { exec, spawn } from "node:child_process";
4
+ import { readdir } from "node:fs/promises";
5
+ import path, { join, resolve } from "node:path";
6
+ import { performance } from "node:perf_hooks";
7
+ import { existsSync, promises } from "node:fs";
8
+ import { DuckDBInstance } from "@duckdb/node-api";
9
+ import os from "os";
10
+ import { dirname, resolve as resolve$1 } from "path";
11
+ import { existsSync as existsSync$1, promises as promises$1 } from "fs";
12
+ import { clean, compare, valid } from "semver";
13
+ import colorConvert from "color-convert";
14
+ import "@duckdb/node-api/lib/DuckDBResultReader.js";
15
+ import os$1, { cpus, freemem, totalmem } from "node:os";
16
+ import yargsParser from "yargs-parser";
17
+ const LOG_LEVEL = {
18
+ SILENT: 0,
19
+ ERROR: 1,
20
+ WARN: 2,
21
+ INFO: 3,
22
+ DEBUG: 4
23
+ };
24
+ const LOG_LEVEL_LABEL = {
25
+ SILENT: "",
26
+ ERROR: "ERR",
27
+ WARN: "WRN",
28
+ INFO: "NFO",
29
+ DEBUG: "DBG"
30
+ };
31
+ const { ERROR, WARN, INFO, DEBUG } = LOG_LEVEL_LABEL;
32
+ const stringToLevelMap = {
33
+ SILENT: LOG_LEVEL.SILENT,
34
+ ERROR: LOG_LEVEL.ERROR,
35
+ WARN: LOG_LEVEL.WARN,
36
+ INFO: LOG_LEVEL.INFO,
37
+ DEBUG: LOG_LEVEL.DEBUG
38
+ };
39
+ function setIntialLogLevel() {
40
+ if (typeof process.env.LOG_LEVEL === "string") {
41
+ setTimeout(() => {
42
+ log.debug(`Setting log level to ${process.env.LOG_LEVEL} from environment variable`);
43
+ });
44
+ return stringToLevelMap[process.env.LOG_LEVEL.toUpperCase()];
45
+ }
46
+ if (typeof process.env.LOG_LEVEL === "number") {
47
+ setTimeout(() => {
48
+ log.debug(`Setting log level to ${process.env.LOG_LEVEL} from environment variable`);
49
+ });
50
+ return process.env.LOG_LEVEL;
51
+ }
52
+ return null;
53
+ }
54
+ let logLevel = setIntialLogLevel();
55
+ const getLogLevel = () => logLevel;
56
+ function error(...messages) {
57
+ if (logLevel === null) return;
58
+ if (logLevel >= LOG_LEVEL.ERROR) {
59
+ process.stderr.write(prefix(ERROR));
60
+ console.error(...messages);
61
+ }
62
+ }
63
+ function warn(...messages) {
64
+ if (logLevel === null) return;
65
+ if (logLevel >= LOG_LEVEL.WARN) {
66
+ process.stderr.write(prefix(WARN));
67
+ console.warn(...messages);
68
+ }
69
+ }
70
+ function info(...messages) {
71
+ if (logLevel === null) return;
72
+ if (logLevel >= LOG_LEVEL.INFO) {
73
+ process.stderr.write(prefix(INFO));
74
+ console.info(...messages);
75
+ }
76
+ }
77
+ function time(label) {
78
+ if (logLevel === null) return;
79
+ if (logLevel >= LOG_LEVEL.INFO) console.time(label);
80
+ }
81
+ function timeEnd(label) {
82
+ if (logLevel === null) return;
83
+ if (logLevel >= LOG_LEVEL.INFO) {
84
+ process.stderr.write(prefix(INFO));
85
+ console.timeEnd(label);
86
+ }
87
+ }
88
+ function debug(...messages) {
89
+ if (logLevel === null) return;
90
+ if (logLevel >= LOG_LEVEL.DEBUG) {
91
+ process.stderr.write(prefix(DEBUG));
92
+ console.debug(...messages);
93
+ }
94
+ }
95
+ function prefix(label) {
96
+ const formatPrefix = (label$1, colorFn = (s) => s) => `${colorFn(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} ${label$1} `)} `;
97
+ if (process.env.COLOR === "0") return `[${label}] `;
98
+ switch (label) {
99
+ case LOG_LEVEL_LABEL.ERROR: return formatPrefix(LOG_LEVEL_LABEL.ERROR, c.bgRedBright.black.bold);
100
+ case LOG_LEVEL_LABEL.WARN: return formatPrefix(LOG_LEVEL_LABEL.WARN, c.bgYellow.black.bold);
101
+ case LOG_LEVEL_LABEL.INFO: return formatPrefix(LOG_LEVEL_LABEL.INFO, c.bgBlueBright.black.bold);
102
+ case LOG_LEVEL_LABEL.DEBUG: return formatPrefix(LOG_LEVEL_LABEL.DEBUG, c.bgWhite.bold);
103
+ default: throw Error("Invalid log level");
104
+ }
105
+ }
106
+ const log = {
107
+ error,
108
+ warn,
109
+ info,
110
+ debug,
111
+ time,
112
+ timeEnd
113
+ };
114
+ function dateFormatLong(epochTime) {
115
+ if (!epochTime) return "Invalid date";
116
+ return (/* @__PURE__ */ new Date(epochTime * 1e3)).toLocaleString("en-gb", {
117
+ day: "2-digit",
118
+ month: "short",
119
+ year: "numeric"
120
+ });
121
+ }
122
+ function dateFormatCalendarHeader(epochTime) {
123
+ if (!epochTime) return "Invalid date";
124
+ return new Date(epochTime).toLocaleString("en-gb", {
125
+ month: "long",
126
+ year: "numeric"
127
+ });
128
+ }
129
+ function dateFormatShort(epochTime) {
130
+ return new Date(epochTime).toLocaleString("en-gb", {
131
+ day: "2-digit",
132
+ month: "short",
133
+ year: "2-digit"
134
+ });
135
+ }
136
+ function dateTimeFormatShort(epochTime) {
137
+ return new Date(epochTime).toLocaleString("da-dk", {
138
+ hour: "2-digit",
139
+ minute: "2-digit",
140
+ day: "2-digit",
141
+ month: "short",
142
+ year: "2-digit"
143
+ });
144
+ }
145
+ function dateFormatRelative(epochTime) {
146
+ const now = Date.now();
147
+ const hourMillis = 60 * 60 * 1e3;
148
+ const dayMillis = 24 * hourMillis;
149
+ const difference = now - epochTime * 1e3;
150
+ if (difference < 0) return "Unknown time ago";
151
+ if (difference > dayMillis) {
152
+ const days = Math.floor(difference / dayMillis);
153
+ return `${days} day${days > 1 ? "s" : ""} ago`;
154
+ }
155
+ const hours = Math.floor(difference / hourMillis);
156
+ if (hours > 1) return `${hours} hours ago`;
157
+ if (hours === 1) return "1 hour ago";
158
+ return "<1 hour ago";
159
+ }
160
+ const last = (arr) => arr.at(-1);
161
+ const allExceptLast = (arr) => {
162
+ if (arr.length <= 1) return [];
163
+ return arr.slice(0, arr.length - 1);
164
+ };
165
+ const allExceptFirst = (arr) => {
166
+ if (arr.length <= 1) return [];
167
+ return arr.slice(1);
168
+ };
169
+ function getSeparator(path$1) {
170
+ if (path$1.includes("\\")) return "\\";
171
+ return "/";
172
+ }
173
+ const getPathFromRepoAndHead = (repo, branch) => [repo, encodeURIComponent(branch)].join("/");
174
+ const branchCompare = (a, b) => {
175
+ const defaultBranchNames = ["main", "master"];
176
+ if (defaultBranchNames.includes(a)) return -1;
177
+ if (defaultBranchNames.includes(b)) return 1;
178
+ return a.toLowerCase().localeCompare(b.toLowerCase());
179
+ };
180
+ const semverCompare = (a, b) => {
181
+ const validA = valid(clean(a));
182
+ const validB = valid(clean(b));
183
+ if (!validA || !validB) {
184
+ if (validA) return 1;
185
+ if (validB) return -1;
186
+ return a.toLowerCase().localeCompare(b.toLowerCase());
187
+ }
188
+ return compare(validA, validB);
189
+ };
190
+ const brightnessCalculationCache = /* @__PURE__ */ new Map();
191
+ function weightedDistanceIn3D(hex) {
192
+ const rgb = hexToRgb(hex);
193
+ return Math.sqrt(Math.pow(rgb[0], 2) * .241 + Math.pow(rgb[1], 2) * .691 + Math.pow(rgb[2], 2) * .068);
194
+ }
195
+ const hexToRgbCache = /* @__PURE__ */ new Map();
196
+ function hexToRgb(hexString) {
197
+ const cachedColor = hexToRgbCache.get(hexString);
198
+ if (cachedColor) return cachedColor;
199
+ const rgb = colorConvert.hex.rgb(hexString);
200
+ hexToRgbCache.set(hexString, rgb);
201
+ return rgb;
202
+ }
203
+ const isDarkColor = (color) => {
204
+ if (!/^#([0-9A-F]{3}){1,2}$/i.test(color)) throw new Error(`Invalid hex color: ${color}`);
205
+ const cachedColor = brightnessCalculationCache.get(color);
206
+ if (cachedColor !== void 0) return cachedColor;
207
+ const brightness = weightedDistanceIn3D(color);
208
+ const isDark = brightness < 186;
209
+ brightnessCalculationCache.set(color, isDark);
210
+ return isDark;
211
+ };
212
+ const colorCache = /* @__PURE__ */ new Map();
213
+ function hslToHex(h, s, l) {
214
+ const key = `${h}-${s}-${l}`;
215
+ const cachedColor = colorCache.get(key);
216
+ if (cachedColor) return cachedColor;
217
+ const hex = `#${colorConvert.hsl.hex([
218
+ h,
219
+ s,
220
+ l
221
+ ])}`;
222
+ colorCache.set(key, hex);
223
+ return hex;
224
+ }
225
+ function getLightness(hex) {
226
+ return weightedDistanceIn3D(hex) / 255;
227
+ }
228
+ const isTree = (d = null) => d?.type === "tree";
229
+ const isBlob = (d = null) => d?.type === "blob";
230
+ function generateVersionComparisonLink({ currentVersion, latestVersion: latestVersion$1 }) {
231
+ return `https://github.com/git-truck/git-truck/compare/v${currentVersion}...v${latestVersion$1}`;
232
+ }
233
+ function invariant(condition, message) {
234
+ if (!condition) throw new Error(message);
235
+ }
236
+ function sleep(ms) {
237
+ return new Promise((resolve$2) => {
238
+ setTimeout(resolve$2, ms);
239
+ });
240
+ }
241
+ function getWeek(date) {
242
+ const tempDate = new Date(date);
243
+ tempDate.setHours(0, 0, 0, 0);
244
+ tempDate.setDate(tempDate.getDate() + 4 - (tempDate.getDay() || 7));
245
+ const yearStart = new Date(tempDate.getFullYear(), 0, 1);
246
+ const weekNo = Math.ceil(((tempDate.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
247
+ return weekNo;
248
+ }
249
+ function getTimeIntervals(timeUnit, minTime, maxTime) {
250
+ const intervals = [];
251
+ const startDate = /* @__PURE__ */ new Date(minTime * 1e3);
252
+ const endDate = /* @__PURE__ */ new Date(maxTime * 1e3);
253
+ const currentDate = new Date(startDate);
254
+ while (currentDate <= endDate) {
255
+ const currTime = currentDate.getTime() / 1e3;
256
+ if (timeUnit === "week") {
257
+ const weekNum = getWeek(currentDate);
258
+ intervals.push([`Week ${weekNum < 10 ? "0" : ""}${weekNum} ${currentDate.getFullYear()}`, currTime]);
259
+ currentDate.setDate(currentDate.getDate() + 7);
260
+ } else if (timeUnit === "year") {
261
+ intervals.push([currentDate.getFullYear().toString(), currTime]);
262
+ currentDate.setFullYear(currentDate.getFullYear() + 1);
263
+ } else if (timeUnit === "month") {
264
+ intervals.push([currentDate.toLocaleString("en-gb", {
265
+ month: "long",
266
+ year: "numeric"
267
+ }), currTime]);
268
+ currentDate.setMonth(currentDate.getMonth() + 1);
269
+ } else if (timeUnit === "day") {
270
+ intervals.push([currentDate.toLocaleDateString("en-gb", {
271
+ day: "numeric",
272
+ month: "long",
273
+ year: "numeric",
274
+ weekday: "short"
275
+ }).replace(",", ""), currTime]);
276
+ currentDate.setDate(currentDate.getDate() + 1);
277
+ }
278
+ }
279
+ return intervals;
280
+ }
281
+ function analyzeRenamedFile(file, timestamp, authortime, renamedFiles, repo) {
282
+ const movedFileRegex = /(?:.*{(?<oldPath>.*)\s=>\s(?<newPath>.*)}.*)|(?:^(?<oldPath2>.*) => (?<newPath2>.*))$/gm;
283
+ const replaceRegex = /{.*}/gm;
284
+ const match = movedFileRegex.exec(file);
285
+ const groups = match?.groups ?? {};
286
+ let oldPath;
287
+ let newPath;
288
+ if (groups["oldPath"] || groups["newPath"]) {
289
+ const oldP = groups["oldPath"] ?? "";
290
+ const newP = groups["newPath"] ?? "";
291
+ oldPath = repo + "/" + file.replace(replaceRegex, oldP).replace("//", "/");
292
+ newPath = repo + "/" + file.replace(replaceRegex, newP).replace("//", "/");
293
+ } else {
294
+ oldPath = repo + "/" + (groups["oldPath2"] ?? "");
295
+ newPath = repo + "/" + (groups["newPath2"] ?? "");
296
+ }
297
+ renamedFiles.push({
298
+ fromname: oldPath,
299
+ toname: newPath,
300
+ timestamp,
301
+ timestampauthor: authortime
302
+ });
303
+ return newPath;
304
+ }
305
+ const formatMs = (ms) => {
306
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
307
+ else return `${(ms / 1e3).toFixed(2)}s`;
308
+ };
309
+ /**
310
+ * This functions handles try / catch for you, so your code stays flat.
311
+ * @param promise An async function
312
+ * @returns A tuple of the result and an error. If there is no error, the error will be null.
313
+ */
314
+ async function promiseHelper(promise) {
315
+ try {
316
+ return [await promise, null];
317
+ } catch (e) {
318
+ return [null, e];
319
+ }
320
+ }
321
+ const numToFriendlyString = (num) => {
322
+ if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
323
+ else if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
324
+ else return num.toString();
325
+ };
326
+ var DB = class DB {
327
+ connectionPromise;
328
+ repoSanitized;
329
+ branchSanitized;
330
+ selectedRange;
331
+ static async init(dbPath) {
332
+ const dir = dirname(dbPath);
333
+ if (!existsSync$1(dir)) await promises$1.mkdir(dir, { recursive: true });
334
+ const instance = await DuckDBInstance.create(dbPath, { temp_directory: dir });
335
+ const connection = await instance.connect();
336
+ await this.initTables(connection);
337
+ await this.initViews(connection, 0, 1e12);
338
+ return connection;
339
+ }
340
+ static async clearCache() {
341
+ const dir = resolve$1(os.tmpdir(), "git-truck-cache");
342
+ if (!existsSync$1(dir)) return;
343
+ await promises$1.rm(dir, {
344
+ recursive: true,
345
+ force: true
346
+ });
347
+ }
348
+ constructor(repo, branch) {
349
+ this.repoSanitized = repo.replace(/\W/g, "_") + "_";
350
+ this.branchSanitized = branch.replace(/\W/g, "_") + "_";
351
+ const dbPath = resolve$1(os.tmpdir(), "git-truck-cache", this.repoSanitized, this.branchSanitized + ".db");
352
+ this.connectionPromise = DB.init(dbPath);
353
+ this.selectedRange = [0, 1e12];
354
+ }
355
+ async disconnect() {
356
+ const connection = await this.connectionPromise;
357
+ connection.disconnectSync();
358
+ }
359
+ async query(query) {
360
+ return (await (await this.connectionPromise).runAndReadAll(query)).getRowObjects();
361
+ }
362
+ async run(query) {
363
+ await (await this.connectionPromise).run(query);
364
+ }
365
+ /**
366
+
367
+ * usingTableAppender is a helper function to create an appender and close it after the callback is done.
368
+
369
+ * It is useful for inserting large amounts of data into the database efficiently.
370
+
371
+ * @param table The table to append to
372
+
373
+ * @param callback The callback that will be called with an appender
374
+
375
+ * @returns The result of the callback
376
+
377
+ */
378
+ async usingTableAppender(table, callback) {
379
+ const appender = await (await this.connectionPromise).createAppender(table);
380
+ try {
381
+ await callback(appender);
382
+ } finally {
383
+ appender.closeSync();
384
+ }
385
+ }
386
+ async checkpoint() {
387
+ await this.run("CHECKPOINT;");
388
+ }
389
+ static async initTables(db) {
390
+ await db.run(`
391
+ CREATE TABLE IF NOT EXISTS commits (
392
+ hash VARCHAR,
393
+ author VARCHAR,
394
+ committertime UINTEGER,
395
+ authortime UINTEGER
396
+ );
397
+ CREATE TABLE IF NOT EXISTS filechanges (
398
+ commithash VARCHAR,
399
+ insertions UINTEGER,
400
+ deletions UINTEGER,
401
+ filepath VARCHAR,
402
+ );
403
+ CREATE TABLE IF NOT EXISTS authorunions (
404
+ alias VARCHAR PRIMARY KEY,
405
+ actualname VARCHAR
406
+ );
407
+ CREATE TABLE IF NOT EXISTS renames (
408
+ fromname VARCHAR,
409
+ toname VARCHAR,
410
+ timestamp UINTEGER,
411
+ timestampauthor UINTEGER
412
+ );
413
+ CREATE TABLE IF NOT EXISTS hiddenfiles (
414
+ path VARCHAR
415
+ );
416
+ CREATE TABLE IF NOT EXISTS metadata (
417
+ field VARCHAR,
418
+ value UBIGINT,
419
+ value2 VARCHAR
420
+ );
421
+ CREATE TABLE IF NOT EXISTS temporary_renames (
422
+ fromname VARCHAR,
423
+ toname VARCHAR,
424
+ timestamp UINTEGER,
425
+ timestampend UINTEGER
426
+ );
427
+ CREATE TABLE IF NOT EXISTS files (
428
+ path VARCHAR
429
+ );
430
+ `);
431
+ }
432
+ async clearAllTables() {
433
+ await this.run(`
434
+ DELETE FROM commits;
435
+ DELETE FROM filechanges;
436
+ DELETE FROM authorunions;
437
+ DELETE FROM renames;
438
+ DELETE FROM hiddenfiles;
439
+ DELETE FROM metadata;
440
+ DELETE FROM temporary_renames;
441
+ DELETE FROM files;
442
+ `);
443
+ }
444
+ async createIndexes() {
445
+ await this.run(`
446
+ CREATE INDEX IF NOT EXISTS commitstime ON commits(committertime);
447
+ CREATE INDEX IF NOT EXISTS renamestime ON renames(timestamp);
448
+ `);
449
+ }
450
+ static async initViews(db, start, end) {
451
+ await db.run(`
452
+ CREATE OR REPLACE VIEW commits_unioned AS
453
+ SELECT c.hash, CASE WHEN u.actualname IS NOT NULL THEN u.actualname ELSE c.author END AS author, c.committertime, c.authortime FROM
454
+ commits c LEFT JOIN authorunions u ON c.author = u.alias
455
+ WHERE c.committertime BETWEEN ${start} AND ${end};
456
+
457
+ CREATE OR REPLACE VIEW filechanges_commits AS
458
+ SELECT f.commithash, f.insertions, f.deletions, f.filepath, author, c.committertime, c.authortime FROM
459
+ filechanges f JOIN commits_unioned c on f.commithash = c.hash;
460
+
461
+ CREATE OR REPLACE VIEW filechanges_commits_renamed AS
462
+ SELECT f.commithash, f.insertions, f.deletions, f.author, f.committertime, f.authortime,
463
+ CASE
464
+ WHEN r.toname IS NOT NULL THEN r.toname
465
+ ELSE f.filepath
466
+ END AS filepath
467
+ FROM filechanges_commits f
468
+ LEFT JOIN temporary_renames r ON f.filepath = r.fromname
469
+ AND (
470
+ f.committertime BETWEEN r.timestamp AND r.timestampend
471
+ --OR (f.committertime = r.timestampend + 1
472
+ --AND f.authortime < r.timestampend)
473
+ );
474
+
475
+ CREATE OR REPLACE VIEW filtered_files AS
476
+ SELECT f.path
477
+ FROM files f
478
+ LEFT JOIN hiddenfiles g ON
479
+ (g.path LIKE '*.%' AND f.path GLOB g.path)
480
+ OR (g.path NOT LIKE '*.%' AND f.path GLOB g.path || '*')
481
+ WHERE g.path IS NULL;
482
+
483
+
484
+ CREATE OR REPLACE VIEW filechanges_commits_renamed_files AS
485
+ SELECT * FROM filechanges_commits_renamed f
486
+ INNER JOIN filtered_files fi on fi.path = f.filepath;
487
+
488
+ CREATE OR REPLACE VIEW relevant_renames AS
489
+ SELECT fromname, toname, min(timestamp) AS timestamp, timestampauthor FROM renames
490
+ WHERE timestamp BETWEEN ${start} AND ${end}
491
+ group by fromname, toname, timestampauthor;
492
+
493
+ CREATE OR REPLACE VIEW combined_result AS
494
+ SELECT f.commithash, f.insertions, f.deletions, c.committertime, c.authortime,
495
+ CASE WHEN u.actualname IS NOT NULL THEN u.actualname ELSE c.author END AS author,
496
+ CASE
497
+ WHEN r.toname IS NOT NULL THEN r.toname
498
+ ELSE f.filepath
499
+ END AS filepath
500
+ FROM commits c
501
+ LEFT JOIN authorunions u ON c.author = u.alias
502
+ JOIN filechanges f ON f.commithash = c.hash
503
+ LEFT JOIN temporary_renames r ON f.filepath = r.fromname AND c.committertime BETWEEN r.timestamp AND r.timestampend
504
+ INNER JOIN files fi on fi.path = filepath
505
+ WHERE c.committertime BETWEEN ${start} AND ${end}
506
+
507
+ `);
508
+ }
509
+ async updateTimeInterval(timeSeriesStart, timeSeriesEnd) {
510
+ const start = Number.isNaN(timeSeriesStart) ? 0 : timeSeriesStart;
511
+ const end = Number.isNaN(timeSeriesEnd) ? 1e12 : timeSeriesEnd;
512
+ this.selectedRange = [start, end];
513
+ await DB.initViews(await this.connectionPromise, start, end);
514
+ }
515
+ async replaceAuthorUnions(unions) {
516
+ await this.run(`
517
+ DELETE FROM authorunions;
518
+ `);
519
+ await this.usingTableAppender("authorunions", async (appender) => {
520
+ for (const union of unions) {
521
+ const [actualname, ...aliases] = union;
522
+ for (const alias of aliases) {
523
+ appender.appendVarchar(alias);
524
+ appender.appendVarchar(actualname);
525
+ appender.endRow();
526
+ }
527
+ }
528
+ });
529
+ }
530
+ async replaceTemporaryRenames(renames) {
531
+ await this.run(`
532
+ DELETE FROM temporary_renames;
533
+ `);
534
+ await this.usingTableAppender("temporary_renames", async (appender) => {
535
+ for (const rename of renames) {
536
+ rename.fromname ? appender.appendVarchar(rename.fromname) : appender.appendDefault();
537
+ rename.toname ? appender.appendVarchar(rename.toname) : appender.appendDefault();
538
+ appender.appendUInteger(rename.timestamp);
539
+ appender.appendUInteger(rename.timestampend);
540
+ appender.endRow();
541
+ }
542
+ });
543
+ }
544
+ async getAuthorUnions() {
545
+ const res = (await (await this.connectionPromise).runAndReadAll(`
546
+ SELECT actualname, LIST(alias) as aliases FROM authorunions GROUP BY actualname;
547
+ `)).getRowObjects();
548
+ return res.map((row) => [row["actualname"], ...row["aliases"].items]);
549
+ }
550
+ async getRawUnions() {
551
+ const res = await this.query(`
552
+ SELECT * FROM authorunions;
553
+ `);
554
+ return res;
555
+ }
556
+ async getCommitTimeAtIndex(idx) {
557
+ const res = await this.query(`
558
+ SELECT committertime FROM commits ORDER BY committertime DESC OFFSET ${idx} LIMIT 1;
559
+ `);
560
+ return res.length > 0 ? Number(res[0]["committertime"]) : 0;
561
+ }
562
+ async getOverallTimeRange() {
563
+ const res = await this.query(`
564
+ SELECT MIN(committertime) as min, MAX(committertime) as max from commits;
565
+ `);
566
+ return [Number(res[0]["min"]), Number(res[0]["max"])];
567
+ }
568
+ async getCurrentRenameIntervals() {
569
+ const res = await this.query(`
570
+ SELECT * FROM relevant_renames ORDER BY timestamp DESC, timestampauthor DESC;
571
+ `);
572
+ return res.map((row) => {
573
+ return {
574
+ fromname: row["fromname"],
575
+ toname: row["toname"],
576
+ timestamp: 0,
577
+ timestampend: Number(row["timestamp"])
578
+ };
579
+ });
580
+ }
581
+ async getHiddenFiles() {
582
+ const res = await this.query(`
583
+ SELECT path FROM hiddenfiles ORDER BY path ASC;
584
+ `);
585
+ return res.map((row) => row["path"]);
586
+ }
587
+ async replaceHiddenFiles(hiddenFiles) {
588
+ await this.run(`
589
+ DELETE FROM hiddenfiles;
590
+ `);
591
+ await this.usingTableAppender("hiddenfiles", async (appender) => {
592
+ for (const path$1 of hiddenFiles) {
593
+ appender.appendVarchar(path$1);
594
+ appender.endRow();
595
+ }
596
+ });
597
+ }
598
+ async getCommits(path$1, count) {
599
+ const res = await this.query(`
600
+ SELECT distinct commithash, author, committertime, authortime, message, body
601
+ FROM filechanges_commits_renamed_cached
602
+ WHERE filepath GLOB '${path$1}*'
603
+ ORDER BY committertime DESC, commithash
604
+ LIMIT ${count};
605
+ `);
606
+ return res.map((row) => {
607
+ return {
608
+ author: row["author"],
609
+ committertime: row["committertime"],
610
+ authortime: row["authortime"],
611
+ body: row["body"],
612
+ hash: row["commithash"],
613
+ message: row["message"]
614
+ };
615
+ });
616
+ }
617
+ async getCommitHashes(path$1, count) {
618
+ const res = await this.query(`
619
+ SELECT distinct commithash
620
+ FROM filechanges_commits_renamed_cached
621
+ WHERE filepath GLOB '${path$1}*'
622
+ ORDER BY committertime DESC, commithash
623
+ LIMIT ${count};
624
+ `);
625
+ return res.map((row) => {
626
+ return row["commithash"];
627
+ });
628
+ }
629
+ async getCommitCountForPath(path$1) {
630
+ const res = await this.query(`
631
+ SELECT COUNT(DISTINCT commithash) AS count from filechanges_commits_renamed_cached WHERE filepath GLOB '${path$1}*';
632
+ `);
633
+ return Number(res[0]["count"]);
634
+ }
635
+ async getCommitCountPerFile() {
636
+ const res = await this.query(`
637
+ SELECT filepath, count(DISTINCT commithash) AS count
638
+ FROM filechanges_commits_renamed_cached
639
+ GROUP BY filepath
640
+ ORDER BY count DESC;
641
+ `);
642
+ const result = {};
643
+ res.forEach((row) => {
644
+ result[row["filepath"]] = Number(row["count"]);
645
+ });
646
+ return result;
647
+ }
648
+ async getLastChangedPerFile() {
649
+ const res = await this.query(`
650
+ SELECT filepath, MAX(committertime) AS max_time
651
+ FROM filechanges_commits_renamed_cached
652
+ GROUP BY filepath;
653
+ `);
654
+ const result = {};
655
+ res.forEach((row) => {
656
+ result[row["filepath"]] = Number(row["max_time"]);
657
+ });
658
+ return result;
659
+ }
660
+ async getAuthorCountPerFile() {
661
+ const res = await this.query(`
662
+ SELECT filepath, count(DISTINCT author) AS author_count
663
+ FROM filechanges_commits_renamed_cached
664
+ GROUP BY filepath;
665
+ `);
666
+ const result = {};
667
+ res.forEach((row) => {
668
+ result[row["filepath"]] = Number(row["author_count"]);
669
+ });
670
+ return result;
671
+ }
672
+ async getMaxMinContribCounts() {
673
+ const res = await this.query(`
674
+ SELECT MAX(contribsum) as max, MIN(contribsum) as min FROM (SELECT filepath, SUM(insertions + deletions) AS contribsum FROM filechanges_commits_renamed_cached GROUP BY filepath);
675
+ `);
676
+ return {
677
+ max: Number(res[0]["max"]),
678
+ min: Number(res[0]["min"])
679
+ };
680
+ }
681
+ async getContribSumPerFile() {
682
+ const res = await this.query(`
683
+ SELECT filepath, SUM(insertions + deletions) AS contribsum FROM filechanges_commits_renamed_cached GROUP BY filepath;
684
+ `);
685
+ return res.reduce((acc, row) => {
686
+ acc[row["filepath"]] = Number(row["contribsum"]);
687
+ return acc;
688
+ }, {});
689
+ }
690
+ async getDominantAuthorPerFile() {
691
+ const res = await this.query(`
692
+ WITH RankedAuthors AS (
693
+ SELECT filepath, author, SUM(insertions + deletions) AS total_contribcount,
694
+ ROW_NUMBER() OVER (PARTITION BY filepath ORDER BY SUM(insertions + deletions) DESC, author ASC) AS rank
695
+ FROM filechanges_commits_renamed_cached
696
+ GROUP BY filepath, author
697
+ )
698
+ SELECT filepath, author, total_contribcount
699
+ FROM RankedAuthors
700
+ WHERE rank = 1;
701
+ `);
702
+ const result = {};
703
+ res.forEach((row) => {
704
+ const author = row["author"];
705
+ if (typeof author !== "string") throw new Error("Error when getting dominant author per file: Author is not a string");
706
+ result[row["filepath"]] = {
707
+ author,
708
+ contribcount: Number(row["total_contribcount"])
709
+ };
710
+ });
711
+ return result;
712
+ }
713
+ async updateCachedResult() {
714
+ await this.run(`
715
+ CREATE OR REPLACE TEMP TABLE filechanges_commits_renamed_cached AS
716
+ SELECT * FROM filechanges_commits_renamed_files;
717
+ `);
718
+ }
719
+ async addRenames(renames) {
720
+ await this.usingTableAppender("renames", async (appender) => {
721
+ for (const rename of renames) {
722
+ rename.fromname ? appender.appendVarchar(rename.fromname) : appender.appendDefault();
723
+ rename.toname ? appender.appendVarchar(rename.toname) : appender.appendDefault();
724
+ appender.appendUInteger(rename.timestamp);
725
+ appender.appendUInteger(rename.timestampauthor);
726
+ appender.endRow();
727
+ }
728
+ });
729
+ }
730
+ async replaceFiles(files) {
731
+ await this.run(`
732
+ DELETE FROM files;
733
+ `);
734
+ await this.usingTableAppender("files", async (appender) => {
735
+ for (const file of files) {
736
+ appender.appendVarchar(file.path);
737
+ appender.endRow();
738
+ }
739
+ });
740
+ }
741
+ async getFiles() {
742
+ const res = await this.query(`
743
+ FROM files;
744
+ `);
745
+ return res.map((row) => row["path"]);
746
+ }
747
+ async commitTableEmpty() {
748
+ const res = await this.query(`
749
+ SELECT * FROM commits LIMIT 1;
750
+ `);
751
+ return res.length === 0;
752
+ }
753
+ async getLatestCommitHash(beforeTime) {
754
+ const res = await this.query(`
755
+ SELECT hash FROM commits WHERE committertime <= ${beforeTime ?? 1e12} ORDER BY committertime DESC LIMIT 1;
756
+ `);
757
+ if (res.length < 1) throw new Error("Could not get latest commit hash. Commits table is empty. beforeTime set to " + beforeTime);
758
+ return res[0]["hash"];
759
+ }
760
+ async getAuthors() {
761
+ const res = await this.query(`
762
+ SELECT DISTINCT author FROM commits_unioned;
763
+ `);
764
+ return res.map((row) => row["author"]);
765
+ }
766
+ async getNewestAndOldestChangeDates() {
767
+ const res = await this.query(`
768
+ SELECT MAX(max_time) AS newest, MIN(max_time) AS oldest FROM (SELECT filepath, MAX(committertime) AS max_time FROM filechanges_commits_renamed_cached GROUP BY filepath);
769
+ `);
770
+ return {
771
+ newestChangeDate: res[0]["newest"],
772
+ oldestChangeDate: res[0]["oldest"]
773
+ };
774
+ }
775
+ async getCommitCount() {
776
+ const res = await this.query(`
777
+ SELECT count(distinct commithash) AS count FROM filechanges_commits_renamed_cached;
778
+ `);
779
+ return Number(res[0]["count"]);
780
+ }
781
+ async getMaxAndMinCommitCount() {
782
+ const res = await this.query(`
783
+ SELECT MAX(count) as max_commits, MIN(count) as min_commits FROM (SELECT filepath, count(distinct commithash) AS count FROM filechanges_commits_renamed_cached GROUP BY filepath ORDER BY count DESC);
784
+ `);
785
+ return {
786
+ maxCommitCount: Number(res[0]["max_commits"]),
787
+ minCommitCount: Number(res[0]["min_commits"])
788
+ };
789
+ }
790
+ async getAuthorContribsForPath(path$1, isblob) {
791
+ const res = await this.query(`
792
+ SELECT author, SUM(insertions + deletions) AS contribsum FROM filechanges_commits_renamed_cached WHERE filepath ${isblob ? "=" : "GLOB"} '${path$1}${isblob ? "" : "*"}' GROUP BY author ORDER BY contribsum DESC, author ASC;
793
+ `);
794
+ return res.map((row) => {
795
+ return {
796
+ author: row["author"],
797
+ contribs: Number(row["contribsum"])
798
+ };
799
+ });
800
+ }
801
+ getTimeStringFormat(timerange) {
802
+ const durationDays = (timerange[1] - timerange[0]) / (60 * 60 * 24);
803
+ if (durationDays < 150) return ["%a %-d %B %Y", "day"];
804
+ if (durationDays < 1e3) return ["Week %V %Y", "week"];
805
+ if (durationDays < 4e3) return ["%B %Y", "month"];
806
+ return ["%Y", "year"];
807
+ }
808
+ async getCommitCountPerTime(timerange) {
809
+ const [query, timeUnit] = this.getTimeStringFormat(timerange);
810
+ const res = await this.query(`
811
+ SELECT strftime(date, '${query}') as timestring, count(*) AS count, MIN(committertime) AS ct FROM (SELECT date_trunc('${timeUnit}',to_timestamp(committertime)) AS date, committertime FROM commits) GROUP BY date ORDER BY date ASC;
812
+ `);
813
+ const mapped = res.map((x) => {
814
+ return {
815
+ date: x["timestring"],
816
+ count: Number(x["count"]),
817
+ timestamp: Number(x["ct"])
818
+ };
819
+ });
820
+ const final = [];
821
+ const allIntervals = getTimeIntervals(timeUnit, timerange[0], timerange[1]);
822
+ for (const [dateString, timestamp] of allIntervals) {
823
+ const existing = mapped.find((x) => x.date === dateString);
824
+ if (existing) final.push(existing);
825
+ else final.push({
826
+ date: dateString,
827
+ count: 0,
828
+ timestamp
829
+ });
830
+ }
831
+ const sorted = final.sort((a, b) => a.timestamp - b.timestamp);
832
+ return sorted;
833
+ }
834
+ async updateColorSeed(seed) {
835
+ await this.run(`
836
+ DELETE FROM metadata WHERE field = 'colorseed';
837
+ INSERT INTO metadata (field, value, value2) VALUES ('colorseed', null, '${seed}');
838
+ `);
839
+ console.log("inserted seed", seed);
840
+ }
841
+ async getColorSeed() {
842
+ const res = await this.query(`
843
+ SELECT value2 FROM metadata WHERE field = 'colorseed';
844
+ `);
845
+ if (res.length < 1) return null;
846
+ console.log("retrieved seed", res[0]["value2"]);
847
+ return res[0]["value2"];
848
+ }
849
+ async getLastRunInfo() {
850
+ const res = await this.query(`
851
+ SELECT value as time, value2 as hash FROM metadata WHERE field = 'finished' ORDER BY value DESC LIMIT 1;
852
+ `);
853
+ if (!res[0]) return {
854
+ time: 0,
855
+ hash: ""
856
+ };
857
+ return {
858
+ time: Number(res[0]["time"]),
859
+ hash: res[0]["hash"]
860
+ };
861
+ }
862
+ async addCommits(commits) {
863
+ await this.usingTableAppender("commits", async (appender) => {
864
+ for (const [hash, commit] of commits) {
865
+ if (!commit) throw new Error(`Commit with hash ${hash} is undefined`);
866
+ appender.appendVarchar(hash);
867
+ appender.appendVarchar(commit.author);
868
+ appender.appendUInteger(commit.committertime);
869
+ appender.appendUInteger(commit.authortime);
870
+ appender.endRow();
871
+ }
872
+ });
873
+ await this.usingTableAppender("filechanges", async (appender) => {
874
+ for (const [hash, commit] of commits) {
875
+ if (!commit) throw new Error(`Commit with hash ${hash} is undefined`);
876
+ for (const change of commit.fileChanges) {
877
+ appender.appendVarchar(commit.hash);
878
+ appender.appendUInteger(change.insertions);
879
+ appender.appendUInteger(change.deletions);
880
+ appender.appendVarchar(change.path);
881
+ appender.endRow();
882
+ }
883
+ }
884
+ });
885
+ }
886
+ };
887
+ const AnalyzerDataInterfaceVersion = 16;
888
+ const ANALYZER_CACHE_MISS_REASONS = {
889
+ OTHER_REPO: "The cache was not created for this repo",
890
+ NOT_CACHED: "No cache was found",
891
+ BRANCH_HEAD_CHANGED: "Branch head changed",
892
+ DATA_VERSION_MISMATCH: "Outdated cache"
893
+ };
894
+ var GitCaller = class GitCaller {
895
+ useCache = true;
896
+ catFileCache = /* @__PURE__ */ new Map();
897
+ constructor(repo, branch, repoPath) {
898
+ this.repo = repo;
899
+ this.branch = branch;
900
+ this.repoPath = repoPath;
901
+ }
902
+ static async isGitRepo(path$1) {
903
+ const gitFolderPath = resolve(path$1, ".git");
904
+ const hasGitFolder = existsSync(gitFolderPath);
905
+ if (!hasGitFolder) return false;
906
+ const [, findBranchHeadError] = await promiseHelper(GitCaller.findBranchHead(path$1));
907
+ return Boolean(hasGitFolder && !findBranchHeadError);
908
+ }
909
+ static async isValidRevision(revision, path$1) {
910
+ const gitFolder = join(path$1, ".git");
911
+ const [, findBranchHeadError] = await promiseHelper(GitCaller._revParse(gitFolder, revision));
912
+ return !findBranchHeadError;
913
+ }
914
+ /**
915
+
916
+ *
917
+
918
+ * @param repo The repo to find the branch head for
919
+
920
+ * @param branch The branch to find the head for (default: checkout branch)
921
+
922
+ * @returns Promise<[branchHead: string, branchName: string]>
923
+
924
+ */
925
+ static async findBranchHead(repo, branch) {
926
+ if (!branch) {
927
+ const [foundBranch, getBranchError] = await promiseHelper(GitCaller._getRepositoryHead(repo));
928
+ if (getBranchError) throw getBranchError;
929
+ branch = foundBranch;
930
+ }
931
+ const gitFolder = join(repo, ".git");
932
+ if (!existsSync(gitFolder)) throw Error("No git folder exists at " + gitFolder);
933
+ const branchHead = await GitCaller._revParse(gitFolder, branch);
934
+ log.debug(`${branch} -> [commit] ${branchHead}`);
935
+ return [branchHead, branch];
936
+ }
937
+ static getCachePath(repo, branch) {
938
+ return resolve(os$1.tmpdir(), "git-truck", repo, `${branch}.json`);
939
+ }
940
+ async getRepositoryHead() {
941
+ return await GitCaller._getRepositoryHead(this.repo);
942
+ }
943
+ async gitLogSpecificCommits(commits) {
944
+ const args = [
945
+ "log",
946
+ "--no-walk",
947
+ "--numstat",
948
+ "--format=\"author <|%aN|> date <|%ct %at|> message <|%s|> body <|%b|> hash <|%H|>\"",
949
+ ...commits
950
+ ];
951
+ const result = await runProcess(this.repoPath, "git", args);
952
+ return result.trim();
953
+ }
954
+ static async _getRepositoryHead(dir) {
955
+ const result = await runProcess(dir, "git", [
956
+ "rev-parse",
957
+ "--abbrev-ref",
958
+ "HEAD"
959
+ ]);
960
+ return result.trim();
961
+ }
962
+ async lsTree(hash) {
963
+ return await GitCaller._lsTree(this.repoPath, hash);
964
+ }
965
+ static async _lsTree(repo, hash) {
966
+ const result = await runProcess(repo, "git", [
967
+ "ls-tree",
968
+ "-rlt",
969
+ hash
970
+ ]);
971
+ return result.trim();
972
+ }
973
+ async revParse(ref) {
974
+ return await GitCaller._revParse(this.repoPath, ref);
975
+ }
976
+ static async _revParse(dir, ref) {
977
+ const result = await runProcess(dir, "git", [
978
+ "rev-list",
979
+ "-n",
980
+ "1",
981
+ ref
982
+ ]);
983
+ return result.trim();
984
+ }
985
+ static async getRepoMetadata(repoPath) {
986
+ const repoDir = getDirName(repoPath);
987
+ const parentDir = getBaseDirFromPath(repoDir);
988
+ const isRepo = await GitCaller.isGitRepo(repoPath);
989
+ if (!isRepo) return {
990
+ status: "Error",
991
+ errorMessage: "Not a valid git repository",
992
+ name: repoDir,
993
+ path: repoPath,
994
+ parentDirPath: parentDir
995
+ };
996
+ const refs = GitCaller.parseRefs(await GitCaller._getRefs(repoPath));
997
+ const allHeads = new Set([...Object.entries(refs.Branches), ...Object.entries(refs.Tags)]).values();
998
+ const headsWithCaches = await Promise.all(Array.from(allHeads).map(async ([headName, head]) => {
999
+ const [result] = await GitCaller.retrieveCachedResult({
1000
+ repo: getDirName(repoPath),
1001
+ branch: headName,
1002
+ branchHead: head
1003
+ });
1004
+ return {
1005
+ headName,
1006
+ head,
1007
+ isAnalyzed: result !== null
1008
+ };
1009
+ }));
1010
+ const analyzedHeads = headsWithCaches.filter((head) => head.isAnalyzed).reduce((acc, headEntry) => ({
1011
+ ...acc,
1012
+ [headEntry.head]: true,
1013
+ [headEntry.headName]: true
1014
+ }), {});
1015
+ const repoHead = await GitCaller._getRepositoryHead(repoPath);
1016
+ try {
1017
+ const [findBranchHeadResult, error$1] = await promiseHelper(GitCaller.findBranchHead(repoPath));
1018
+ if (error$1) return {
1019
+ status: "Error",
1020
+ errorMessage: error$1.message,
1021
+ name: repoDir,
1022
+ path: repoPath,
1023
+ parentDirPath: parentDir
1024
+ };
1025
+ const [branchHead, branch] = findBranchHeadResult;
1026
+ const [data, reasons] = await GitCaller.retrieveCachedResult({
1027
+ repo: repoDir,
1028
+ branch,
1029
+ branchHead
1030
+ });
1031
+ if (!data) return {
1032
+ status: "Success",
1033
+ isAnalyzed: false,
1034
+ data: null,
1035
+ reasons,
1036
+ name: repoDir,
1037
+ path: repoPath,
1038
+ parentDirPath: parentDir,
1039
+ currentHead: branch,
1040
+ refs,
1041
+ analyzedHeads
1042
+ };
1043
+ return {
1044
+ status: "Success",
1045
+ isAnalyzed: true,
1046
+ data,
1047
+ reasons: [],
1048
+ name: repoDir,
1049
+ path: repoPath,
1050
+ parentDirPath: parentDir,
1051
+ currentHead: repoHead,
1052
+ refs,
1053
+ analyzedHeads
1054
+ };
1055
+ } catch (e) {
1056
+ return {
1057
+ status: "Error",
1058
+ errorMessage: e instanceof Error ? e.message : "Unknown error",
1059
+ name: repoDir,
1060
+ path: repoPath,
1061
+ parentDirPath: parentDir
1062
+ };
1063
+ }
1064
+ }
1065
+ static async scanDirectoryForRepositories(argPath) {
1066
+ let userRepo = null;
1067
+ const [pathIsRepo] = await describeAsyncJob({
1068
+ job: () => GitCaller.isGitRepo(argPath),
1069
+ beforeMsg: "Checking if path is a git repo...",
1070
+ afterMsg: "Done checking if path is a git repo",
1071
+ errorMsg: "Error checking if path is a git repo"
1072
+ });
1073
+ const baseDir = resolve(pathIsRepo ? getBaseDirFromPath(argPath) : argPath);
1074
+ const entries = await promises.readdir(baseDir, { withFileTypes: true });
1075
+ const dirs = entries.filter((entry) => entry.isDirectory()).map(({ name }) => name);
1076
+ const [repoResults] = await describeAsyncJob({
1077
+ job: () => Promise.allSettled(dirs.map(async (repo) => {
1078
+ const result = await GitCaller.getRepoMetadata(join(baseDir, repo));
1079
+ if (!result) throw Error("Not a git repo");
1080
+ return result;
1081
+ })),
1082
+ beforeMsg: "Scanning for repositories...",
1083
+ afterMsg: "Done scanning for repositories",
1084
+ errorMsg: "Error scanning for repositories"
1085
+ });
1086
+ const onlyRepos = repoResults.filter((currentRepo) => {
1087
+ if (currentRepo.status === "rejected") return false;
1088
+ if (pathIsRepo && currentRepo.value.name === getDirName(argPath)) userRepo = currentRepo.value;
1089
+ return true;
1090
+ }).map((result) => result.value);
1091
+ return [userRepo, onlyRepos];
1092
+ }
1093
+ static parseRefs(refsAsMultilineString) {
1094
+ const gitRefs = {
1095
+ Branches: {},
1096
+ Tags: {}
1097
+ };
1098
+ const regex = /^(?<hash>.*) refs\/(?<ref_type>.*?)\/(?<path>.*)$/gm;
1099
+ const matches = refsAsMultilineString.matchAll(regex);
1100
+ let next = matches.next();
1101
+ while (next.value) {
1102
+ const groups = next.value.groups;
1103
+ if (!groups) break;
1104
+ next = matches.next();
1105
+ const hash = groups["hash"];
1106
+ const ref_type = groups["ref_type"];
1107
+ const path$1 = groups["path"];
1108
+ switch (ref_type) {
1109
+ case "heads":
1110
+ gitRefs.Branches[path$1] = hash;
1111
+ break;
1112
+ case "remotes": break;
1113
+ case "tags":
1114
+ gitRefs.Tags[path$1] = hash;
1115
+ break;
1116
+ default: break;
1117
+ }
1118
+ }
1119
+ gitRefs.Branches = Object.fromEntries(Object.entries(gitRefs.Branches).sort(([a], [b]) => branchCompare(a, b)));
1120
+ gitRefs.Tags = Object.fromEntries(Object.entries(gitRefs.Tags).sort(([a], [b]) => semverCompare(a, b) * -1));
1121
+ return gitRefs;
1122
+ }
1123
+ async gitLog(skip, count) {
1124
+ const args = [
1125
+ "log",
1126
+ `--skip=${skip}`,
1127
+ `--max-count=${count}`,
1128
+ this.branch,
1129
+ "--summary",
1130
+ "--numstat",
1131
+ "--format=\"author <|%aN|> date <|%ct %at|> message <|%s|> body <|%b|> hash <|%H|>\""
1132
+ ];
1133
+ const result = await runProcess(this.repoPath, "git", args);
1134
+ return result.trim();
1135
+ }
1136
+ async gitLogSimple(skip, count, instance, index) {
1137
+ const args = [
1138
+ "log",
1139
+ `--skip=${skip}`,
1140
+ `--max-count=${count}`,
1141
+ this.branch,
1142
+ "--summary",
1143
+ "--numstat",
1144
+ "--format=\"<|%aN|><|%ct %at|><|%H|>\""
1145
+ ];
1146
+ const result = await runProcess(this.repoPath, "git", args, instance, index);
1147
+ return result.trim();
1148
+ }
1149
+ static async retrieveCachedResult({ repo, branch, branchHead }) {
1150
+ const reasons = [];
1151
+ const cachedDataPath = GitCaller.getCachePath(repo, branch);
1152
+ if (!existsSync(cachedDataPath)) return [null, [ANALYZER_CACHE_MISS_REASONS.NOT_CACHED]];
1153
+ const cachedData = JSON.parse(await promises.readFile(cachedDataPath, "utf8"));
1154
+ const branchHeadMatches = branchHead === cachedData.commit.hash;
1155
+ if (!branchHeadMatches) reasons.push(ANALYZER_CACHE_MISS_REASONS.BRANCH_HEAD_CHANGED);
1156
+ const dataVersionMatches = cachedData.interfaceVersion === AnalyzerDataInterfaceVersion;
1157
+ if (!branchHeadMatches) reasons.push(ANALYZER_CACHE_MISS_REASONS.DATA_VERSION_MISMATCH);
1158
+ const repoMatches = repo === cachedData.repo;
1159
+ const cacheConditions = {
1160
+ branchHeadMatches,
1161
+ dataVersionMatches,
1162
+ repoMatches
1163
+ };
1164
+ if (!Object.values(cacheConditions).every(Boolean)) return [null, reasons];
1165
+ return [cachedData, reasons];
1166
+ }
1167
+ setUseCache(useCache) {
1168
+ this.useCache = useCache;
1169
+ }
1170
+ async commitCountSinceCommit(hash, branch) {
1171
+ const result = await runProcess(this.repoPath, "git", [
1172
+ "rev-list",
1173
+ "--count",
1174
+ `${hash}..${branch}`
1175
+ ]);
1176
+ return result;
1177
+ }
1178
+ async getRefs() {
1179
+ return await GitCaller._getRefs(this.repo);
1180
+ }
1181
+ static async _getRefs(repo) {
1182
+ const result = await runProcess(repo, "git", ["show-ref"]);
1183
+ return result;
1184
+ }
1185
+ async catFile(hash) {
1186
+ const result = await runProcess(this.repoPath, "git", [
1187
+ "cat-file",
1188
+ "-p",
1189
+ hash
1190
+ ]);
1191
+ return result;
1192
+ }
1193
+ async findBranchHead() {
1194
+ return await GitCaller.findBranchHead(this.repoPath, this.branch);
1195
+ }
1196
+ async catFileCached(hash) {
1197
+ if (this.useCache) {
1198
+ const cachedValue = this.catFileCache.get(hash);
1199
+ if (cachedValue) return cachedValue;
1200
+ }
1201
+ const result = await this.catFile(hash);
1202
+ this.catFileCache.set(hash, result);
1203
+ return result;
1204
+ }
1205
+ async getCommitCount() {
1206
+ const result = await runProcess(this.repoPath, "git", [
1207
+ "rev-list",
1208
+ "--count",
1209
+ this.branch
1210
+ ]);
1211
+ return result;
1212
+ }
1213
+ async getDefaultGitSettingValue(setting) {
1214
+ const result = await runProcess(this.repoPath, "git", ["config", setting]);
1215
+ return result;
1216
+ }
1217
+ async resetGitSetting(settingToReset, value) {
1218
+ if (!value) {
1219
+ await runProcess(this.repoPath, "git", [
1220
+ "config",
1221
+ "--unset",
1222
+ settingToReset
1223
+ ]);
1224
+ log.debug(`Unset ${settingToReset}`);
1225
+ } else {
1226
+ await runProcess(this.repoPath, "git", [
1227
+ "config",
1228
+ settingToReset,
1229
+ value
1230
+ ]);
1231
+ log.debug(`Reset ${settingToReset} to ${value}`);
1232
+ }
1233
+ }
1234
+ async setGitSetting(setting, value) {
1235
+ await runProcess(this.repoPath, "git", [
1236
+ "config",
1237
+ setting,
1238
+ value
1239
+ ]);
1240
+ log.debug(`Set ${setting} to ${value}`);
1241
+ }
1242
+ };
1243
+ const gitLogRegex = /"author\s+<\|(?<author>.*?)\|>\s+date\s+<\|(?<datecommitter>\d+)\s(?<dateauthor>\d+)\|>\s+message\s+<\|(?<message>[\s\S]*?)\|>\s+body\s+<\|(?<body>[\s\S]*?)\|>\s+hash\s+<\|(?<hash>.+?)\|>"\s*(?<contributions>(?:(?:\d+|-)\s+(?:\d+|-)\s+.+\s?)*)(?<modes>(?:\s.+\s*)*)/gmu;
1244
+ const gitLogRegexSimple = /"<\|(?<author>.*?)\|><\|(?<datecommitter>\d+)\s(?<dateauthor>\d+)\|><\|(?<hash>.+?)\|>"\s*(?<contributions>(?:(?:\d+|-)\s+(?:\d+|-)\s+.+\s?)*)(?<modes>(?:\s.+\s*)*)/gmu;
1245
+ const contribRegex = /(?<insertions>\d+|-)\s+(?<deletions>\d+|-)\s+(?<file>.+)/gm;
1246
+ const treeRegex = /^\S+? (?<type>\S+) (?<hash>\S+)\s+(?<size>\S+)\s+(?<path>.+)/gm;
1247
+ const modeRegex = /\s(?<mode>\w+)\s\w+\s\d+\s(?<file>.+)/gmu;
1248
+ const OPTIONS_LOCAL_STORAGE_KEY = "options";
1249
+ var MetadataDB = class {
1250
+ path = resolve$1(os.tmpdir(), "git-truck-cache", "metadata.json");
1251
+ separator = "---";
1252
+ async readMetadata() {
1253
+ try {
1254
+ const data = JSON.parse(await promises$1.readFile(this.path, "utf8"));
1255
+ return data;
1256
+ } catch (e) {
1257
+ return {
1258
+ completions: {},
1259
+ authorcolors: {}
1260
+ };
1261
+ }
1262
+ }
1263
+ async setMetadata(newData) {
1264
+ const asString = JSON.stringify(newData);
1265
+ await promises$1.writeFile(this.path, asString, "utf8");
1266
+ }
1267
+ async setCompletion(repo, branch, hash) {
1268
+ const currentMetadata = await this.readMetadata();
1269
+ currentMetadata.completions[`${repo}${this.separator}${branch}`] = {
1270
+ hash,
1271
+ time: Math.floor(Date.now() / 1e3)
1272
+ };
1273
+ await this.setMetadata(currentMetadata);
1274
+ }
1275
+ async addAuthorColor(author, color) {
1276
+ const currentMetadata = await this.readMetadata();
1277
+ if (color === "") delete currentMetadata.authorcolors[author];
1278
+ else currentMetadata.authorcolors[author] = color;
1279
+ await this.setMetadata(currentMetadata);
1280
+ }
1281
+ async getAuthorColors() {
1282
+ const currentMetadata = await this.readMetadata();
1283
+ return currentMetadata.authorcolors;
1284
+ }
1285
+ async getLastRun(repo, branch) {
1286
+ const currentMetadata = await this.readMetadata();
1287
+ return currentMetadata.completions[`${repo}${this.separator}${branch}`];
1288
+ }
1289
+ async getCompletedRepos() {
1290
+ const completedResults = [];
1291
+ const currentMetadata = await this.readMetadata();
1292
+ for (const [key, val] of Object.entries(currentMetadata.completions)) {
1293
+ const [repo, branch] = key.split(this.separator);
1294
+ completedResults.push({
1295
+ repo,
1296
+ branch,
1297
+ time: val.time
1298
+ });
1299
+ }
1300
+ return completedResults;
1301
+ }
1302
+ };
1303
+ var InstanceManager = class {
1304
+ static instances = /* @__PURE__ */ new Map();
1305
+ static metadataDB;
1306
+ static getOrCreateMetadataDB() {
1307
+ if (!this.metadataDB) this.metadataDB = new MetadataDB();
1308
+ return this.metadataDB;
1309
+ }
1310
+ static getOrCreateInstance(repo, branch, repoPath) {
1311
+ if (!this.instances) this.instances = /* @__PURE__ */ new Map();
1312
+ const existing = this.instances.get(repo)?.get(branch);
1313
+ if (existing) return existing;
1314
+ const newInstance = new ServerInstance(repo, branch, repoPath);
1315
+ const existingRepo = this.instances.get(repo);
1316
+ if (existingRepo) existingRepo.set(branch, newInstance);
1317
+ else this.instances.set(repo, new Map([[branch, newInstance]]));
1318
+ return newInstance;
1319
+ }
1320
+ static getInstance(repo, branch) {
1321
+ if (!this.instances) this.instances = /* @__PURE__ */ new Map();
1322
+ return this.instances.get(repo)?.get(branch);
1323
+ }
1324
+ static async closeAllDBConnections() {
1325
+ const promises$2 = Array.from(this.instances.values()).flatMap((repoInstance) => Array.from(repoInstance.values()).flatMap((branchInstance) => branchInstance.db.disconnect()));
1326
+ await Promise.all(promises$2);
1327
+ this.instances = /* @__PURE__ */ new Map();
1328
+ }
1329
+ };
1330
+ var ServerInstance = class {
1331
+ analyzationStatus = "Starting";
1332
+ gitCaller;
1333
+ db;
1334
+ progress = [0];
1335
+ totalCommitCount = 0;
1336
+ fileTreeAsOf = null;
1337
+ prevResult = null;
1338
+ prevInvokeReason = "unknown";
1339
+ prevProgress = {
1340
+ str: "",
1341
+ timestamp: 0
1342
+ };
1343
+ constructor(repo, branch, repoPath) {
1344
+ this.repo = repo;
1345
+ this.branch = branch;
1346
+ this.repoPath = repoPath;
1347
+ this.gitCaller = new GitCaller(repo, branch, repoPath);
1348
+ this.db = new DB(repo, branch);
1349
+ }
1350
+ updateProgress(index) {
1351
+ this.progress[index]++;
1352
+ }
1353
+ async gathererWorker(sectionStart, sectionEnd, index) {
1354
+ const CHUNK_SIZE = 7e4;
1355
+ for (let start = sectionStart; start <= sectionEnd; start += CHUNK_SIZE) {
1356
+ const end = Math.min(start + CHUNK_SIZE, sectionEnd);
1357
+ const commits = /* @__PURE__ */ new Map();
1358
+ const renamedFiles = [];
1359
+ log.debug(`thread ${index} gathering ${start}-${end}`);
1360
+ await this.gatherCommitsInRange(start, end, commits, renamedFiles, index);
1361
+ await this.db.addRenames(renamedFiles);
1362
+ await this.db.addCommits(commits);
1363
+ this.progress[index] = end - sectionStart;
1364
+ }
1365
+ }
1366
+ async updateTimeInterval(start, end) {
1367
+ await this.db.updateTimeInterval(start, end);
1368
+ this.fileTreeAsOf = await this.db.getLatestCommitHash(end);
1369
+ }
1370
+ async analyzeTree() {
1371
+ if (!this.fileTreeAsOf) this.fileTreeAsOf = await this.db.getLatestCommitHash();
1372
+ const rawContent = await this.gitCaller.lsTree(this.fileTreeAsOf);
1373
+ const lsTreeEntries = [];
1374
+ const matches = rawContent.matchAll(treeRegex);
1375
+ let fileCount = 0;
1376
+ for (const match of matches) {
1377
+ if (!match.groups) continue;
1378
+ const groups = match.groups;
1379
+ lsTreeEntries.push({
1380
+ type: groups["type"],
1381
+ hash: groups["hash"],
1382
+ size: groups["size"] === "-" ? void 0 : Number(groups["size"]),
1383
+ path: this.repo + "/" + groups["path"]
1384
+ });
1385
+ }
1386
+ const rootTree = {
1387
+ type: "tree",
1388
+ path: this.repo,
1389
+ name: this.repo,
1390
+ hash: this.fileTreeAsOf,
1391
+ children: []
1392
+ };
1393
+ await this.db.replaceFiles(lsTreeEntries);
1394
+ for (const child of lsTreeEntries) {
1395
+ const prevTrees = child.path.split("/").slice(1);
1396
+ const newName = prevTrees.pop();
1397
+ const newPath = `${child.path}`;
1398
+ let currTree = rootTree;
1399
+ for (const treePath of prevTrees) {
1400
+ const foundTree = currTree.children.find((t) => t.name === treePath && t.type === "tree");
1401
+ if (!foundTree) continue;
1402
+ currTree = foundTree;
1403
+ }
1404
+ switch (child.type) {
1405
+ case "tree": {
1406
+ const newTree = {
1407
+ type: "tree",
1408
+ path: newPath,
1409
+ name: newName,
1410
+ hash: child.hash,
1411
+ children: []
1412
+ };
1413
+ currTree.children.push(newTree);
1414
+ break;
1415
+ }
1416
+ case "blob": {
1417
+ fileCount += 1;
1418
+ const blob = {
1419
+ type: "blob",
1420
+ hash: child.hash,
1421
+ path: newPath,
1422
+ name: newName,
1423
+ sizeInBytes: child.size
1424
+ };
1425
+ currTree.children.push(blob);
1426
+ break;
1427
+ }
1428
+ }
1429
+ }
1430
+ this.treeCleanup(rootTree);
1431
+ return {
1432
+ rootTree,
1433
+ fileCount
1434
+ };
1435
+ }
1436
+ treeCleanup(tree) {
1437
+ for (const child of tree.children) if (child.type === "tree") {
1438
+ const ctree = child;
1439
+ this.treeCleanup(ctree);
1440
+ }
1441
+ tree.children = tree.children.filter((child) => {
1442
+ if (child.type === "blob") return true;
1443
+ else {
1444
+ const ctree = child;
1445
+ if (ctree.children.length === 0) return false;
1446
+ return true;
1447
+ }
1448
+ });
1449
+ if (tree.children.length === 1 && tree.children[0].type === "tree") {
1450
+ const temp = tree.children[0];
1451
+ tree.children = temp.children;
1452
+ tree.name = `${tree.name}/${temp.name}`;
1453
+ tree.path = `${tree.path}/${temp.name}`;
1454
+ }
1455
+ }
1456
+ async gatherCommitsFromGitLog(gitLogResult, commits, renamedFiles) {
1457
+ const matches = gitLogResult.matchAll(gitLogRegexSimple);
1458
+ const FileModifications = [];
1459
+ for (const match of matches) {
1460
+ const groups = match.groups ?? {};
1461
+ const author = groups.author;
1462
+ const committertime = Number(groups.datecommitter);
1463
+ const authortime = Number(groups.dateauthor);
1464
+ const hash = groups.hash;
1465
+ const contributionsString = groups.contributions;
1466
+ const modesString = groups.modes;
1467
+ const fileChanges = [];
1468
+ if (modesString) {
1469
+ const modeMatches = modesString.matchAll(modeRegex);
1470
+ for (const modeMatch of modeMatches) {
1471
+ const file = modeMatch.groups?.file.trim();
1472
+ const mode = modeMatch.groups?.mode.trim();
1473
+ if (!file || !mode) continue;
1474
+ if (mode === "delete" || mode === "create") FileModifications.push({
1475
+ path: file,
1476
+ timestamp: committertime,
1477
+ timestampauthor: authortime,
1478
+ type: mode
1479
+ });
1480
+ }
1481
+ }
1482
+ if (contributionsString) {
1483
+ const contribMatches = contributionsString.matchAll(contribRegex);
1484
+ for (const contribMatch of contribMatches) {
1485
+ const file = contribMatch.groups?.file.trim();
1486
+ const isBinary = contribMatch.groups?.insertions === "-";
1487
+ if (!file) throw Error("file not found");
1488
+ const fileHasMoved = file.includes("=>");
1489
+ let filePath = file;
1490
+ if (fileHasMoved) filePath = analyzeRenamedFile(file, committertime, authortime, renamedFiles, this.repo);
1491
+ const insertions = isBinary ? 1 : Number(contribMatch.groups?.insertions ?? "0");
1492
+ const deletions = isBinary ? 0 : Number(contribMatch.groups?.deletions ?? "0");
1493
+ fileChanges.push({
1494
+ isBinary,
1495
+ insertions,
1496
+ deletions,
1497
+ path: this.repo + "/" + filePath,
1498
+ mode: "modify"
1499
+ });
1500
+ }
1501
+ }
1502
+ commits.set(hash, {
1503
+ author,
1504
+ committertime,
1505
+ authortime,
1506
+ hash,
1507
+ coauthors: [],
1508
+ fileChanges
1509
+ });
1510
+ }
1511
+ renamedFiles.push(...FileModifications.map((modification) => {
1512
+ if (modification.type === "delete") return {
1513
+ fromname: modification.path,
1514
+ toname: null,
1515
+ timestamp: modification.timestamp,
1516
+ timestampauthor: modification.timestampauthor
1517
+ };
1518
+ return {
1519
+ fromname: null,
1520
+ toname: modification.path,
1521
+ timestamp: modification.timestamp,
1522
+ timestampauthor: modification.timestampauthor
1523
+ };
1524
+ }));
1525
+ }
1526
+ async getFullCommits(gitLogResult) {
1527
+ const commits = [];
1528
+ const matches = gitLogResult.matchAll(gitLogRegex);
1529
+ for (const match of matches) {
1530
+ const groups = match.groups ?? {};
1531
+ const author = groups.author;
1532
+ const message = groups.message;
1533
+ const body = groups.body;
1534
+ const committertime = Number(groups.datecommitter);
1535
+ const authortime = Number(groups.dateauthor);
1536
+ const hash = groups.hash;
1537
+ const contributionsString = groups.contributions;
1538
+ const fileChanges = [];
1539
+ if (contributionsString) {
1540
+ const contribMatches = contributionsString.matchAll(contribRegex);
1541
+ for (const contribMatch of contribMatches) {
1542
+ const file = contribMatch.groups?.file.trim();
1543
+ const isBinary = contribMatch.groups?.insertions === "-";
1544
+ if (!file) throw Error("file not found");
1545
+ const insertions = isBinary ? 1 : Number(contribMatch.groups?.insertions ?? "0");
1546
+ const deletions = isBinary ? 0 : Number(contribMatch.groups?.deletions ?? "0");
1547
+ fileChanges.push({
1548
+ isBinary,
1549
+ insertions,
1550
+ deletions,
1551
+ path: file,
1552
+ mode: "modify"
1553
+ });
1554
+ }
1555
+ }
1556
+ commits.push({
1557
+ author,
1558
+ committertime,
1559
+ authortime,
1560
+ hash,
1561
+ fileChanges,
1562
+ message,
1563
+ body
1564
+ });
1565
+ }
1566
+ return commits;
1567
+ }
1568
+ async gatherCommitsInRange(start, end, commits, renamedFiles, index) {
1569
+ const gitLogResult = await this.gitCaller.gitLogSimple(start, end - start, this, index);
1570
+ await this.gatherCommitsFromGitLog(gitLogResult, commits, renamedFiles);
1571
+ log.debug("done gathering");
1572
+ }
1573
+ flattenChains(chains) {
1574
+ return chains.flatMap((chain) => {
1575
+ const destinationName = chain[0].toname;
1576
+ return chain.map((interval) => ({
1577
+ ...interval,
1578
+ toname: destinationName
1579
+ }));
1580
+ });
1581
+ }
1582
+ async updateRenames() {
1583
+ const rawRenames = await this.db.getCurrentRenameIntervals();
1584
+ const files = await this.db.getFiles();
1585
+ const renameChains = this.generateRenameChains(rawRenames, files);
1586
+ const flattenedRenames = this.flattenChains(renameChains);
1587
+ await this.db.replaceTemporaryRenames(flattenedRenames);
1588
+ }
1589
+ generateRenameChains(orderedRenames, currentFiles) {
1590
+ const currentPathToRenameChain = /* @__PURE__ */ new Map();
1591
+ const finishedChains = [];
1592
+ for (const file of currentFiles) currentPathToRenameChain.set(file, [{
1593
+ fromname: file,
1594
+ toname: file,
1595
+ timestamp: 0,
1596
+ timestampend: 4e9
1597
+ }]);
1598
+ for (const rename of orderedRenames) {
1599
+ if (rename.toname === null) continue;
1600
+ const existing = currentPathToRenameChain.get(rename.toname);
1601
+ if (existing) {
1602
+ const prevRename = existing[existing.length - 1];
1603
+ prevRename.timestamp = rename.timestampend;
1604
+ if (rename.fromname !== null) {
1605
+ existing.push(rename);
1606
+ currentPathToRenameChain.set(rename.fromname, existing);
1607
+ } else {
1608
+ prevRename.timestamp = rename.timestampend;
1609
+ finishedChains.push(existing);
1610
+ }
1611
+ currentPathToRenameChain.delete(rename.toname);
1612
+ }
1613
+ }
1614
+ finishedChains.push(...currentPathToRenameChain.values());
1615
+ return finishedChains;
1616
+ }
1617
+ getThreadCount(repoCommitCount) {
1618
+ const estimatedBytesPerCommit = 1300;
1619
+ const minimumBytesPerThread = 4e8;
1620
+ const systemMemoryToNotUse = 8e8;
1621
+ const threadsToNotUse = 2;
1622
+ const availableMemory = process.platform === "linux" || process.platform === "win32" ? freemem() - systemMemoryToNotUse : Math.floor(totalmem() / 2) - systemMemoryToNotUse;
1623
+ const availableThreadCount = Math.min(Math.max(cpus().length - threadsToNotUse, 2), 4);
1624
+ if (availableMemory < 1) return availableThreadCount;
1625
+ const estimatedBytesPerThread = Math.max(estimatedBytesPerCommit * repoCommitCount, minimumBytesPerThread);
1626
+ const threadsBasedOnMemory = Math.floor(availableMemory / estimatedBytesPerThread);
1627
+ return Math.max(2, Math.min(availableThreadCount, threadsBasedOnMemory));
1628
+ }
1629
+ calculateSections(commitCount, threadCount) {
1630
+ const sections = [];
1631
+ if (threadCount === 2) {
1632
+ const section1Size = Math.floor(commitCount * 53 / 100);
1633
+ sections.push([0, section1Size]);
1634
+ sections.push([section1Size, commitCount]);
1635
+ } else if (threadCount === 3) {
1636
+ const section1Size = Math.floor(commitCount * 35 / 100);
1637
+ const section2Size = Math.floor(commitCount * 33 / 100);
1638
+ sections.push([0, section1Size]);
1639
+ sections.push([section1Size, section1Size + section2Size]);
1640
+ sections.push([section1Size + section2Size, commitCount]);
1641
+ } else if (threadCount === 4) {
1642
+ const section1Size = Math.floor(commitCount * 27 / 100);
1643
+ const section2Size = Math.floor(commitCount * 26 / 100);
1644
+ const section3Size = Math.floor(commitCount * 24 / 100);
1645
+ sections.push([0, section1Size]);
1646
+ sections.push([section1Size, section1Size + section2Size]);
1647
+ sections.push([section1Size + section2Size, section1Size + section2Size + section3Size]);
1648
+ sections.push([section1Size + section2Size + section3Size, commitCount]);
1649
+ } else throw new Error("Invalid threadCount. Only 2, 3, or 4 are allowed.");
1650
+ return sections;
1651
+ }
1652
+ async loadRepoData() {
1653
+ this.analyzationStatus = "Starting";
1654
+ let commitCount = await this.gitCaller.getCommitCount();
1655
+ const priorRun = await InstanceManager.getOrCreateMetadataDB().getLastRun(this.repo, this.branch);
1656
+ if (!await this.db.commitTableEmpty()) if (priorRun) {
1657
+ const latestCommit = await this.db.getLatestCommitHash();
1658
+ commitCount = await this.gitCaller.commitCountSinceCommit(latestCommit, this.branch);
1659
+ log.info(`Repo has been analyzed previously, only analzying ${commitCount} commits`);
1660
+ } else {
1661
+ log.warn("Incomplete database found. Clearing and running complete analysis.");
1662
+ await this.db.clearAllTables();
1663
+ }
1664
+ if (commitCount < 1) return;
1665
+ const quotePathDefaultValue = await this.gitCaller.getDefaultGitSettingValue("core.quotepath");
1666
+ await this.gitCaller.setGitSetting("core.quotePath", "off");
1667
+ const renamesDefaultValue = await this.gitCaller.getDefaultGitSettingValue("diff.renames");
1668
+ await this.gitCaller.setGitSetting("diff.renames", "true");
1669
+ const renameLimitDefaultValue = await this.gitCaller.getDefaultGitSettingValue("diff.renameLimit");
1670
+ await this.gitCaller.setGitSetting("diff.renameLimit", "1000000");
1671
+ this.totalCommitCount = commitCount;
1672
+ const threadCount = this.getThreadCount(commitCount);
1673
+ this.progress = Array(threadCount).fill(0);
1674
+ this.analyzationStatus = "Hydrating";
1675
+ const sections = this.calculateSections(commitCount, threadCount);
1676
+ const promises$2 = Array.from({ length: threadCount }, async (_, i) => {
1677
+ const sectionStart = sections[i][0];
1678
+ const sectionEnd = sections[i][1];
1679
+ log.debug("start thread " + sectionStart + "-" + sectionEnd + ", " + i);
1680
+ await this.gathererWorker(sectionStart, sectionEnd, i);
1681
+ log.debug("finished thread: " + i);
1682
+ });
1683
+ await Promise.all(promises$2);
1684
+ await this.db.createIndexes();
1685
+ await this.db.checkpoint();
1686
+ await this.gitCaller.resetGitSetting("core.quotepath", quotePathDefaultValue);
1687
+ await this.gitCaller.resetGitSetting("diff.renames", renamesDefaultValue);
1688
+ await this.gitCaller.resetGitSetting("diff.renameLimit", renameLimitDefaultValue);
1689
+ await InstanceManager.getOrCreateMetadataDB().setCompletion(this.repo, this.branch, await this.db.getLatestCommitHash());
1690
+ this.analyzationStatus = "GeneratingChart";
1691
+ }
1692
+ };
1693
+ function runProcess(dir, command, args, serverInstance, index) {
1694
+ log.debug(`exec ${dir} $ ${command} ${args.join(" ")}`);
1695
+ return new Promise((resolve$2, reject) => {
1696
+ try {
1697
+ const prcs = spawn(command, args, { cwd: path.resolve(dir) });
1698
+ const chunks = [];
1699
+ const errorHandler = (buf) => reject(buf.toString().trim());
1700
+ prcs.once("error", errorHandler);
1701
+ prcs.stderr.once("data", errorHandler);
1702
+ prcs.stdout.on("data", (buf) => {
1703
+ chunks.push(buf);
1704
+ if (serverInstance && index !== void 0) serverInstance.updateProgress(index);
1705
+ });
1706
+ prcs.stdout.on("end", () => {
1707
+ resolve$2(Buffer.concat(chunks).toString().trim());
1708
+ });
1709
+ } catch (e) {
1710
+ log.error(e);
1711
+ reject(e);
1712
+ }
1713
+ });
1714
+ }
1715
+ function generateTruckFrames(length) {
1716
+ const frames = [];
1717
+ for (let i = 0; i < length; i++) {
1718
+ const prefix$1 = " ".repeat(length - i - 1);
1719
+ const frame = `${prefix$1}🚛\n`;
1720
+ frames.push(frame);
1721
+ }
1722
+ return frames;
1723
+ }
1724
+ function createTruckSpinner() {
1725
+ return getLogLevel() === null ? createSpinner("", {
1726
+ interval: 1e3 / 20,
1727
+ frames: generateTruckFrames(20)
1728
+ }) : null;
1729
+ }
1730
+ let spinner = null;
1731
+ /**
1732
+
1733
+ * This function is a wrapper around a job that provides a spinner and logs the result of the job.
1734
+
1735
+ * @returns
1736
+
1737
+ */
1738
+ async function describeAsyncJob({ job = async () => null, beforeMsg = "", afterMsg = "", errorMsg = "", ms = null }) {
1739
+ spinner = createTruckSpinner();
1740
+ const success = (text, final = false) => {
1741
+ if (getLogLevel() === LOG_LEVEL.SILENT) return;
1742
+ if (spinner === null) return log.info(text);
1743
+ spinner.success({ text });
1744
+ if (!final) spinner.start();
1745
+ };
1746
+ const output = (text) => {
1747
+ if (spinner) {
1748
+ spinner.update({
1749
+ text,
1750
+ frames: generateTruckFrames(text.length)
1751
+ });
1752
+ spinner.start();
1753
+ } else log.info(text);
1754
+ };
1755
+ const error$1 = (text) => spinner === null ? log.error(text) : spinner.error({ text });
1756
+ if (beforeMsg.length > 0) output(beforeMsg);
1757
+ try {
1758
+ const startTime = performance.now();
1759
+ const result = await job();
1760
+ const stopTime = performance.now();
1761
+ const suffix = c.gray(`${formatMs(!ms ? stopTime - startTime : ms)}`);
1762
+ success(`${afterMsg} ${suffix}`, true);
1763
+ return [result, null];
1764
+ } catch (e) {
1765
+ error$1(errorMsg);
1766
+ log.error(e);
1767
+ return [null, e];
1768
+ }
1769
+ }
1770
+ function getCommandLine() {
1771
+ switch (process.platform) {
1772
+ case "darwin": return "open";
1773
+ case "win32": return "start \"\"";
1774
+ default: return "xdg-open";
1775
+ }
1776
+ }
1777
+ function openFile(repoDir, repoPath) {
1778
+ repoPath = path.resolve(repoDir, "..", repoPath.split("/").join("/"));
1779
+ const command = `${getCommandLine()} "${repoPath}"`;
1780
+ exec(command).stderr?.on("data", (e) => {
1781
+ log.error(`Cannot open file ${path.resolve(repoDir, repoPath)}: ${e}`);
1782
+ });
1783
+ }
1784
+ let latestVersion = null;
1785
+ async function getLatestVersion() {
1786
+ if (!latestVersion) {
1787
+ const [result] = await promiseHelper(fetch("https://registry.npmjs.org/-/package/git-truck/dist-tags").then((res) => res.json()).then((pkg) => pkg.latest));
1788
+ latestVersion = result;
1789
+ }
1790
+ return latestVersion;
1791
+ }
1792
+ const readGitRepos = async (baseDir) => {
1793
+ const entries = await readdir(baseDir, { withFileTypes: true });
1794
+ return entries.filter((entry) => entry.isDirectory() && existsSync(path.join(baseDir, entry.name)) && !entry.name.startsWith(".") && existsSync(path.join(baseDir, entry.name, ".git"))).map(({ name }) => ({
1795
+ name,
1796
+ path: path.join(baseDir, name),
1797
+ parentDirPath: baseDir,
1798
+ status: "Loading"
1799
+ }));
1800
+ };
1801
+ function parseArgs(rawArgs = process.argv.slice(2)) {
1802
+ return yargsParser(rawArgs, { configuration: { "duplicate-arguments-array": false } });
1803
+ }
1804
+ function getArgsWithDefaults() {
1805
+ const args = parseArgs();
1806
+ const tempArgs = {
1807
+ path: ".",
1808
+ ...args
1809
+ };
1810
+ return tempArgs;
1811
+ }
1812
+ async function getArgs() {
1813
+ const args = getArgsWithDefaults();
1814
+ const pathIsRepo = await GitCaller.isGitRepo(args.path);
1815
+ args.path = pathIsRepo ? getBaseDirFromPath(args.path) : args.path;
1816
+ return args;
1817
+ }
1818
+ const getBaseDirFromPath = (repoPath) => path.resolve(repoPath, "..");
1819
+ function getDirName(repoPath) {
1820
+ return path.basename(path.resolve(repoPath));
1821
+ }
1822
+ export { DB, GitCaller, InstanceManager, OPTIONS_LOCAL_STORAGE_KEY, allExceptFirst, allExceptLast, dateFormatCalendarHeader, dateFormatLong, dateFormatRelative, dateFormatShort, dateTimeFormatShort, generateVersionComparisonLink, getArgs, getArgsWithDefaults, getBaseDirFromPath, getDirName, getLatestVersion, getLightness, getPathFromRepoAndHead, getSeparator, hslToHex, invariant, isBlob, isDarkColor, isTree, last, log, numToFriendlyString, openFile, promiseHelper, readGitRepos, semverCompare, sleep };