git-truck 0.8.1 → 0.8.4-1

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
@@ -4383,7 +4383,7 @@ function parents(obj) {
4383
4383
  async function diffAndUpdate_mut(data, currCommit, parentHash) {
4384
4384
  const { author } = currCommit;
4385
4385
  const currHash = currCommit.hash;
4386
- log.debug(`comparing [${currHash}] -> [${parentHash}]`);
4386
+ log.debug(`comparing aaaa [${currHash}] -> [${parentHash}]`);
4387
4387
  const fileChanges = await gitDiffNumStatAnalyzed(parentHash, currHash, renamedFiles);
4388
4388
  for (const fileChange of fileChanges) {
4389
4389
  const { pos, neg, file } = fileChange;
@@ -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.0";
5536
+ var version = "0.8.4-0";
5537
5537
  var private2 = false;
5538
5538
  var description = "Visualizing a Git repository";
5539
5539
  var license = "MIT";
@@ -5546,6 +5546,7 @@ var scripts = {
5546
5546
  build: "remix setup node && cross-env NODE_ENV=production remix build && node ./post-build.js",
5547
5547
  dev: "cross-env NODE_ENV=development remix build && node dev.js",
5548
5548
  "dev:remix": "cross-env NODE_ENV=development remix watch",
5549
+ postinstall: "npm run build",
5549
5550
  "dev:node": "cross-env NODE_ENV=development nodemon --watch ./build/index.js ./build/index.js --",
5550
5551
  start: "cross-env NODE_ENV=production node ./build/index.js",
5551
5552
  format: "eslint --cache --fix src/**/*.{ts,tsx} && prettier --loglevel warn --write src/**/*.{ts,tsx}"
@@ -5703,7 +5704,9 @@ async function findBranchHead(repo, branch) {
5703
5704
  async function analyzeCommitLight(hash) {
5704
5705
  const rawContent = await GitCaller.getInstance().catFileCached(hash);
5705
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");
5706
5708
  const match = commitRegex.exec(rawContent);
5709
+ log.debug("after match: " + JSON.stringify(match));
5707
5710
  const groups = (match == null ? void 0 : match.groups) ?? {};
5708
5711
  const tree = groups["tree"];
5709
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.1",
3
+ "version": "0.8.4-1",
4
4
  "private": false,
5
5
  "description": "Visualizing a Git repository",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "build": "remix setup node && cross-env NODE_ENV=production remix build && node ./post-build.js",
14
14
  "dev": "cross-env NODE_ENV=development remix build && node dev.js",
15
15
  "dev:remix": "cross-env NODE_ENV=development remix watch",
16
+ "postinstall": "npm run build",
16
17
  "dev:node": "cross-env NODE_ENV=development nodemon --watch ./build/index.js ./build/index.js --",
17
18
  "start": "cross-env NODE_ENV=production node ./build/index.js",
18
19
  "format": "eslint --cache --fix src/**/*.{ts,tsx} && prettier --loglevel warn --write src/**/*.{ts,tsx}"
@@ -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
+ }
@@ -1,186 +1,186 @@
1
- import { emptyGitCommitHash } from "./constants"
2
- import isBinaryPath from "is-binary-path"
3
- import { join } from "path"
4
- import { log } from "./log.server"
5
- import {
6
- GitBlobObject,
7
- GitCommitObject,
8
- GitCommitObjectLight,
9
- HydratedGitBlobObject,
10
- HydratedGitCommitObject,
11
- HydratedGitTreeObject,
12
- Person,
13
- PersonWithTime,
14
- } from "./model"
15
- import { analyzeCommitLight } from "./analyze.server"
16
- import { gitDiffNumStatAnalyzed, lookupFileInTree } from "./util"
17
- import { Queue } from "./queue"
18
-
19
- const renamedFiles = new Map<string, string>()
20
-
21
- const authors = new Set<string>()
22
-
23
- export async function hydrateData(repo: string, commit: GitCommitObject): Promise<HydratedGitCommitObject> {
24
- const data = commit as HydratedGitCommitObject
25
-
26
- initially_mut(data)
27
-
28
- const { hash: first } = data
29
-
30
- await bfs(first, repo, data)
31
-
32
- finally_mut(data)
33
-
34
- return data
35
- }
36
-
37
- export function getAuthorSet() {
38
- return Array.from(authors)
39
- }
40
-
41
- function initially_mut(data: HydratedGitCommitObject) {
42
- data.minNoCommits = Number.MAX_VALUE
43
- data.maxNoCommits = Number.MIN_VALUE
44
- data.oldestLatestChangeEpoch = Number.MAX_VALUE
45
- data.newestLatestChangeEpoch = Number.MIN_VALUE
46
-
47
- addAuthorsField_mut(data.tree)
48
- }
49
-
50
- function addAuthorsField_mut(tree: HydratedGitTreeObject) {
51
- for (const child of tree.children) {
52
- if (child.type === "blob") {
53
- child.authors = {}
54
- } else {
55
- addAuthorsField_mut(child)
56
- }
57
- }
58
- }
59
-
60
- async function bfs(first: string, repo: string, data: HydratedGitCommitObject) {
61
- const expandedHashes = new Set<string>()
62
- const queue = new Queue<string>()
63
-
64
- queue.enqueue(first)
65
-
66
- while (!queue.isEmpty()) {
67
- const currHash = queue.dequeue()
68
-
69
- if (expandedHashes.has(currHash)) continue
70
-
71
- expandedHashes.add(currHash)
72
-
73
- // don't compare the empty commit to it's parent
74
- if (currHash == emptyGitCommitHash) continue
75
-
76
- const currCommit = await analyzeCommitLight(currHash)
77
- authors.add((currCommit.author as Person).name)
78
- for (const person of currCommit.coauthors) authors.add(person.name)
79
-
80
- const parentsOfCurr = parents(currCommit)
81
-
82
- for (const parentHash of parentsOfCurr) {
83
- switch (parentsOfCurr.size) {
84
- case 2: // curr is a merge commit
85
- queue.enqueue(parentHash)
86
- break
87
- case 1: // curr is a linear commit
88
- await diffAndUpdate_mut(data, currCommit, parentHash)
89
- queue.enqueue(parentHash)
90
- break
91
- default:
92
- // curr is the root commit
93
- await diffAndUpdate_mut(data, currCommit, emptyGitCommitHash)
94
- break
95
- }
96
- }
97
- }
98
- }
99
-
100
- function parents(obj: GitCommitObject | GitCommitObjectLight): Set<string> {
101
- const parents = new Set<string>()
102
-
103
- if (obj.parent !== null) parents.add(obj.parent)
104
- if (obj.parent2 !== null) parents.add(obj.parent2)
105
-
106
- return parents
107
- }
108
-
109
- async function diffAndUpdate_mut(data: HydratedGitCommitObject, currCommit: GitCommitObjectLight, parentHash: string) {
110
- const { author } = currCommit
111
-
112
- const currHash = currCommit.hash
113
-
114
- log.debug(`comparing [${currHash}] -> [${parentHash}]`)
115
-
116
- const fileChanges = await gitDiffNumStatAnalyzed(parentHash, currHash, renamedFiles)
117
-
118
- for (const fileChange of fileChanges) {
119
- const { pos, neg, file } = fileChange
120
-
121
- const blob = await lookupFileInTree(data.tree, file)
122
-
123
- if (file === "dev/null") continue
124
- if (blob) {
125
- updateBlob_mut(blob, author, currCommit, pos, neg)
126
- }
127
- }
128
- }
129
-
130
- function isBinaryFile(blob: GitBlobObject) {
131
- return isBinaryPath(join(blob.path, blob.name))
132
- }
133
-
134
- function updateBlob_mut(
135
- blob: GitBlobObject,
136
- author: PersonWithTime,
137
- currCommit: GitCommitObjectLight,
138
- pos: number,
139
- neg: number
140
- ) {
141
- const noCommits = 1 + ((blob as HydratedGitBlobObject).noCommits ?? 0)
142
-
143
- const isBinary = isBinaryFile(blob)
144
-
145
- const hydratedBlob = {
146
- ...blob,
147
- authors: (blob as HydratedGitBlobObject).authors ?? {},
148
- noLines: isBinary ? 0 : blob.content?.split("\n").length,
149
- noCommits: noCommits,
150
- isBinary: isBinary,
151
- } as HydratedGitBlobObject
152
-
153
- if (!isBinary) {
154
- const current = hydratedBlob.authors?.[author.name] ?? 0
155
-
156
- const newValue = current + pos + neg
157
- if (newValue > 0) {
158
- for (const coauthor of currCommit.coauthors) {
159
- hydratedBlob.authors[coauthor.name] = newValue
160
- }
161
- hydratedBlob.authors[author.name] = newValue
162
- }
163
- }
164
-
165
- if (!hydratedBlob.lastChangeEpoch) {
166
- const epoch = currCommit.author.timestamp
167
- hydratedBlob.lastChangeEpoch = epoch
168
- }
169
-
170
- Object.assign(blob, hydratedBlob)
171
- log.debug(`Updated blob ${blob.name} from commit ${currCommit.hash}`)
172
- }
173
-
174
- function finally_mut(data: HydratedGitCommitObject) {
175
- discardContentField_mut(data.tree)
176
- }
177
-
178
- function discardContentField_mut(tree: HydratedGitTreeObject) {
179
- for (const child of tree.children) {
180
- if (child.type === "blob") {
181
- child.content = undefined
182
- } else {
183
- discardContentField_mut(child)
184
- }
185
- }
186
- }
1
+ import { emptyGitCommitHash } from "./constants"
2
+ import isBinaryPath from "is-binary-path"
3
+ import { join } from "path"
4
+ import { log } from "./log.server"
5
+ import {
6
+ GitBlobObject,
7
+ GitCommitObject,
8
+ GitCommitObjectLight,
9
+ HydratedGitBlobObject,
10
+ HydratedGitCommitObject,
11
+ HydratedGitTreeObject,
12
+ Person,
13
+ PersonWithTime,
14
+ } from "./model"
15
+ import { analyzeCommitLight } from "./analyze.server"
16
+ import { gitDiffNumStatAnalyzed, lookupFileInTree } from "./util"
17
+ import { Queue } from "./queue"
18
+
19
+ const renamedFiles = new Map<string, string>()
20
+
21
+ const authors = new Set<string>()
22
+
23
+ export async function hydrateData(repo: string, commit: GitCommitObject): Promise<HydratedGitCommitObject> {
24
+ const data = commit as HydratedGitCommitObject
25
+
26
+ initially_mut(data)
27
+
28
+ const { hash: first } = data
29
+
30
+ await bfs(first, repo, data)
31
+
32
+ finally_mut(data)
33
+
34
+ return data
35
+ }
36
+
37
+ export function getAuthorSet() {
38
+ return Array.from(authors)
39
+ }
40
+
41
+ function initially_mut(data: HydratedGitCommitObject) {
42
+ data.minNoCommits = Number.MAX_VALUE
43
+ data.maxNoCommits = Number.MIN_VALUE
44
+ data.oldestLatestChangeEpoch = Number.MAX_VALUE
45
+ data.newestLatestChangeEpoch = Number.MIN_VALUE
46
+
47
+ addAuthorsField_mut(data.tree)
48
+ }
49
+
50
+ function addAuthorsField_mut(tree: HydratedGitTreeObject) {
51
+ for (const child of tree.children) {
52
+ if (child.type === "blob") {
53
+ child.authors = {}
54
+ } else {
55
+ addAuthorsField_mut(child)
56
+ }
57
+ }
58
+ }
59
+
60
+ async function bfs(first: string, repo: string, data: HydratedGitCommitObject) {
61
+ const expandedHashes = new Set<string>()
62
+ const queue = new Queue<string>()
63
+
64
+ queue.enqueue(first)
65
+
66
+ while (!queue.isEmpty()) {
67
+ const currHash = queue.dequeue()
68
+
69
+ if (expandedHashes.has(currHash)) continue
70
+
71
+ expandedHashes.add(currHash)
72
+
73
+ // don't compare the empty commit to it's parent
74
+ if (currHash == emptyGitCommitHash) continue
75
+
76
+ const currCommit = await analyzeCommitLight(currHash)
77
+ authors.add((currCommit.author as Person).name)
78
+ for (const person of currCommit.coauthors) authors.add(person.name)
79
+
80
+ const parentsOfCurr = parents(currCommit)
81
+
82
+ for (const parentHash of parentsOfCurr) {
83
+ switch (parentsOfCurr.size) {
84
+ case 2: // curr is a merge commit
85
+ queue.enqueue(parentHash)
86
+ break
87
+ case 1: // curr is a linear commit
88
+ await diffAndUpdate_mut(data, currCommit, parentHash)
89
+ queue.enqueue(parentHash)
90
+ break
91
+ default:
92
+ // curr is the root commit
93
+ await diffAndUpdate_mut(data, currCommit, emptyGitCommitHash)
94
+ break
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ function parents(obj: GitCommitObject | GitCommitObjectLight): Set<string> {
101
+ const parents = new Set<string>()
102
+
103
+ if (obj.parent !== null) parents.add(obj.parent)
104
+ if (obj.parent2 !== null) parents.add(obj.parent2)
105
+
106
+ return parents
107
+ }
108
+
109
+ async function diffAndUpdate_mut(data: HydratedGitCommitObject, currCommit: GitCommitObjectLight, parentHash: string) {
110
+ const { author } = currCommit
111
+
112
+ const currHash = currCommit.hash
113
+
114
+ log.debug(`comparing aaaa [${currHash}] -> [${parentHash}]`)
115
+
116
+ const fileChanges = await gitDiffNumStatAnalyzed(parentHash, currHash, renamedFiles)
117
+
118
+ for (const fileChange of fileChanges) {
119
+ const { pos, neg, file } = fileChange
120
+
121
+ const blob = await lookupFileInTree(data.tree, file)
122
+
123
+ if (file === "dev/null") continue
124
+ if (blob) {
125
+ updateBlob_mut(blob, author, currCommit, pos, neg)
126
+ }
127
+ }
128
+ }
129
+
130
+ function isBinaryFile(blob: GitBlobObject) {
131
+ return isBinaryPath(join(blob.path, blob.name))
132
+ }
133
+
134
+ function updateBlob_mut(
135
+ blob: GitBlobObject,
136
+ author: PersonWithTime,
137
+ currCommit: GitCommitObjectLight,
138
+ pos: number,
139
+ neg: number
140
+ ) {
141
+ const noCommits = 1 + ((blob as HydratedGitBlobObject).noCommits ?? 0)
142
+
143
+ const isBinary = isBinaryFile(blob)
144
+
145
+ const hydratedBlob = {
146
+ ...blob,
147
+ authors: (blob as HydratedGitBlobObject).authors ?? {},
148
+ noLines: isBinary ? 0 : blob.content?.split("\n").length,
149
+ noCommits: noCommits,
150
+ isBinary: isBinary,
151
+ } as HydratedGitBlobObject
152
+
153
+ if (!isBinary) {
154
+ const current = hydratedBlob.authors?.[author.name] ?? 0
155
+
156
+ const newValue = current + pos + neg
157
+ if (newValue > 0) {
158
+ for (const coauthor of currCommit.coauthors) {
159
+ hydratedBlob.authors[coauthor.name] = newValue
160
+ }
161
+ hydratedBlob.authors[author.name] = newValue
162
+ }
163
+ }
164
+
165
+ if (!hydratedBlob.lastChangeEpoch) {
166
+ const epoch = currCommit.author.timestamp
167
+ hydratedBlob.lastChangeEpoch = epoch
168
+ }
169
+
170
+ Object.assign(blob, hydratedBlob)
171
+ log.debug(`Updated blob ${blob.name} from commit ${currCommit.hash}`)
172
+ }
173
+
174
+ function finally_mut(data: HydratedGitCommitObject) {
175
+ discardContentField_mut(data.tree)
176
+ }
177
+
178
+ function discardContentField_mut(tree: HydratedGitTreeObject) {
179
+ for (const child of tree.children) {
180
+ if (child.type === "blob") {
181
+ child.content = undefined
182
+ } else {
183
+ discardContentField_mut(child)
184
+ }
185
+ }
186
+ }