git-truck 0.8.2 → 0.8.4-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.
@@ -31,7 +31,7 @@ jobs:
31
31
  env:
32
32
  GITHUB_TOKEN: ${{ secrets.TOKEN }}
33
33
 
34
- - run: npm install --package-lock-only
34
+ - run: npm install --package-lock-only --ignore-scripts
35
35
  - run: git add package-lock.json
36
36
  - run: git config user.email "version-bot@example.com"
37
37
  - run: git config user.name "Version Bot"
package/build/index.js CHANGED
@@ -5533,7 +5533,7 @@ async function latestVersion(packageName, options) {
5533
5533
 
5534
5534
  // package.json
5535
5535
  var name = "git-truck";
5536
- var version = "0.8.2";
5536
+ var version = "0.8.3";
5537
5537
  var private2 = false;
5538
5538
  var description = "Visualizing a Git repository";
5539
5539
  var license = "MIT";
@@ -5704,7 +5704,9 @@ async function findBranchHead(repo, branch) {
5704
5704
  async function analyzeCommitLight(hash) {
5705
5705
  const rawContent = await GitCaller.getInstance().catFileCached(hash);
5706
5706
  const commitRegex = /tree (?<tree>.*)\s*(?:parent (?<parent>.*)\s*)?(?:parent (?<parent2>.*)\s*)?author (?<authorName>.*?) <(?<authorEmail>.*?)> (?<authorTimeStamp>\d*?) (?<authorTimeZone>.*?)\s*committer (?<committerName>.*?) <(?<committerEmail>.*?)> (?<committerTimeStamp>\d*?) (?<committerTimeZone>.*)\s*(?:gpgsig (?:.|\s)*?-----END PGP SIGNATURE-----)?\s*(?<message>.*)\s*(?<description>(?:.|\s)*)/gm;
5707
+ log.debug("before match");
5707
5708
  const match = commitRegex.exec(rawContent);
5709
+ log.debug("after match: " + JSON.stringify(match));
5708
5710
  const groups = (match == null ? void 0 : match.groups) ?? {};
5709
5711
  const tree = groups["tree"];
5710
5712
  const parent = groups["parent"] ?? emptyGitCommitHash;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-truck",
3
- "version": "0.8.2",
3
+ "version": "0.8.4-0",
4
4
  "private": false,
5
5
  "description": "Visualizing a Git repository",
6
6
  "license": "MIT",
@@ -1,321 +1,322 @@
1
- import fsSync, { promises as fs } from "fs"
2
- import {
3
- GitBlobObject,
4
- GitCommitObject,
5
- GitCommitObjectLight,
6
- GitTreeObject,
7
- AnalyzerData,
8
- AnalyzerDataInterfaceVersion,
9
- TruckUserConfig,
10
- } from "./model"
11
- import { log, setLogLevel } from "./log.server"
12
- import {
13
- describeAsyncJob,
14
- formatMs,
15
- writeRepoToFile,
16
- getCurrentBranch,
17
- getRepoName,
18
- getDefaultGitSettingValue,
19
- resetGitSetting,
20
- setGitSetting,
21
- } from "./util"
22
- import { GitCaller } from "./git-caller"
23
- import { emptyGitCommitHash } from "./constants"
24
- import { resolve, isAbsolute, join } from "path"
25
- import { performance } from "perf_hooks"
26
- import { getAuthorSet, hydrateData } from "./hydrate.server"
27
- import {} from "@remix-run/node"
28
- import { getArgs } from "./args.server"
29
- import ignore from "ignore"
30
- import { applyIgnore, applyMetrics, initMetrics, TreeCleanup } from "./postprocessing.server"
31
- import latestVersion from "latest-version"
32
- import pkg from "../../package.json"
33
- import { getCoAuthors } from "./coauthors.server"
34
- import { exec } from "child_process"
35
-
36
- let repoDir = "."
37
-
38
- export async function findBranchHead(repo: string, branch: string | null) {
39
- if (branch === null) branch = await getCurrentBranch(repo)
40
-
41
- const gitFolder = join(repo, ".git")
42
- if (!fsSync.existsSync(gitFolder)) {
43
- throw Error(`${repo} is not a git repository`)
44
- }
45
- // Find file containing the branch head
46
- const branchPath = join(gitFolder, "refs/heads/" + branch)
47
- const absolutePath = join(process.cwd(), branchPath)
48
- log.debug("Looking for branch head at " + absolutePath)
49
-
50
- const branchHead = (await fs.readFile(branchPath, "utf-8")).trim()
51
- log.debug(`${branch} -> [commit]${branchHead}`)
52
- if (!branchHead) throw Error("Branch head not found")
53
-
54
- return [branchHead, branch]
55
- }
56
-
57
- export async function analyzeCommitLight(hash: string): Promise<GitCommitObjectLight> {
58
- const rawContent = await GitCaller.getInstance().catFileCached(hash)
59
- const commitRegex =
60
- /tree (?<tree>.*)\s*(?:parent (?<parent>.*)\s*)?(?:parent (?<parent2>.*)\s*)?author (?<authorName>.*?) <(?<authorEmail>.*?)> (?<authorTimeStamp>\d*?) (?<authorTimeZone>.*?)\s*committer (?<committerName>.*?) <(?<committerEmail>.*?)> (?<committerTimeStamp>\d*?) (?<committerTimeZone>.*)\s*(?:gpgsig (?:.|\s)*?-----END PGP SIGNATURE-----)?\s*(?<message>.*)\s*(?<description>(?:.|\s)*)/gm
61
-
62
- const match = commitRegex.exec(rawContent)
63
- const groups = match?.groups ?? {}
64
-
65
- const tree = groups["tree"]
66
- const parent = groups["parent"] ?? emptyGitCommitHash
67
- const parent2 = groups["parent2"] ?? null
68
- const author = {
69
- name: groups["authorName"],
70
- email: groups["authorEmail"],
71
- timestamp: Number(groups["authorTimeStamp"]),
72
- timezone: groups["authorTimeZone"],
73
- }
74
- const committer = {
75
- name: groups["committerName"],
76
- email: groups["committerEmail"],
77
- timestamp: Number(groups["committerTimeStamp"]),
78
- timezone: groups["committerTimeZone"],
79
- }
80
- const message = groups["message"]
81
- const description = groups["description"]
82
- const coauthors = description ? getCoAuthors(description) : []
83
-
84
- return {
85
- type: "commit",
86
- hash,
87
- tree,
88
- parent,
89
- parent2,
90
- author,
91
- committer,
92
- message,
93
- description,
94
- coauthors,
95
- }
96
- }
97
-
98
- export async function analyzeCommit(repoName: string, hash: string): Promise<GitCommitObject> {
99
- if (hash === undefined) {
100
- throw Error("Hash is required")
101
- }
102
- const { tree, ...commit } = await analyzeCommitLight(hash)
103
- const commitObject = {
104
- ...commit,
105
- tree: await analyzeTree(repoName, repoName, tree),
106
- }
107
- return commitObject
108
- }
109
-
110
- async function analyzeTree(path: string, name: string, hash: string): Promise<GitTreeObject> {
111
- const rawContent = await GitCaller.getInstance().catFileCached(hash)
112
- const entries = rawContent.split("\n").filter((x) => x.trim().length > 0)
113
-
114
- const children: (GitTreeObject | GitBlobObject)[] = []
115
- for await (const line of entries) {
116
- const catFileRegex = /^.+?\s(?<type>\w+)\s(?<hash>.+?)\s+(?<name>.+?)\s*$/g
117
- const groups = catFileRegex.exec(line)?.groups ?? {}
118
-
119
- const type = groups["type"]
120
- const hash = groups["hash"]
121
- const name = groups["name"]
122
-
123
- const newPath = [path, name].join("/")
124
- log.debug(`Path: ${newPath}`)
125
-
126
- switch (type) {
127
- case "tree":
128
- children.push(await analyzeTree(newPath, name, hash))
129
- break
130
- case "blob":
131
- children.push({
132
- type: "blob",
133
- hash,
134
- path: newPath,
135
- name,
136
- content: await GitCaller.getInstance().catFileCached(hash),
137
- blameAuthors: await GitCaller.getInstance().parseBlame(newPath),
138
- })
139
- break
140
- default:
141
- throw new Error(` type ${type}`)
142
- }
143
- }
144
-
145
- return {
146
- type: "tree",
147
- path,
148
- name,
149
- hash,
150
- children,
151
- }
152
- }
153
-
154
- function getCommandLine() {
155
- switch (process.platform) {
156
- case "darwin":
157
- return "open" // MacOS
158
- case "win32":
159
- return "start" // Windows
160
- default:
161
- return "xdg-open" // Linux
162
- }
163
- }
164
-
165
- export function openFile(path: string) {
166
- path = path.split("/").slice(1).join("/") ?? path.split("\\").slice(1).join("\\")
167
- exec(`${getCommandLine()} ${resolve(repoDir, path)}`).stderr?.on("data", (e) => {
168
- // TODO show error in UI
169
- log.error(`Cannot open file ${resolve(repoDir, path)}: ${e}`)
170
- })
171
- }
172
-
173
- export async function updateTruckConfig(repoDir: string, updaterFn: (tc: TruckUserConfig) => TruckUserConfig) {
174
- const truckConfigPath = resolve(repoDir, "truckconfig.json")
175
- let currentConfig: TruckUserConfig = {}
176
- try {
177
- const configFileContents = await fs.readFile(truckConfigPath, "utf-8")
178
- if (configFileContents) currentConfig = JSON.parse(configFileContents)
179
- } catch (e) {}
180
- const updatedConfig = updaterFn(currentConfig)
181
- await fs.writeFile(truckConfigPath, JSON.stringify(updatedConfig, null, 2))
182
- }
183
-
184
- export async function analyze(useCache = true) {
185
- const args = await getArgs()
186
- GitCaller.initInstance(args.path)
187
-
188
- if (args?.log) {
189
- setLogLevel(args.log as string)
190
- }
191
-
192
- repoDir = args.path
193
- if (!isAbsolute(repoDir)) repoDir = resolve(process.cwd(), repoDir)
194
-
195
- const branch = args.branch
196
-
197
- const hiddenFiles = args.hiddenFiles
198
-
199
- const start = performance.now()
200
- const [branchHead, branchName] = await describeAsyncJob(
201
- () => findBranchHead(repoDir, branch),
202
- "Finding branch head",
203
- "Found branch head",
204
- "Error finding branch head"
205
- )
206
- const repoName = getRepoName(repoDir)
207
-
208
- let data: AnalyzerData | null = null
209
-
210
- const dataPath = getOutPathFromRepoAndBranch(repoName, branchName)
211
- if (fsSync.existsSync(dataPath)) {
212
- const path = getOutPathFromRepoAndBranch(repoName, branchName)
213
- const cachedData = JSON.parse(await fs.readFile(path, "utf8")) as AnalyzerData
214
-
215
- // Check if the current branchHead matches the hash of the analyzed commit from the cache
216
- const branchHeadMatches = branchHead === cachedData.commit.hash
217
-
218
- // Check if the data uses the most recent analyzer data interface
219
- const dataVersionMatches = cachedData.interfaceVersion === AnalyzerDataInterfaceVersion
220
-
221
- const cacheConditions = {
222
- branchHeadMatches,
223
- dataVersionMatches,
224
- refresh: !useCache,
225
- }
226
-
227
- // Only return cached data if every criteria is met
228
- if (Object.values(cacheConditions).every(Boolean)) {
229
- data = {
230
- ...cachedData,
231
- hiddenFiles,
232
- }
233
- } else {
234
- const reasons = Object.entries(cacheConditions)
235
- .filter(([, value]) => !value)
236
- .map(([key, value]) => `${key}: ${value}`)
237
- .join(", ")
238
- log.info(`Reanalyzing, since the following cache conditions were not met: ${reasons}`)
239
- }
240
- }
241
-
242
- if (data === null) {
243
- const quotePathDefaultValue = await getDefaultGitSettingValue(repoDir, "core.quotepath")
244
- await setGitSetting(repoDir, "core.quotePath", "off")
245
- const renamesDefaultValue = await getDefaultGitSettingValue(repoDir, "diff.renames")
246
- await setGitSetting(repoDir, "diff.renames", "true")
247
- const renameLimitDefaultValue = await getDefaultGitSettingValue(repoDir, "diff.renameLimit")
248
- await setGitSetting(repoDir, "diff.renameLimit", "1000000")
249
-
250
- const runDateEpoch = Date.now()
251
- const repoTree = await describeAsyncJob(
252
- () => analyzeCommit(repoName, branchHead),
253
- "Analyzing commit tree",
254
- "Commit tree analyzed",
255
- "Error analyzing commit tree"
256
- )
257
-
258
- const hydratedRepoTree = await describeAsyncJob(
259
- () => hydrateData(repoDir, repoTree),
260
- "Hydrating commit tree",
261
- "Commit tree hydrated",
262
- "Error hydrating commit tree"
263
- )
264
-
265
- await resetGitSetting(repoDir, "core.quotepath", quotePathDefaultValue)
266
- await resetGitSetting(repoDir, "diff.renames", renamesDefaultValue)
267
- await resetGitSetting(repoDir, "diff.renameLimit", renameLimitDefaultValue)
268
-
269
- const defaultOutPath = getOutPathFromRepoAndBranch(repoName, branchName)
270
- let outPath = resolve((args.out as string) ?? defaultOutPath)
271
- if (!isAbsolute(outPath)) outPath = resolve(process.cwd(), outPath)
272
-
273
- let latestV: string | undefined
274
-
275
- try {
276
- latestV = await latestVersion(pkg.name)
277
- } catch {}
278
-
279
- const authorUnions = args.unionedAuthors as string[][]
280
- data = {
281
- cached: false,
282
- hiddenFiles,
283
- authors: getAuthorSet(),
284
- repo: repoName,
285
- branch: branchName,
286
- commit: hydratedRepoTree,
287
- authorUnions: authorUnions,
288
- interfaceVersion: AnalyzerDataInterfaceVersion,
289
- currentVersion: pkg.version,
290
- latestVersion: latestV,
291
- lastRunEpoch: runDateEpoch,
292
- }
293
-
294
- await describeAsyncJob(
295
- () =>
296
- writeRepoToFile(outPath, {
297
- ...data,
298
- cached: true,
299
- } as AnalyzerData),
300
- "Writing data to file",
301
- `Wrote data to ${resolve(outPath)}`,
302
- `Error writing data to file ${outPath}`
303
- )
304
- }
305
-
306
- const truckignore = ignore().add(hiddenFiles)
307
- data.commit.tree = applyIgnore(data.commit.tree, truckignore)
308
- TreeCleanup(data.commit.tree)
309
- initMetrics(data)
310
- data.commit.tree = applyMetrics(data, data.commit.tree)
311
-
312
- const stop = performance.now()
313
-
314
- log.raw(`\nDone in ${formatMs(stop - start)}`)
315
-
316
- return data
317
- }
318
-
319
- export function getOutPathFromRepoAndBranch(repoName: string, branchName: string) {
320
- return resolve(__dirname, "..", ".temp", repoName, `${branchName}.json`)
321
- }
1
+ import fsSync, { promises as fs } from "fs"
2
+ import {
3
+ GitBlobObject,
4
+ GitCommitObject,
5
+ GitCommitObjectLight,
6
+ GitTreeObject,
7
+ AnalyzerData,
8
+ AnalyzerDataInterfaceVersion,
9
+ TruckUserConfig,
10
+ } from "./model"
11
+ import { log, setLogLevel } from "./log.server"
12
+ import {
13
+ describeAsyncJob,
14
+ formatMs,
15
+ writeRepoToFile,
16
+ getCurrentBranch,
17
+ getRepoName,
18
+ getDefaultGitSettingValue,
19
+ resetGitSetting,
20
+ setGitSetting,
21
+ } from "./util"
22
+ import { GitCaller } from "./git-caller"
23
+ import { emptyGitCommitHash } from "./constants"
24
+ import { resolve, isAbsolute, join } from "path"
25
+ import { performance } from "perf_hooks"
26
+ import { getAuthorSet, hydrateData } from "./hydrate.server"
27
+ import {} from "@remix-run/node"
28
+ import { getArgs } from "./args.server"
29
+ import ignore from "ignore"
30
+ import { applyIgnore, applyMetrics, initMetrics, TreeCleanup } from "./postprocessing.server"
31
+ import latestVersion from "latest-version"
32
+ import pkg from "../../package.json"
33
+ import { getCoAuthors } from "./coauthors.server"
34
+ import { exec } from "child_process"
35
+
36
+ let repoDir = "."
37
+
38
+ export async function findBranchHead(repo: string, branch: string | null) {
39
+ if (branch === null) branch = await getCurrentBranch(repo)
40
+
41
+ const gitFolder = join(repo, ".git")
42
+ if (!fsSync.existsSync(gitFolder)) {
43
+ throw Error(`${repo} is not a git repository`)
44
+ }
45
+ // Find file containing the branch head
46
+ const branchPath = join(gitFolder, "refs/heads/" + branch)
47
+ const absolutePath = join(process.cwd(), branchPath)
48
+ log.debug("Looking for branch head at " + absolutePath)
49
+
50
+ const branchHead = (await fs.readFile(branchPath, "utf-8")).trim()
51
+ log.debug(`${branch} -> [commit]${branchHead}`)
52
+ if (!branchHead) throw Error("Branch head not found")
53
+
54
+ return [branchHead, branch]
55
+ }
56
+
57
+ export async function analyzeCommitLight(hash: string): Promise<GitCommitObjectLight> {
58
+ const rawContent = await GitCaller.getInstance().catFileCached(hash)
59
+ const commitRegex =
60
+ /tree (?<tree>.*)\s*(?:parent (?<parent>.*)\s*)?(?:parent (?<parent2>.*)\s*)?author (?<authorName>.*?) <(?<authorEmail>.*?)> (?<authorTimeStamp>\d*?) (?<authorTimeZone>.*?)\s*committer (?<committerName>.*?) <(?<committerEmail>.*?)> (?<committerTimeStamp>\d*?) (?<committerTimeZone>.*)\s*(?:gpgsig (?:.|\s)*?-----END PGP SIGNATURE-----)?\s*(?<message>.*)\s*(?<description>(?:.|\s)*)/gm
61
+ log.debug("before match")
62
+ const match = commitRegex.exec(rawContent)
63
+ log.debug("after match: " + JSON.stringify(match))
64
+ const groups = match?.groups ?? {}
65
+
66
+ const tree = groups["tree"]
67
+ const parent = groups["parent"] ?? emptyGitCommitHash
68
+ const parent2 = groups["parent2"] ?? null
69
+ const author = {
70
+ name: groups["authorName"],
71
+ email: groups["authorEmail"],
72
+ timestamp: Number(groups["authorTimeStamp"]),
73
+ timezone: groups["authorTimeZone"],
74
+ }
75
+ const committer = {
76
+ name: groups["committerName"],
77
+ email: groups["committerEmail"],
78
+ timestamp: Number(groups["committerTimeStamp"]),
79
+ timezone: groups["committerTimeZone"],
80
+ }
81
+ const message = groups["message"]
82
+ const description = groups["description"]
83
+ const coauthors = description ? getCoAuthors(description) : []
84
+
85
+ return {
86
+ type: "commit",
87
+ hash,
88
+ tree,
89
+ parent,
90
+ parent2,
91
+ author,
92
+ committer,
93
+ message,
94
+ description,
95
+ coauthors,
96
+ }
97
+ }
98
+
99
+ export async function analyzeCommit(repoName: string, hash: string): Promise<GitCommitObject> {
100
+ if (hash === undefined) {
101
+ throw Error("Hash is required")
102
+ }
103
+ const { tree, ...commit } = await analyzeCommitLight(hash)
104
+ const commitObject = {
105
+ ...commit,
106
+ tree: await analyzeTree(repoName, repoName, tree),
107
+ }
108
+ return commitObject
109
+ }
110
+
111
+ async function analyzeTree(path: string, name: string, hash: string): Promise<GitTreeObject> {
112
+ const rawContent = await GitCaller.getInstance().catFileCached(hash)
113
+ const entries = rawContent.split("\n").filter((x) => x.trim().length > 0)
114
+
115
+ const children: (GitTreeObject | GitBlobObject)[] = []
116
+ for await (const line of entries) {
117
+ const catFileRegex = /^.+?\s(?<type>\w+)\s(?<hash>.+?)\s+(?<name>.+?)\s*$/g
118
+ const groups = catFileRegex.exec(line)?.groups ?? {}
119
+
120
+ const type = groups["type"]
121
+ const hash = groups["hash"]
122
+ const name = groups["name"]
123
+
124
+ const newPath = [path, name].join("/")
125
+ log.debug(`Path: ${newPath}`)
126
+
127
+ switch (type) {
128
+ case "tree":
129
+ children.push(await analyzeTree(newPath, name, hash))
130
+ break
131
+ case "blob":
132
+ children.push({
133
+ type: "blob",
134
+ hash,
135
+ path: newPath,
136
+ name,
137
+ content: await GitCaller.getInstance().catFileCached(hash),
138
+ blameAuthors: await GitCaller.getInstance().parseBlame(newPath),
139
+ })
140
+ break
141
+ default:
142
+ throw new Error(` type ${type}`)
143
+ }
144
+ }
145
+
146
+ return {
147
+ type: "tree",
148
+ path,
149
+ name,
150
+ hash,
151
+ children,
152
+ }
153
+ }
154
+
155
+ function getCommandLine() {
156
+ switch (process.platform) {
157
+ case "darwin":
158
+ return "open" // MacOS
159
+ case "win32":
160
+ return "start" // Windows
161
+ default:
162
+ return "xdg-open" // Linux
163
+ }
164
+ }
165
+
166
+ export function openFile(path: string) {
167
+ path = path.split("/").slice(1).join("/") ?? path.split("\\").slice(1).join("\\")
168
+ exec(`${getCommandLine()} ${resolve(repoDir, path)}`).stderr?.on("data", (e) => {
169
+ // TODO show error in UI
170
+ log.error(`Cannot open file ${resolve(repoDir, path)}: ${e}`)
171
+ })
172
+ }
173
+
174
+ export async function updateTruckConfig(repoDir: string, updaterFn: (tc: TruckUserConfig) => TruckUserConfig) {
175
+ const truckConfigPath = resolve(repoDir, "truckconfig.json")
176
+ let currentConfig: TruckUserConfig = {}
177
+ try {
178
+ const configFileContents = await fs.readFile(truckConfigPath, "utf-8")
179
+ if (configFileContents) currentConfig = JSON.parse(configFileContents)
180
+ } catch (e) {}
181
+ const updatedConfig = updaterFn(currentConfig)
182
+ await fs.writeFile(truckConfigPath, JSON.stringify(updatedConfig, null, 2))
183
+ }
184
+
185
+ export async function analyze(useCache = true) {
186
+ const args = await getArgs()
187
+ GitCaller.initInstance(args.path)
188
+
189
+ if (args?.log) {
190
+ setLogLevel(args.log as string)
191
+ }
192
+
193
+ repoDir = args.path
194
+ if (!isAbsolute(repoDir)) repoDir = resolve(process.cwd(), repoDir)
195
+
196
+ const branch = args.branch
197
+
198
+ const hiddenFiles = args.hiddenFiles
199
+
200
+ const start = performance.now()
201
+ const [branchHead, branchName] = await describeAsyncJob(
202
+ () => findBranchHead(repoDir, branch),
203
+ "Finding branch head",
204
+ "Found branch head",
205
+ "Error finding branch head"
206
+ )
207
+ const repoName = getRepoName(repoDir)
208
+
209
+ let data: AnalyzerData | null = null
210
+
211
+ const dataPath = getOutPathFromRepoAndBranch(repoName, branchName)
212
+ if (fsSync.existsSync(dataPath)) {
213
+ const path = getOutPathFromRepoAndBranch(repoName, branchName)
214
+ const cachedData = JSON.parse(await fs.readFile(path, "utf8")) as AnalyzerData
215
+
216
+ // Check if the current branchHead matches the hash of the analyzed commit from the cache
217
+ const branchHeadMatches = branchHead === cachedData.commit.hash
218
+
219
+ // Check if the data uses the most recent analyzer data interface
220
+ const dataVersionMatches = cachedData.interfaceVersion === AnalyzerDataInterfaceVersion
221
+
222
+ const cacheConditions = {
223
+ branchHeadMatches,
224
+ dataVersionMatches,
225
+ refresh: !useCache,
226
+ }
227
+
228
+ // Only return cached data if every criteria is met
229
+ if (Object.values(cacheConditions).every(Boolean)) {
230
+ data = {
231
+ ...cachedData,
232
+ hiddenFiles,
233
+ }
234
+ } else {
235
+ const reasons = Object.entries(cacheConditions)
236
+ .filter(([, value]) => !value)
237
+ .map(([key, value]) => `${key}: ${value}`)
238
+ .join(", ")
239
+ log.info(`Reanalyzing, since the following cache conditions were not met: ${reasons}`)
240
+ }
241
+ }
242
+
243
+ if (data === null) {
244
+ const quotePathDefaultValue = await getDefaultGitSettingValue(repoDir, "core.quotepath")
245
+ await setGitSetting(repoDir, "core.quotePath", "off")
246
+ const renamesDefaultValue = await getDefaultGitSettingValue(repoDir, "diff.renames")
247
+ await setGitSetting(repoDir, "diff.renames", "true")
248
+ const renameLimitDefaultValue = await getDefaultGitSettingValue(repoDir, "diff.renameLimit")
249
+ await setGitSetting(repoDir, "diff.renameLimit", "1000000")
250
+
251
+ const runDateEpoch = Date.now()
252
+ const repoTree = await describeAsyncJob(
253
+ () => analyzeCommit(repoName, branchHead),
254
+ "Analyzing commit tree",
255
+ "Commit tree analyzed",
256
+ "Error analyzing commit tree"
257
+ )
258
+
259
+ const hydratedRepoTree = await describeAsyncJob(
260
+ () => hydrateData(repoDir, repoTree),
261
+ "Hydrating commit tree",
262
+ "Commit tree hydrated",
263
+ "Error hydrating commit tree"
264
+ )
265
+
266
+ await resetGitSetting(repoDir, "core.quotepath", quotePathDefaultValue)
267
+ await resetGitSetting(repoDir, "diff.renames", renamesDefaultValue)
268
+ await resetGitSetting(repoDir, "diff.renameLimit", renameLimitDefaultValue)
269
+
270
+ const defaultOutPath = getOutPathFromRepoAndBranch(repoName, branchName)
271
+ let outPath = resolve((args.out as string) ?? defaultOutPath)
272
+ if (!isAbsolute(outPath)) outPath = resolve(process.cwd(), outPath)
273
+
274
+ let latestV: string | undefined
275
+
276
+ try {
277
+ latestV = await latestVersion(pkg.name)
278
+ } catch {}
279
+
280
+ const authorUnions = args.unionedAuthors as string[][]
281
+ data = {
282
+ cached: false,
283
+ hiddenFiles,
284
+ authors: getAuthorSet(),
285
+ repo: repoName,
286
+ branch: branchName,
287
+ commit: hydratedRepoTree,
288
+ authorUnions: authorUnions,
289
+ interfaceVersion: AnalyzerDataInterfaceVersion,
290
+ currentVersion: pkg.version,
291
+ latestVersion: latestV,
292
+ lastRunEpoch: runDateEpoch,
293
+ }
294
+
295
+ await describeAsyncJob(
296
+ () =>
297
+ writeRepoToFile(outPath, {
298
+ ...data,
299
+ cached: true,
300
+ } as AnalyzerData),
301
+ "Writing data to file",
302
+ `Wrote data to ${resolve(outPath)}`,
303
+ `Error writing data to file ${outPath}`
304
+ )
305
+ }
306
+
307
+ const truckignore = ignore().add(hiddenFiles)
308
+ data.commit.tree = applyIgnore(data.commit.tree, truckignore)
309
+ TreeCleanup(data.commit.tree)
310
+ initMetrics(data)
311
+ data.commit.tree = applyMetrics(data, data.commit.tree)
312
+
313
+ const stop = performance.now()
314
+
315
+ log.raw(`\nDone in ${formatMs(stop - start)}`)
316
+
317
+ return data
318
+ }
319
+
320
+ export function getOutPathFromRepoAndBranch(repoName: string, branchName: string) {
321
+ return resolve(__dirname, "..", ".temp", repoName, `${branchName}.json`)
322
+ }