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.
- package/.github/workflows/bump-version.yml +1 -1
- package/build/index.js +5 -2
- package/package.json +2 -1
- package/src/analyzer/analyze.server.ts +322 -321
- package/src/analyzer/hydrate.server.ts +186 -186
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
await
|
|
245
|
-
|
|
246
|
-
await
|
|
247
|
-
|
|
248
|
-
await
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
await resetGitSetting(repoDir, "
|
|
267
|
-
await resetGitSetting(repoDir, "diff.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
`
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
+
}
|