git-truck 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.eslintrc.json +20 -0
  2. package/.github/workflows/test-and-build.yml +39 -0
  3. package/.husky/pre-commit +4 -0
  4. package/.truckignore +1 -0
  5. package/.vscode/extensions.json +7 -0
  6. package/.vscode/launch.json +24 -0
  7. package/.vscode/settings.json +6 -0
  8. package/LICENSE +21 -0
  9. package/README.md +60 -0
  10. package/app/README.md +46 -0
  11. package/app/entry.client.tsx +4 -0
  12. package/app/entry.server.tsx +27 -0
  13. package/app/parser/.eslintignore +3 -0
  14. package/app/parser/.eslintrc.json +18 -0
  15. package/app/parser/src/TruckIgnore.server.ts +20 -0
  16. package/app/parser/src/constants.ts +1 -0
  17. package/app/parser/src/hydrate.server.ts +199 -0
  18. package/app/parser/src/index.ts +5 -0
  19. package/app/parser/src/log.server.ts +97 -0
  20. package/app/parser/src/model.ts +77 -0
  21. package/app/parser/src/parse.server.ts +276 -0
  22. package/app/parser/src/parse.test.ts +32 -0
  23. package/app/parser/src/queue.ts +86 -0
  24. package/app/parser/src/util.test.ts +8 -0
  25. package/app/parser/src/util.ts +216 -0
  26. package/app/root.tsx +35 -0
  27. package/app/routes/index.tsx +43 -0
  28. package/app/src/authorUnionUtil.test.ts +82 -0
  29. package/app/src/authorUnionUtil.ts +52 -0
  30. package/app/src/components/AuthorDistFragment.tsx +27 -0
  31. package/app/src/components/AuthorDistOther.tsx +24 -0
  32. package/app/src/components/Chart.tsx +362 -0
  33. package/app/src/components/Details.tsx +177 -0
  34. package/app/src/components/EnumSelect.tsx +31 -0
  35. package/app/src/components/GlobalInfo.tsx +17 -0
  36. package/app/src/components/Legend.tsx +65 -0
  37. package/app/src/components/LegendFragment.tsx +29 -0
  38. package/app/src/components/LegendOther.tsx +43 -0
  39. package/app/src/components/Main.tsx +19 -0
  40. package/app/src/components/Options.tsx +24 -0
  41. package/app/src/components/Providers.tsx +121 -0
  42. package/app/src/components/SearchBar.tsx +36 -0
  43. package/app/src/components/SidePanel.tsx +25 -0
  44. package/app/src/components/Spacer.tsx +62 -0
  45. package/app/src/components/Toggle.tsx +21 -0
  46. package/app/src/components/Tooltip.tsx +131 -0
  47. package/app/src/components/util.tsx +150 -0
  48. package/app/src/const.ts +5 -0
  49. package/app/src/contexts/DataContext.ts +12 -0
  50. package/app/src/contexts/MetricContext.ts +14 -0
  51. package/app/src/contexts/OptionsContext.ts +46 -0
  52. package/app/src/contexts/SearchContext.ts +16 -0
  53. package/app/src/extension-color.ts +34 -0
  54. package/app/src/hooks.ts +17 -0
  55. package/app/src/lang-map.d.ts +3 -0
  56. package/app/src/metrics.ts +319 -0
  57. package/app/src/react-app-env.d.ts +1 -0
  58. package/app/src/reportWebVitals.ts +15 -0
  59. package/app/src/setupTests.ts +5 -0
  60. package/app/src/util.ts +33 -0
  61. package/app/styles/App.css +3 -0
  62. package/app/styles/Chart.css +26 -0
  63. package/app/styles/index.css +35 -0
  64. package/app/styles/vars.css +17 -0
  65. package/cli.js +2 -0
  66. package/package.json +99 -0
  67. package/parse.sh +26 -0
  68. package/project-statement.md +43 -0
  69. package/public/favicon.ico +0 -0
  70. package/remix.config.js +21 -0
  71. package/remix.env.d.ts +2 -0
  72. package/server.js +41 -0
  73. package/truckconfig.json +8 -0
  74. package/tsconfig.json +20 -0
@@ -0,0 +1,131 @@
1
+ import { Box, BoxSubTitle, LegendDot } from "./util"
2
+ import { useMouse } from "react-use"
3
+ import { useMemo, useRef } from "react"
4
+ import styled from "styled-components"
5
+ import { HydratedGitBlobObject } from "../../parser/src/model"
6
+ import { useOptions } from "../contexts/OptionsContext"
7
+ import { Spacer } from "./Spacer"
8
+ import { useMetricCaches } from "../contexts/MetricContext"
9
+ import { MetricType } from "../metrics"
10
+ import { useCSSVar } from "../hooks"
11
+ import { dateFormatRelative } from "../util"
12
+
13
+ const TooltipBox = styled(Box)<{
14
+ visible: boolean
15
+ right: boolean
16
+ }>`
17
+ padding: calc(0.5 * var(--unit)) var(--unit);
18
+ min-width: 0;
19
+ width: max-content;
20
+ position: absolute;
21
+ top: 0px;
22
+ left: 0px;
23
+ will-change: transform visibility;
24
+ display: flex;
25
+ border-radius: calc(2 * var(--unit));
26
+ align-items: center;
27
+
28
+ pointer-events: none;
29
+ visibility: ${({ visible }) => (visible ? "visible" : "hidden")};
30
+ `
31
+
32
+ const TooltipContainer = styled.div`
33
+ position: absolute;
34
+ inset: 0;
35
+ pointer-events: none;
36
+ overflow: hidden;
37
+ `
38
+
39
+ interface TooltipProps {
40
+ hoveredBlob: HydratedGitBlobObject | null
41
+ }
42
+
43
+ export function Tooltip({ hoveredBlob }: TooltipProps) {
44
+ const tooltipContainerRef = useRef<HTMLDivElement>(null)
45
+ const { metricType } = useOptions()
46
+ const documentElementRef = useRef(document.documentElement)
47
+ const mouse = useMouse(documentElementRef)
48
+ const unitRaw = useCSSVar("--unit")
49
+ const unit = unitRaw ? Number(unitRaw.replace("px", "")) : 0
50
+ const metricCaches = useMetricCaches()
51
+ const color = useMemo(() => {
52
+ if (!hoveredBlob) {
53
+ return null
54
+ }
55
+ const { colormap } = metricCaches.get(metricType)!
56
+ const color = colormap.get(hoveredBlob.path)
57
+ return color
58
+ }, [hoveredBlob, metricCaches, metricType])
59
+ const toolTipWidth = tooltipContainerRef.current
60
+ ? tooltipContainerRef.current.getBoundingClientRect().width
61
+ : 0
62
+
63
+ const right = mouse.docX + toolTipWidth < window.innerWidth - 3 * unit
64
+
65
+ const visible = hoveredBlob !== null
66
+ const transformStyles = { transform: "none" }
67
+ if (visible) {
68
+ if (right)
69
+ transformStyles.transform = `translate(calc(var(--unit) + ${mouse.docX}px), calc(var(--unit) + ${mouse.docY}px))`
70
+ else
71
+ transformStyles.transform = `translate(calc(var(--unit) * -1 + ${mouse.docX}px - 100%), calc(var(--unit) + ${mouse.docY}px))`
72
+ }
73
+
74
+ return (
75
+ <TooltipContainer>
76
+ <TooltipBox
77
+ ref={tooltipContainerRef}
78
+ right={right}
79
+ visible={visible}
80
+ style={transformStyles}
81
+ >
82
+ {color ? <LegendDot dotColor={color} /> : null}
83
+ <Spacer horizontal />
84
+ <BoxSubTitle>{hoveredBlob?.name}</BoxSubTitle>
85
+ <Spacer horizontal />
86
+ <ColorMetricDependentInfo
87
+ metric={metricType}
88
+ hoveredBlob={hoveredBlob}
89
+ />
90
+ </TooltipBox>
91
+ </TooltipContainer>
92
+ )
93
+ }
94
+
95
+ function ColorMetricDependentInfo(props: {
96
+ metric: MetricType
97
+ hoveredBlob: HydratedGitBlobObject | null
98
+ }) {
99
+ switch (props.metric) {
100
+ case "MOST_COMMITS":
101
+ const noCommits = props.hoveredBlob?.noCommits
102
+ if (!noCommits) return null
103
+ return (
104
+ <>
105
+ {noCommits} commit{noCommits > 1 ? <>s</> : null}
106
+ </>
107
+ )
108
+ case "LAST_CHANGED":
109
+ const epoch = props.hoveredBlob?.lastChangeEpoch
110
+ if (!epoch) return null
111
+ return <>{dateFormatRelative(epoch)}</>
112
+ case "SINGLE_AUTHOR":
113
+ const authors = props.hoveredBlob
114
+ ? Object.entries(props.hoveredBlob?.authors)
115
+ : []
116
+ switch (authors.length) {
117
+ case 0:
118
+ return null
119
+ case 1:
120
+ return <>{authors[0][0]} dominates</>
121
+ default:
122
+ return <>{authors.length} authors</>
123
+ }
124
+ case "TOP_CONTRIBUTOR":
125
+ const dominant = props.hoveredBlob?.dominantAuthor
126
+ if (!dominant) return null
127
+ return <>{dominant[0]}</>
128
+ default:
129
+ return null
130
+ }
131
+ }
@@ -0,0 +1,150 @@
1
+ import styled from "styled-components"
2
+
3
+ export const BoxTitle = styled.h2`
4
+ font-size: 1.5em;
5
+ font-weight: bold;
6
+ margin-bottom: 0;
7
+ margin-top: 0;
8
+ color: var(--title-color);
9
+ overflow: hidden;
10
+ text-overflow: ellipsis;
11
+ white-space: nowrap;
12
+ `
13
+
14
+ export const BoxSubTitle = styled.h2`
15
+ font-size: 1em;
16
+ font-weight: bold;
17
+ margin-bottom: 0;
18
+ margin-top: 0;
19
+ color: var(--title-color);
20
+ `
21
+
22
+ export const CloseButton = styled.button`
23
+ background: none;
24
+ border: none;
25
+ font-size: larger;
26
+ position: absolute;
27
+ top: calc(var(--unit));
28
+ right: calc(var(--unit));
29
+ cursor: pointer;
30
+ `
31
+
32
+ export const Container = styled.div`
33
+ height: 100%;
34
+ display: grid;
35
+ grid-template-columns: var(--side-panel-width) 1fr;
36
+ grid-template-rows: 1fr;
37
+ `
38
+
39
+ export const Box = styled.div`
40
+ /* border: 1px var(--border-color-alpha) solid; */
41
+ margin: var(--unit);
42
+ color: var(--text-color);
43
+ width: calc(var(--side-panel-width) - 6 * var(--unit));
44
+ background-color: #fff;
45
+ border-radius: var(--unit);
46
+ padding: calc(2 * var(--unit));
47
+ position: relative;
48
+ /* Generated with: https://shadows.brumm.af/ */
49
+ box-shadow: var(--shadow);
50
+ `
51
+
52
+ export const Stack = styled.div`
53
+ display: flex;
54
+ flex-direction: column;
55
+ `
56
+
57
+ export const Label = styled.label`
58
+ padding-left: calc(var(--unit) + var(--border-width));
59
+ font-weight: bold;
60
+ font-size: 0.8em;
61
+ `
62
+
63
+ export const Select = styled.select`
64
+ width: 100%;
65
+ display: block;
66
+ padding: var(--unit);
67
+ border: 1px var(--border-color) solid;
68
+ border-radius: calc(0.5 * var(--unit));
69
+ `
70
+
71
+ export const SearchField = styled.input`
72
+ border: 1px var(--border-color) solid;
73
+ flex-grow: 1;
74
+ border-radius: calc(0.5 * var(--unit));
75
+ padding: var(--unit);
76
+ `
77
+
78
+ export const LegendEntry = styled.div`
79
+ font-size: small;
80
+ position: relative;
81
+ display: flex;
82
+ flex-direction: row;
83
+ place-items: center;
84
+ line-height: 100%;
85
+ margin: 0px;
86
+ `
87
+
88
+ export const LegendDot = styled.div<{ dotColor: string }>`
89
+ height: 100%;
90
+ aspect-ratio: 1;
91
+ width: 1em;
92
+ border-radius: 50%;
93
+ background-color: ${({ dotColor }) => dotColor};
94
+ box-shadow: var(--small-shadow);
95
+ `
96
+
97
+ export const LegendLable = styled.p`
98
+ padding: 0px;
99
+ margin: 0px;
100
+ font-weight: bold;
101
+ `
102
+
103
+ export const ToggleButton = styled.button<{
104
+ collapse: boolean
105
+ relative: boolean
106
+ }>`
107
+ position: ${(props) => (props.relative ? "relative" : "absolute")};
108
+ top: ${(props) => (props.relative ? "unset" : "var(--unit)")};
109
+ right: ${(props) => (props.relative ? "unset" : "var(--unit)")};
110
+ border: none;
111
+ background-color: rgba(0, 0, 0, 0);
112
+ transition-duration: 0.4s;
113
+ color: grey;
114
+ transform-origin: 50% 55%;
115
+ transform: ${(props) => (props.collapse ? "rotate(180deg)" : "none")};
116
+ font-size: large;
117
+ &:hover {
118
+ color: #606060;
119
+ cursor: pointer;
120
+ }
121
+ `
122
+
123
+ export const LegendGradient = styled.div<{ min: string; max: string }>`
124
+ background-image: linear-gradient(
125
+ to right,
126
+ ${(props) => `${props.min},${props.max}`}
127
+ );
128
+ width: 100%;
129
+ height: 20px;
130
+ border-radius: calc(var(--unit) * 0.5);
131
+ `
132
+
133
+ export const GradientLegendDiv = styled.div`
134
+ display: flex;
135
+ flex-direction: row;
136
+ justify-content: space-between;
137
+ `
138
+
139
+ export const DetailsKey = styled.span<{ grow?: boolean }>`
140
+ white-space: pre;
141
+ font-size: 0.9em;
142
+ font-weight: 500;
143
+ opacity: 0.7;
144
+ `
145
+
146
+ export const DetailsValue = styled.p`
147
+ overflow-wrap: anywhere;
148
+ font-size: 0.9em;
149
+ text-align: right;
150
+ `
@@ -0,0 +1,5 @@
1
+ export const treemapPadding = 20
2
+ export const bubblePadding = 10
3
+ export const textSpacingFromCircle = 0
4
+ export const searchMatchColor = "red"
5
+ export const estimatedLetterWidth = 7
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from "react"
2
+ import { ParserData } from "../../parser/src/model"
3
+
4
+ export const DataContext = createContext<ParserData | undefined>(undefined)
5
+
6
+ export function useData() {
7
+ const context = useContext(DataContext)
8
+ if (!context) {
9
+ throw new Error("useData must be used within a DataContext")
10
+ }
11
+ return context
12
+ }
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react"
2
+ import { MetricCache, MetricType } from "../metrics"
3
+
4
+ export const MetricContext = createContext<
5
+ Map<MetricType, MetricCache> | undefined
6
+ >(undefined)
7
+
8
+ export function useMetricCaches() {
9
+ const context = useContext(MetricContext)
10
+ if (!context) {
11
+ throw new Error("useMetricCache must be used within a MetricContext")
12
+ }
13
+ return context
14
+ }
@@ -0,0 +1,46 @@
1
+ import { createContext, useContext } from "react"
2
+ import { HydratedGitBlobObject } from "../../parser/src/model"
3
+ import { Metric, MetricType } from "../metrics"
4
+
5
+ export const Chart = {
6
+ BUBBLE_CHART: "Bubble chart",
7
+ TREE_MAP: "Tree map",
8
+ }
9
+
10
+ export type ChartType = keyof typeof Chart
11
+
12
+ export interface Options {
13
+ metricType: MetricType
14
+ chartType: ChartType
15
+ clickedBlob: HydratedGitBlobObject | null
16
+ setClickedBlob: (blob: HydratedGitBlobObject | null) => void
17
+ setMetricType: (metricType: MetricType) => void
18
+ setChartType: (chartType: ChartType) => void
19
+ }
20
+
21
+ export const OptionsContext = createContext<Options | undefined>(undefined)
22
+
23
+ export function useOptions() {
24
+ const context = useContext(OptionsContext)
25
+ if (!context) {
26
+ throw new Error("useSearch must be used within a SearchProvider")
27
+ }
28
+ return context
29
+ }
30
+
31
+ export function getDefaultOptions() {
32
+ return {
33
+ metricType: Object.keys(Metric)[0] as MetricType,
34
+ chartType: Object.keys(Chart)[0] as ChartType,
35
+ clickedBlob: null,
36
+ setClickedBlob: () => {
37
+ throw new Error("No setClickedBlob function provided")
38
+ },
39
+ setChartType: () => {
40
+ throw new Error("No chartTypeSetter provided")
41
+ },
42
+ setMetricType: () => {
43
+ throw new Error("No metricTypeSetter provided")
44
+ },
45
+ }
46
+ }
@@ -0,0 +1,16 @@
1
+ import { createContext, Dispatch, SetStateAction, useContext } from "react"
2
+
3
+ type Search = {
4
+ searchText: string
5
+ setSearchText: Dispatch<SetStateAction<string>>
6
+ }
7
+
8
+ export const SearchContext = createContext<Search | undefined>(undefined)
9
+
10
+ export function useSearch(): Search {
11
+ const context = useContext(SearchContext)
12
+ if (!context) {
13
+ throw new Error("useSearch must be used within a SearchProvider")
14
+ }
15
+ return context
16
+ }
@@ -0,0 +1,34 @@
1
+ import gitcolors from "github-colors/colors.json"
2
+ import langMap from "lang-map"
3
+
4
+ interface ColorResult {
5
+ lang: string
6
+ color: string | null
7
+ }
8
+
9
+ const lowercasedColors = new Map<string, ColorResult>()
10
+ for (const [key, value] of Object.entries(gitcolors)) {
11
+ lowercasedColors.set(key.toLowerCase(), {
12
+ ...value,
13
+ lang: key,
14
+ })
15
+ }
16
+
17
+ export function getColorFromExtension(extension: string) {
18
+ const langMatches = langMap.languages(extension.toLowerCase())
19
+ const langs = []
20
+ if (!langMatches) return null
21
+ let colorResult = null
22
+ // Loop through lang resuts
23
+ for (const langResult of langMatches) {
24
+ // If we have a color for the language, return it
25
+ let match = lowercasedColors.get(langResult)
26
+ if (match) {
27
+ colorResult = match
28
+ langs.push(colorResult.lang)
29
+ break
30
+ }
31
+ }
32
+ if (!colorResult) return null
33
+ return colorResult.color
34
+ }
@@ -0,0 +1,17 @@
1
+ import { MutableRefObject, useMemo } from "react"
2
+ import { useComponentSize as useCompSize } from "react-use-size"
3
+
4
+ type RefAndSize = [MutableRefObject<any>, { width: number; height: number }]
5
+
6
+ export function useComponentSize() {
7
+ const { ref, width, height } = useCompSize()
8
+ const size: RefAndSize = useMemo(
9
+ () => [ref, { width, height }],
10
+ [ref, width, height]
11
+ )
12
+ return size
13
+ }
14
+
15
+ export function useCSSVar(varName: string) {
16
+ return getComputedStyle(document.documentElement).getPropertyValue(varName)
17
+ }
@@ -0,0 +1,3 @@
1
+ declare module "lang-map" {
2
+ export function languages(extension: string): string[]
3
+ }