git-truck 0.8.2 → 0.8.6-experimental

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/workflows/bump-version.yml +1 -1
  2. package/README.md +17 -13
  3. package/cli.js +2 -0
  4. package/dev.js +4 -2
  5. package/package.json +4 -4
  6. package/server.ts +13 -8
  7. package/src/analyzer/analyze.server.ts +43 -76
  8. package/src/analyzer/analyze.test.ts +30 -30
  9. package/src/analyzer/args.server.ts +20 -6
  10. package/src/analyzer/constants.ts +1 -1
  11. package/src/analyzer/git-caller.server.ts +290 -0
  12. package/src/analyzer/hydrate.server.ts +1 -1
  13. package/src/analyzer/model.ts +13 -2
  14. package/src/analyzer/{util.ts → util.server.ts} +27 -33
  15. package/src/analyzer/util.test.ts +1 -1
  16. package/src/components/AnalyzingIndicator.tsx +55 -0
  17. package/src/components/Animations.ts +14 -0
  18. package/src/components/Chart.tsx +29 -8
  19. package/src/components/Details.tsx +8 -7
  20. package/src/components/GlobalInfo.tsx +19 -8
  21. package/src/components/HiddenFiles.tsx +3 -3
  22. package/src/components/Legend.tsx +1 -1
  23. package/src/components/LegendOther.tsx +42 -42
  24. package/src/components/Main.tsx +1 -1
  25. package/src/components/SearchBar.tsx +1 -6
  26. package/src/components/util.tsx +19 -10
  27. package/src/const.ts +6 -6
  28. package/src/contexts/ClickedContext.ts +17 -17
  29. package/src/contexts/DataContext.ts +12 -12
  30. package/src/contexts/MetricContext.ts +12 -12
  31. package/src/contexts/OptionsContext.ts +51 -51
  32. package/src/contexts/SearchContext.ts +19 -19
  33. package/src/lang-map.d.ts +3 -3
  34. package/src/metrics.ts +3 -2
  35. package/src/root.tsx +44 -1
  36. package/src/routes/{repo.tsx → $repo.tsx} +59 -15
  37. package/src/routes/index.tsx +156 -46
  38. package/build/index.js +0 -6836
  39. package/post-build.js +0 -14
  40. package/public/favicon.ico +0 -0
  41. package/src/analyzer/git-caller.ts +0 -117
  42. package/src/analyzer/index.ts +0 -4
@@ -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/README.md CHANGED
@@ -18,9 +18,9 @@ To check if these programs are installed, and what version you have, run `node -
18
18
 
19
19
  ## [Get started](#get-started)
20
20
 
21
- 1. In your favorite shell (for example cmd, PowerShell, etc.), navigate to the root directory of a git project you want to visualize.
22
- 2. Execute the command `npx git-truck@latest`. Click `y` if it asks you to download the tool. A blank browser-window will open and Git Truck will now start analyzing your project (This might take a while to run, especially on big projects, you can follow the progress in your terminal). If you get an error in the terminal, you can try installing the tool globally by running `npm i -g git-truck@latest` and then run `git-truck` instead.
23
- 3. When Git Truck is done analyzing your project, it will show the visualization in your browser. Enjoy!
21
+ 1. Within a git repository, or a directory containing git repositories, run the command `npx git-truck@latest`.
22
+ 2. Press `y` if it asks you to download the package.
23
+ 3. The application will now open in your default browser.
24
24
 
25
25
  ## [I got an error or I want to give feedback, what do i do?](#i-got-an-error-or-i-want-to-give-feedback-what-do-i-do)
26
26
 
@@ -38,19 +38,23 @@ npx git-truck [args]
38
38
 
39
39
  ### [Arguments](#arguments)
40
40
 
41
- | arg | description | default value |
42
- | :--------: | :-------------------------------------------------------------------: | :----------------: |
43
- | `--branch` | branch name | checked out branch |
44
- | `--path` | path to git repository | current directory |
45
- | `--log` | output log level. See [here](./src/analyzer/log.server.ts) for values | - |
46
- | `--port` | port to use for the program | 3000 |
41
+ | arg | description | default value |
42
+ | :------------------: | :-------------------------------------------------------------------: | :----------------: |
43
+ | `--branch` | branch name | checked out branch |
44
+ | `--path` | path to a folder or a git repository | current directory |
45
+ | `--log` | output log level. See [here](./src/analyzer/log.server.ts) for values | - |
46
+ | `--port` | port to use for the program | 3000 |
47
+ | `--invalidate-cache` | bypass analyzer cache manually | - |
48
+
49
+ **Note:** Using `--invalidate-cache` will cause the analyzer to run every time the client talks to the server.
47
50
 
48
51
  ### [Configuration](#configuration)
49
52
 
50
53
  You can add a `truckconfig.json` file to the root of your project, where you can define the arguments you want.
51
- Additionally you can define which git-aliases should be considered as the same person.
54
+ Additionally you can define which git-aliases should be considered as the same person using `unionedAuthors`. If provided, the first name in the array is used as the name of the person.
52
55
  You can also define files to ignore.
53
- Example:
56
+
57
+ **Example:**
54
58
 
55
59
  ```json
56
60
  {
@@ -60,7 +64,7 @@ Example:
60
64
  ["Bob", "Bobby Bob"],
61
65
  ["Alice", "aliiii", "alice alice"]
62
66
  ],
63
- "hiddenFiles": ["package-lock.json", "*.bin", "*.svg"]
67
+ "hiddenFiles": ["package-lock.json", "*.bin", "*.svg"],
68
+ "invalidateCache": true
64
69
  }
65
-
66
70
  ```
package/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require("./build/index.js")
package/dev.js CHANGED
@@ -6,12 +6,14 @@ const open = require("open")
6
6
  const getPortLib = (await import("get-port"))
7
7
  const getPort = getPortLib.default
8
8
  const port = await getPort({
9
- port: getPortLib.portNumbers(3000, 4000),
9
+ port: [80, ...getPortLib.portNumbers(3000, 4000)],
10
10
  })
11
11
 
12
+ process.env["PORT"] = port.toString()
13
+
12
14
  open("http://localhost:" + port)
13
15
  await runAll(
14
- [`dev:node -- --port ${port} ${process.argv.join(" ")}`, "dev:remix"],
16
+ [`dev:node -- ${process.argv.slice(2).join(" ")}`, "dev:remix"],
15
17
  {
16
18
  parallel: true,
17
19
  stdout: process.stdout,
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "git-truck",
3
- "version": "0.8.2",
3
+ "version": "0.8.6-experimental",
4
4
  "private": false,
5
5
  "description": "Visualizing a Git repository",
6
6
  "license": "MIT",
7
- "main": "./build/index.js",
8
- "bin": "./build/index.js",
7
+ "main": "./cli.js",
8
+ "bin": "./cli.js",
9
9
  "scripts": {
10
10
  "test": "jest --coverage",
11
11
  "clean": "rimraf -rf build public/build .cache .temp",
12
12
  "tsc": "tsc",
13
- "build": "remix setup node && cross-env NODE_ENV=production remix build && node ./post-build.js",
13
+ "build": "remix setup node && cross-env NODE_ENV=production remix build",
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
16
  "postinstall": "npm run build",
package/server.ts CHANGED
@@ -57,10 +57,7 @@ for usage instructions.`)
57
57
 
58
58
  const staticAssetsPath = join(__dirname, "../public/build")
59
59
  // Remix fingerprints its assets so we can cache forever.
60
- app.use(
61
- "/build",
62
- express.static(staticAssetsPath, { immutable: true, maxAge: "1y" })
63
- )
60
+ app.use("/build", express.static(staticAssetsPath, { immutable: true, maxAge: "1y" }))
64
61
 
65
62
  // Everything else (like favicon.ico) is cached for an hour. You may want to be
66
63
  // more aggressive with this caching.
@@ -76,16 +73,24 @@ for usage instructions.`)
76
73
  })
77
74
  )
78
75
 
76
+ let devServerPort: number | null = null
77
+ let userHasProvidedPort = false
79
78
  let minPort = 3000
80
79
 
81
- if (args.port && !isNaN(parseInt(args.port))) minPort = parseInt(args.port)
80
+ if (args.port && !isNaN(parseInt(args.port))) {
81
+ minPort = parseInt(args.port)
82
+ userHasProvidedPort = true
83
+ }
84
+
85
+ if (process.env["PORT"] && !isNaN(parseInt(process.env["PORT"]))) {
86
+ devServerPort = parseInt(process.env["PORT"])
87
+ }
82
88
 
83
89
  const getPortLib = await import("get-port")
84
90
  const getPort = getPortLib.default
85
91
  const port = await getPort({
86
- port: getPortLib.portNumbers(minPort, minPort + 1000),
92
+ port: devServerPort ?? [...(!userHasProvidedPort ? [80] : []), ...getPortLib.portNumbers(minPort, minPort + 1000)],
87
93
  })
88
-
89
94
  app.listen(port).once("listening", () => printOpen(port))
90
95
  })()
91
96
 
@@ -93,7 +98,7 @@ async function printOpen(port: number) {
93
98
  console.log()
94
99
  console.log(`Now listening on port ${port}`)
95
100
  if (process.env.NODE_ENV !== "development") {
96
- const url = "http://localhost:" + port
101
+ const url = `http://localhost:${port}`
97
102
  console.log(`Opening ${url} in your browser`)
98
103
  await open(url)
99
104
  }
@@ -1,4 +1,4 @@
1
- import fsSync, { promises as fs } from "fs"
1
+ import { promises as fs } from "fs"
2
2
  import {
3
3
  GitBlobObject,
4
4
  GitCommitObject,
@@ -7,25 +7,16 @@ import {
7
7
  AnalyzerData,
8
8
  AnalyzerDataInterfaceVersion,
9
9
  TruckUserConfig,
10
+ TruckConfig,
10
11
  } from "./model"
11
12
  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"
13
+ import { describeAsyncJob, formatMs, writeRepoToFile, getDirName } from "./util.server"
14
+ import { GitCaller } from "./git-caller.server"
23
15
  import { emptyGitCommitHash } from "./constants"
24
- import { resolve, isAbsolute, join } from "path"
16
+ import { resolve, isAbsolute } from "path"
25
17
  import { performance } from "perf_hooks"
26
18
  import { getAuthorSet, hydrateData } from "./hydrate.server"
27
19
  import {} from "@remix-run/node"
28
- import { getArgs } from "./args.server"
29
20
  import ignore from "ignore"
30
21
  import { applyIgnore, applyMetrics, initMetrics, TreeCleanup } from "./postprocessing.server"
31
22
  import latestVersion from "latest-version"
@@ -35,25 +26,6 @@ import { exec } from "child_process"
35
26
 
36
27
  let repoDir = "."
37
28
 
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
29
  export async function analyzeCommitLight(hash: string): Promise<GitCommitObjectLight> {
58
30
  const rawContent = await GitCaller.getInstance().catFileCached(hash)
59
31
  const commitRegex =
@@ -181,9 +153,9 @@ export async function updateTruckConfig(repoDir: string, updaterFn: (tc: TruckUs
181
153
  await fs.writeFile(truckConfigPath, JSON.stringify(updatedConfig, null, 2))
182
154
  }
183
155
 
184
- export async function analyze(useCache = true) {
185
- const args = await getArgs()
156
+ export async function analyze(args: TruckConfig) {
186
157
  GitCaller.initInstance(args.path)
158
+ const git = GitCaller.getInstance()
187
159
 
188
160
  if (args?.log) {
189
161
  setLogLevel(args.log as string)
@@ -197,76 +169,74 @@ export async function analyze(useCache = true) {
197
169
  const hiddenFiles = args.hiddenFiles
198
170
 
199
171
  const start = performance.now()
200
- const [branchHead, branchName] = await describeAsyncJob(
201
- () => findBranchHead(repoDir, branch),
172
+ const [findBranchHeadResult, findBranchHeadError] = await describeAsyncJob(
173
+ () => git.findBranchHead(branch),
202
174
  "Finding branch head",
203
175
  "Found branch head",
204
176
  "Error finding branch head"
205
177
  )
206
- const repoName = getRepoName(repoDir)
207
-
208
- let data: AnalyzerData | null = null
178
+ const repoName = getDirName(repoDir)
209
179
 
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
180
+ if (findBranchHeadError) throw findBranchHeadError
214
181
 
215
- // Check if the current branchHead matches the hash of the analyzed commit from the cache
216
- const branchHeadMatches = branchHead === cachedData.commit.hash
182
+ const [branchHead, branchName] = findBranchHeadResult
217
183
 
218
- // Check if the data uses the most recent analyzer data interface
219
- const dataVersionMatches = cachedData.interfaceVersion === AnalyzerDataInterfaceVersion
184
+ let data: AnalyzerData | null = null
220
185
 
221
- const cacheConditions = {
222
- branchHeadMatches,
223
- dataVersionMatches,
224
- refresh: !useCache,
225
- }
186
+ if (!args.invalidateCache) {
187
+ const [cachedData, reasons] = await GitCaller.retrieveCachedResult({
188
+ repo: repoName,
189
+ branch: branchName,
190
+ branchHead: branchHead,
191
+ })
226
192
 
227
- // Only return cached data if every criteria is met
228
- if (Object.values(cacheConditions).every(Boolean)) {
193
+ if (cachedData) {
229
194
  data = {
230
195
  ...cachedData,
231
196
  hiddenFiles,
232
197
  }
233
198
  } 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}`)
199
+ log.info(
200
+ `Reanalyzing, since the following cache conditions were not met:\n${reasons.map((r) => ` - ${r}`).join("\n")}`
201
+ )
239
202
  }
203
+ } else {
204
+ GitCaller.getInstance().setUseCache(false)
240
205
  }
241
206
 
242
207
  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")
208
+ const quotePathDefaultValue = await git.getDefaultGitSettingValue("core.quotepath")
209
+ await git.setGitSetting("core.quotePath", "off")
210
+ const renamesDefaultValue = await git.getDefaultGitSettingValue("diff.renames")
211
+ await git.setGitSetting("diff.renames", "true")
212
+ const renameLimitDefaultValue = await git.getDefaultGitSettingValue("diff.renameLimit")
213
+ await git.setGitSetting("diff.renameLimit", "1000000")
214
+ const hasUnstagedChanges = await git.hasUnstagedChanges()
249
215
 
250
216
  const runDateEpoch = Date.now()
251
- const repoTree = await describeAsyncJob(
217
+ const [repoTree, repoTreeError] = await describeAsyncJob(
252
218
  () => analyzeCommit(repoName, branchHead),
253
219
  "Analyzing commit tree",
254
220
  "Commit tree analyzed",
255
221
  "Error analyzing commit tree"
256
222
  )
257
223
 
258
- const hydratedRepoTree = await describeAsyncJob(
224
+ if (repoTreeError) throw repoTreeError
225
+
226
+ const [hydratedRepoTree, hydratedRepoTreeError] = await describeAsyncJob(
259
227
  () => hydrateData(repoDir, repoTree),
260
228
  "Hydrating commit tree",
261
229
  "Commit tree hydrated",
262
230
  "Error hydrating commit tree"
263
231
  )
264
232
 
265
- await resetGitSetting(repoDir, "core.quotepath", quotePathDefaultValue)
266
- await resetGitSetting(repoDir, "diff.renames", renamesDefaultValue)
267
- await resetGitSetting(repoDir, "diff.renameLimit", renameLimitDefaultValue)
233
+ await git.resetGitSetting("core.quotepath", quotePathDefaultValue)
234
+ await git.resetGitSetting("diff.renames", renamesDefaultValue)
235
+ await git.resetGitSetting("diff.renameLimit", renameLimitDefaultValue)
236
+
237
+ const defaultOutPath = GitCaller.getCachePath(repoName, branchName)
238
+ if (hydratedRepoTreeError) throw hydratedRepoTreeError
268
239
 
269
- const defaultOutPath = getOutPathFromRepoAndBranch(repoName, branchName)
270
240
  let outPath = resolve((args.out as string) ?? defaultOutPath)
271
241
  if (!isAbsolute(outPath)) outPath = resolve(process.cwd(), outPath)
272
242
 
@@ -289,6 +259,7 @@ export async function analyze(useCache = true) {
289
259
  currentVersion: pkg.version,
290
260
  latestVersion: latestV,
291
261
  lastRunEpoch: runDateEpoch,
262
+ hasUnstagedChanges,
292
263
  }
293
264
 
294
265
  await describeAsyncJob(
@@ -315,7 +286,3 @@ export async function analyze(useCache = true) {
315
286
 
316
287
  return data
317
288
  }
318
-
319
- export function getOutPathFromRepoAndBranch(repoName: string, branchName: string) {
320
- return resolve(__dirname, "..", ".temp", repoName, `${branchName}.json`)
321
- }
@@ -1,30 +1,30 @@
1
- import { getCoAuthors } from "./coauthors.server"
2
-
3
- describe("getCoAuthors", () => {
4
- it("Should return none", () => {
5
- const actual = getCoAuthors("lorem ipsum\n\nCo-authored-by:")
6
- expect(actual.length).toBe(0)
7
- })
8
-
9
- it("Should return none when empty input", () => {
10
- const actual = getCoAuthors("")
11
- expect(actual.length).toBe(0)
12
- })
13
-
14
- it("Should return 2 authors", () => {
15
- const sampleDescription =
16
- "did some stuff\n\nCo-authored-by: Bob Bobby <bob@example.com>\nCo-authored-by: Alice Lmao <alice@example.com>"
17
- const expected = [
18
- {
19
- name: "Bob Bobby",
20
- email: "bob@example.com",
21
- },
22
- {
23
- name: "Alice Lmao",
24
- email: "alice@example.com",
25
- },
26
- ]
27
- const actual = getCoAuthors(sampleDescription)
28
- expect(actual).toStrictEqual(expected)
29
- })
30
- })
1
+ import { getCoAuthors } from "./coauthors.server"
2
+
3
+ describe("getCoAuthors", () => {
4
+ it("Should return none", () => {
5
+ const actual = getCoAuthors("lorem ipsum\n\nCo-authored-by:")
6
+ expect(actual.length).toBe(0)
7
+ })
8
+
9
+ it("Should return none when empty input", () => {
10
+ const actual = getCoAuthors("")
11
+ expect(actual.length).toBe(0)
12
+ })
13
+
14
+ it("Should return 2 authors", () => {
15
+ const sampleDescription =
16
+ "did some stuff\n\nCo-authored-by: Bob Bobby <bob@example.com>\nCo-authored-by: Alice Lmao <alice@example.com>"
17
+ const expected = [
18
+ {
19
+ name: "Bob Bobby",
20
+ email: "bob@example.com",
21
+ },
22
+ {
23
+ name: "Alice Lmao",
24
+ email: "alice@example.com",
25
+ },
26
+ ]
27
+ const actual = getCoAuthors(sampleDescription)
28
+ expect(actual).toStrictEqual(expected)
29
+ })
30
+ })
@@ -2,6 +2,9 @@ import yargsParser from "yargs-parser"
2
2
  import { promises as fs } from "fs"
3
3
  import { resolve } from "path"
4
4
  import { TruckConfig, TruckUserConfig } from "./model"
5
+ import { GitCaller } from "./git-caller.server"
6
+ import { getBaseDirFromPath } from "./util.server"
7
+ import { log } from "./log.server"
5
8
 
6
9
  export function parseArgs(rawArgs: string[] = process.argv.slice(2)) {
7
10
  return yargsParser(rawArgs, {
@@ -11,24 +14,35 @@ export function parseArgs(rawArgs: string[] = process.argv.slice(2)) {
11
14
  })
12
15
  }
13
16
 
14
- export async function getArgs(): Promise<TruckConfig> {
17
+ export async function getArgsWithDefaults(): Promise<TruckConfig> {
15
18
  const args = parseArgs()
16
19
  const tempArgs = {
17
20
  path: ".",
18
- branch: null,
19
21
  hiddenFiles: [] as string[],
20
22
  unionedAuthors: [] as string[][],
23
+ invalidateCache: false,
21
24
  ...args,
22
25
  }
23
26
 
27
+ return tempArgs
28
+ }
29
+
30
+ export async function getTruckConfigWithArgs(repo: string): Promise<TruckConfig> {
31
+ const args = await getArgsWithDefaults()
32
+
33
+ const pathIsRepo = await GitCaller.isGitRepo(args.path)
34
+ args.path = pathIsRepo ? getBaseDirFromPath(args.path) : args.path
35
+
24
36
  let config: TruckUserConfig = {}
25
37
  try {
26
- const configContents = JSON.parse(await fs.readFile(resolve(tempArgs.path, "truckconfig.json"), "utf-8"))
38
+ const configContents = JSON.parse(await fs.readFile(resolve(args.path, repo, "truckconfig.json"), "utf-8"))
27
39
  config = configContents
28
- } catch (e) {}
40
+ } catch (e) {
41
+ log.warn(`No truckconfig.json found in repo ${repo}`)
42
+ }
29
43
 
30
44
  return {
31
- ...tempArgs,
45
+ ...args,
32
46
  ...config,
33
- } as TruckConfig
47
+ }
34
48
  }
@@ -1 +1 @@
1
- export const emptyGitCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1
+ export const emptyGitCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"