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.
- package/.github/workflows/bump-version.yml +1 -1
- package/README.md +17 -13
- package/cli.js +2 -0
- package/dev.js +4 -2
- package/package.json +4 -4
- package/server.ts +13 -8
- package/src/analyzer/analyze.server.ts +43 -76
- package/src/analyzer/analyze.test.ts +30 -30
- package/src/analyzer/args.server.ts +20 -6
- package/src/analyzer/constants.ts +1 -1
- package/src/analyzer/git-caller.server.ts +290 -0
- package/src/analyzer/hydrate.server.ts +1 -1
- package/src/analyzer/model.ts +13 -2
- package/src/analyzer/{util.ts → util.server.ts} +27 -33
- package/src/analyzer/util.test.ts +1 -1
- package/src/components/AnalyzingIndicator.tsx +55 -0
- package/src/components/Animations.ts +14 -0
- package/src/components/Chart.tsx +29 -8
- package/src/components/Details.tsx +8 -7
- package/src/components/GlobalInfo.tsx +19 -8
- package/src/components/HiddenFiles.tsx +3 -3
- package/src/components/Legend.tsx +1 -1
- package/src/components/LegendOther.tsx +42 -42
- package/src/components/Main.tsx +1 -1
- package/src/components/SearchBar.tsx +1 -6
- package/src/components/util.tsx +19 -10
- package/src/const.ts +6 -6
- package/src/contexts/ClickedContext.ts +17 -17
- package/src/contexts/DataContext.ts +12 -12
- package/src/contexts/MetricContext.ts +12 -12
- package/src/contexts/OptionsContext.ts +51 -51
- package/src/contexts/SearchContext.ts +19 -19
- package/src/lang-map.d.ts +3 -3
- package/src/metrics.ts +3 -2
- package/src/root.tsx +44 -1
- package/src/routes/{repo.tsx → $repo.tsx} +59 -15
- package/src/routes/index.tsx +156 -46
- package/build/index.js +0 -6836
- package/post-build.js +0 -14
- package/public/favicon.ico +0 -0
- package/src/analyzer/git-caller.ts +0 -117
- package/src/analyzer/index.ts +0 -4
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
2
|
-
import { faRotate } from "@fortawesome/free-solid-svg-icons"
|
|
3
|
-
import { Form, useTransition } from "remix"
|
|
2
|
+
import { faRotate as reanalyzeIcon, faLeftLong as backIcon } from "@fortawesome/free-solid-svg-icons"
|
|
3
|
+
import { Form, Link, useTransition } from "remix"
|
|
4
4
|
import { dateTimeFormatShort } from "~/util"
|
|
5
5
|
import { useData } from "../contexts/DataContext"
|
|
6
6
|
import { usePath } from "../contexts/PathContext"
|
|
7
7
|
import { Spacer } from "./Spacer"
|
|
8
|
-
import { Box, BoxTitle,
|
|
8
|
+
import { Box, BoxTitle, Code, TextButton } from "./util"
|
|
9
|
+
import styled from "styled-components"
|
|
9
10
|
|
|
10
11
|
export function GlobalInfo() {
|
|
11
12
|
const data = useData()
|
|
@@ -31,26 +32,36 @@ export function GlobalInfo() {
|
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<Box>
|
|
35
|
+
<StyledLink to=".." title="See all projects">
|
|
36
|
+
<FontAwesomeIcon icon={backIcon} color="#333" />
|
|
37
|
+
</StyledLink>
|
|
34
38
|
<BoxTitle>{data.repo}</BoxTitle>
|
|
35
39
|
<Spacer />
|
|
36
40
|
<div>
|
|
37
41
|
<strong>Branch: </strong>
|
|
38
|
-
{data.branch}
|
|
42
|
+
<span>{data.branch}</span>
|
|
39
43
|
<Spacer />
|
|
40
44
|
<strong>Analyzed: </strong>
|
|
41
|
-
{dateTimeFormatShort(data.lastRunEpoch)}
|
|
45
|
+
<span>{dateTimeFormatShort(data.lastRunEpoch)}</span>
|
|
42
46
|
<Spacer />
|
|
43
47
|
<strong>As of commit: </strong>
|
|
44
|
-
<
|
|
48
|
+
<Code inline title={data.commit.message ?? "No commit message"}>
|
|
49
|
+
{data.commit.hash.slice(0, 7)}
|
|
50
|
+
</Code>
|
|
45
51
|
</div>
|
|
46
52
|
<Spacer />
|
|
47
|
-
<Form method="post" action="
|
|
53
|
+
<Form method="post" action=".">
|
|
48
54
|
<input type="hidden" name="refresh" value="true" />
|
|
49
55
|
<TextButton disabled={transitionState.state !== "idle"}>
|
|
50
|
-
<FontAwesomeIcon icon={
|
|
56
|
+
<FontAwesomeIcon icon={reanalyzeIcon} />{" "}
|
|
51
57
|
{!transitionState.submission?.formData.has("refresh") ? "Rerun analyzer" : "Analyzing..."}
|
|
52
58
|
</TextButton>
|
|
53
59
|
</Form>
|
|
54
60
|
</Box>
|
|
55
61
|
)
|
|
56
62
|
}
|
|
63
|
+
|
|
64
|
+
const StyledLink = styled(Link)`
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
margin-right: var(--unit);
|
|
67
|
+
`
|
|
@@ -4,7 +4,7 @@ import { useBoolean } from "react-use"
|
|
|
4
4
|
import styled from "styled-components"
|
|
5
5
|
import { useData } from "~/contexts/DataContext"
|
|
6
6
|
import { ExpandUp } from "./Toggle"
|
|
7
|
-
import { Box, BoxSubTitle,
|
|
7
|
+
import { Box, BoxSubTitle, Code } from "./util"
|
|
8
8
|
import { Form, useTransition } from "remix"
|
|
9
9
|
|
|
10
10
|
const Line = styled.div`
|
|
@@ -67,14 +67,14 @@ export function HiddenFiles() {
|
|
|
67
67
|
<div>
|
|
68
68
|
{data.hiddenFiles.map((hidden) => (
|
|
69
69
|
<Line key={hidden} title={hidden}>
|
|
70
|
-
<InlineForm method="post" action="
|
|
70
|
+
<InlineForm method="post" action=".">
|
|
71
71
|
<input type="hidden" name="unignore" value={hidden} />
|
|
72
72
|
<StyledButton title="Show file" disabled={transitionState.state !== "idle"}>
|
|
73
73
|
<StyledFontAwesomeIcon id="eyeslash" icon={faEyeSlash} />
|
|
74
74
|
<StyledFontAwesomeIcon id="eye" icon={faEye} />
|
|
75
75
|
</StyledButton>
|
|
76
76
|
</InlineForm>
|
|
77
|
-
<
|
|
77
|
+
<Code>{hiddenFileFormat(hidden)}</Code>
|
|
78
78
|
</Line>
|
|
79
79
|
))}
|
|
80
80
|
</div>
|
|
@@ -33,7 +33,7 @@ const GradArrow = styled.i<{ visible: boolean; position: number }>`
|
|
|
33
33
|
position: relative;
|
|
34
34
|
bottom: 11px;
|
|
35
35
|
left: calc(${({ position }) => position * 100}% - ${estimatedLetterWidth}px);
|
|
36
|
-
filter: drop-shadow(0px -2px
|
|
36
|
+
filter: drop-shadow(0px -2px 0px #fff);
|
|
37
37
|
`
|
|
38
38
|
|
|
39
39
|
const StyledBox = styled(Box)`
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import styled from "styled-components"
|
|
2
|
-
import { PointInfo } from "../metrics"
|
|
3
|
-
import { Spacer } from "./Spacer"
|
|
4
|
-
import { LegendDot, LegendEntry, LegendLabel } from "./util"
|
|
5
|
-
|
|
6
|
-
interface LegendOtherProps {
|
|
7
|
-
toggle: () => void
|
|
8
|
-
items: [string, PointInfo][]
|
|
9
|
-
show: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const LegendOtherDiv = styled.div`
|
|
13
|
-
width: fit-content;
|
|
14
|
-
&:hover {
|
|
15
|
-
cursor: pointer;
|
|
16
|
-
}
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
export function LegendOther(props: LegendOtherProps) {
|
|
20
|
-
if (!props.show) return null
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<LegendOtherDiv>
|
|
24
|
-
<LegendEntry onClick={props.toggle}>
|
|
25
|
-
{props.items.slice(0, 14).map(([label, info], i) => {
|
|
26
|
-
const margin = i === 0 ? 0 : -10
|
|
27
|
-
return (
|
|
28
|
-
<LegendDot
|
|
29
|
-
key={`dot${label}`}
|
|
30
|
-
dotColor={info.color}
|
|
31
|
-
style={{
|
|
32
|
-
marginLeft: margin,
|
|
33
|
-
}}
|
|
34
|
-
/>
|
|
35
|
-
)
|
|
36
|
-
})}
|
|
37
|
-
<Spacer horizontal />
|
|
38
|
-
<LegendLabel>{props.items.length} more</LegendLabel>
|
|
39
|
-
</LegendEntry>
|
|
40
|
-
</LegendOtherDiv>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
1
|
+
import styled from "styled-components"
|
|
2
|
+
import { PointInfo } from "../metrics"
|
|
3
|
+
import { Spacer } from "./Spacer"
|
|
4
|
+
import { LegendDot, LegendEntry, LegendLabel } from "./util"
|
|
5
|
+
|
|
6
|
+
interface LegendOtherProps {
|
|
7
|
+
toggle: () => void
|
|
8
|
+
items: [string, PointInfo][]
|
|
9
|
+
show: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LegendOtherDiv = styled.div`
|
|
13
|
+
width: fit-content;
|
|
14
|
+
&:hover {
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
}
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
export function LegendOther(props: LegendOtherProps) {
|
|
20
|
+
if (!props.show) return null
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<LegendOtherDiv>
|
|
24
|
+
<LegendEntry onClick={props.toggle}>
|
|
25
|
+
{props.items.slice(0, 14).map(([label, info], i) => {
|
|
26
|
+
const margin = i === 0 ? 0 : -10
|
|
27
|
+
return (
|
|
28
|
+
<LegendDot
|
|
29
|
+
key={`dot${label}`}
|
|
30
|
+
dotColor={info.color}
|
|
31
|
+
style={{
|
|
32
|
+
marginLeft: margin,
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
})}
|
|
37
|
+
<Spacer horizontal />
|
|
38
|
+
<LegendLabel>{props.items.length} more</LegendLabel>
|
|
39
|
+
</LegendEntry>
|
|
40
|
+
</LegendOtherDiv>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/components/Main.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SearchField, Box, Label, StyledP, SearchResultButton, SearchResultSpan } from "./util"
|
|
1
|
+
import { SearchField, Box, Label, StyledP, SearchResultButton, SearchResultSpan, LightFontAwesomeIcon } from "./util"
|
|
2
2
|
import styled from "styled-components"
|
|
3
3
|
import { Fragment, useEffect, useRef, useState } from "react"
|
|
4
4
|
import { useDebounce } from "react-use"
|
|
@@ -10,7 +10,6 @@ import { useData } from "~/contexts/DataContext"
|
|
|
10
10
|
import { usePath } from "~/contexts/PathContext"
|
|
11
11
|
import { useClickedObject } from "~/contexts/ClickedContext"
|
|
12
12
|
import { allExceptLast, getSeparator } from "~/util"
|
|
13
|
-
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
14
13
|
import { faFolderOpen, faFile } from "@fortawesome/free-solid-svg-icons"
|
|
15
14
|
|
|
16
15
|
const StyledBox = styled(Box)`
|
|
@@ -35,10 +34,6 @@ function findSearchResults(tree: HydratedGitTreeObject, searchString: string) {
|
|
|
35
34
|
return searchResults
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
const LightFontAwesomeIcon = styled(FontAwesomeIcon)`
|
|
39
|
-
opacity: 0.5;
|
|
40
|
-
`
|
|
41
|
-
|
|
42
37
|
export default function SearchBar() {
|
|
43
38
|
const searchFieldRef = useRef<HTMLInputElement>(null)
|
|
44
39
|
const { searchText, setSearchText, searchResults, setSearchResults } = useSearch()
|
package/src/components/util.tsx
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import styled from "styled-components"
|
|
1
|
+
import styled, { css } from "styled-components"
|
|
2
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
font-size: 1.5em;
|
|
4
|
+
const titleBaseStyles = css`
|
|
5
5
|
font-weight: bold;
|
|
6
6
|
margin-bottom: 0;
|
|
7
7
|
margin-top: 0;
|
|
8
8
|
color: var(--title-color);
|
|
9
|
-
overflow: hidden;
|
|
10
9
|
text-overflow: ellipsis;
|
|
10
|
+
word-break: keep-all;
|
|
11
11
|
white-space: nowrap;
|
|
12
|
+
overflow-x: hidden;
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
export const BoxTitle = styled.h2`
|
|
16
|
+
${titleBaseStyles}
|
|
17
|
+
font-size: 1.5em;
|
|
12
18
|
`
|
|
13
19
|
|
|
14
20
|
export const BoxSubTitle = styled.h2`
|
|
21
|
+
${titleBaseStyles}
|
|
15
22
|
font-size: 1em;
|
|
16
|
-
font-weight: bold;
|
|
17
|
-
margin-bottom: 0;
|
|
18
|
-
margin-top: 0;
|
|
19
|
-
color: var(--title-color);
|
|
20
23
|
`
|
|
21
24
|
|
|
22
25
|
export const StyledP = styled.p`
|
|
@@ -25,6 +28,10 @@ export const StyledP = styled.p`
|
|
|
25
28
|
margin: 0.5em 0 0.5em 0;
|
|
26
29
|
`
|
|
27
30
|
|
|
31
|
+
export const LightFontAwesomeIcon = styled(FontAwesomeIcon)`
|
|
32
|
+
opacity: 0.5;
|
|
33
|
+
`
|
|
34
|
+
|
|
28
35
|
export const TextButton = styled.button`
|
|
29
36
|
background: var(--button-bg);
|
|
30
37
|
width: fit-content;
|
|
@@ -166,9 +173,11 @@ export const DetailsValue = styled.p`
|
|
|
166
173
|
font-size: 0.9em;
|
|
167
174
|
`
|
|
168
175
|
|
|
169
|
-
export const
|
|
170
|
-
display: inline-block;
|
|
176
|
+
export const Code = styled.code<{ inline?: boolean }>`
|
|
177
|
+
display: ${(props) => (props.inline ? "inline-block" : "block")};
|
|
171
178
|
font-family: monospace;
|
|
179
|
+
font-size: 1.2em;
|
|
180
|
+
white-space: pre;
|
|
172
181
|
`
|
|
173
182
|
|
|
174
183
|
export const Grower = styled.div`
|
package/src/const.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
6
|
-
export const EstimatedLetterHeightForDirText = 14
|
|
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
|
|
6
|
+
export const EstimatedLetterHeightForDirText = 14
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { createContext, Dispatch, SetStateAction, useContext } from "react"
|
|
2
|
-
import { HydratedGitObject } from "../analyzer/model"
|
|
3
|
-
|
|
4
|
-
export interface clickedObject {
|
|
5
|
-
clickedObject: HydratedGitObject | null
|
|
6
|
-
setClickedObject: Dispatch<SetStateAction<HydratedGitObject | null>>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const ClickedObjectContext = createContext<clickedObject | null>(null)
|
|
10
|
-
|
|
11
|
-
export function useClickedObject() {
|
|
12
|
-
const context = useContext(ClickedObjectContext)
|
|
13
|
-
if (!context) {
|
|
14
|
-
throw new Error("useClickedObject must be used within a ClickedObjectProvider")
|
|
15
|
-
}
|
|
16
|
-
return context
|
|
17
|
-
}
|
|
1
|
+
import { createContext, Dispatch, SetStateAction, useContext } from "react"
|
|
2
|
+
import { HydratedGitObject } from "../analyzer/model"
|
|
3
|
+
|
|
4
|
+
export interface clickedObject {
|
|
5
|
+
clickedObject: HydratedGitObject | null
|
|
6
|
+
setClickedObject: Dispatch<SetStateAction<HydratedGitObject | null>>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ClickedObjectContext = createContext<clickedObject | null>(null)
|
|
10
|
+
|
|
11
|
+
export function useClickedObject() {
|
|
12
|
+
const context = useContext(ClickedObjectContext)
|
|
13
|
+
if (!context) {
|
|
14
|
+
throw new Error("useClickedObject must be used within a ClickedObjectProvider")
|
|
15
|
+
}
|
|
16
|
+
return context
|
|
17
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createContext, useContext } from "react"
|
|
2
|
-
import { AnalyzerData } from "~/analyzer/model"
|
|
3
|
-
|
|
4
|
-
export const DataContext = createContext<AnalyzerData | 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
|
-
}
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import { AnalyzerData } from "~/analyzer/model"
|
|
3
|
+
|
|
4
|
+
export const DataContext = createContext<AnalyzerData | 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
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createContext, useContext } from "react"
|
|
2
|
-
import { MetricsData } from "../metrics"
|
|
3
|
-
|
|
4
|
-
export const MetricsContext = createContext<MetricsData | undefined>(undefined)
|
|
5
|
-
|
|
6
|
-
export function useMetrics() {
|
|
7
|
-
const context = useContext(MetricsContext)
|
|
8
|
-
if (!context) {
|
|
9
|
-
throw new Error("useMetrics must be used within a MetricsContext")
|
|
10
|
-
}
|
|
11
|
-
return context
|
|
12
|
-
}
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import { MetricsData } from "../metrics"
|
|
3
|
+
|
|
4
|
+
export const MetricsContext = createContext<MetricsData | undefined>(undefined)
|
|
5
|
+
|
|
6
|
+
export function useMetrics() {
|
|
7
|
+
const context = useContext(MetricsContext)
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error("useMetrics must be used within a MetricsContext")
|
|
10
|
+
}
|
|
11
|
+
return context
|
|
12
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { createContext, useContext } from "react"
|
|
2
|
-
import { Authorship, AuthorshipType, Metric, MetricType } from "../metrics"
|
|
3
|
-
|
|
4
|
-
export const Chart = {
|
|
5
|
-
BUBBLE_CHART: "Bubble chart",
|
|
6
|
-
TREE_MAP: "Tree map",
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type ChartType = keyof typeof Chart
|
|
10
|
-
|
|
11
|
-
export interface Options {
|
|
12
|
-
metricType: MetricType
|
|
13
|
-
chartType: ChartType
|
|
14
|
-
authorshipType: AuthorshipType
|
|
15
|
-
animationsEnabled: boolean
|
|
16
|
-
setMetricType: (metricType: MetricType) => void
|
|
17
|
-
setChartType: (chartType: ChartType) => void
|
|
18
|
-
setAuthorshipType: (authorshipType: AuthorshipType) => void
|
|
19
|
-
setAnimationsEnabled: (animationsEnabled: boolean) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const OptionsContext = createContext<Options | undefined>(undefined)
|
|
23
|
-
|
|
24
|
-
export function useOptions() {
|
|
25
|
-
const context = useContext(OptionsContext)
|
|
26
|
-
if (!context) {
|
|
27
|
-
throw new Error("useSearch must be used within a SearchProvider")
|
|
28
|
-
}
|
|
29
|
-
return context
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getDefaultOptions(): Options {
|
|
33
|
-
return {
|
|
34
|
-
metricType: Object.keys(Metric)[0] as MetricType,
|
|
35
|
-
chartType: Object.keys(Chart)[0] as ChartType,
|
|
36
|
-
authorshipType: Object.keys(Authorship)[0] as AuthorshipType,
|
|
37
|
-
animationsEnabled: true,
|
|
38
|
-
setChartType: () => {
|
|
39
|
-
throw new Error("No chartTypeSetter provided")
|
|
40
|
-
},
|
|
41
|
-
setMetricType: () => {
|
|
42
|
-
throw new Error("No metricTypeSetter provided")
|
|
43
|
-
},
|
|
44
|
-
setAuthorshipType: () => {
|
|
45
|
-
throw new Error("No AuthorshipTypeSetter provided")
|
|
46
|
-
},
|
|
47
|
-
setAnimationsEnabled: () => {
|
|
48
|
-
throw new Error("No animationsEnabledSetter provided")
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import { Authorship, AuthorshipType, Metric, MetricType } from "../metrics"
|
|
3
|
+
|
|
4
|
+
export const Chart = {
|
|
5
|
+
BUBBLE_CHART: "Bubble chart",
|
|
6
|
+
TREE_MAP: "Tree map",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ChartType = keyof typeof Chart
|
|
10
|
+
|
|
11
|
+
export interface Options {
|
|
12
|
+
metricType: MetricType
|
|
13
|
+
chartType: ChartType
|
|
14
|
+
authorshipType: AuthorshipType
|
|
15
|
+
animationsEnabled: boolean
|
|
16
|
+
setMetricType: (metricType: MetricType) => void
|
|
17
|
+
setChartType: (chartType: ChartType) => void
|
|
18
|
+
setAuthorshipType: (authorshipType: AuthorshipType) => void
|
|
19
|
+
setAnimationsEnabled: (animationsEnabled: boolean) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const OptionsContext = createContext<Options | undefined>(undefined)
|
|
23
|
+
|
|
24
|
+
export function useOptions() {
|
|
25
|
+
const context = useContext(OptionsContext)
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error("useSearch must be used within a SearchProvider")
|
|
28
|
+
}
|
|
29
|
+
return context
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getDefaultOptions(): Options {
|
|
33
|
+
return {
|
|
34
|
+
metricType: Object.keys(Metric)[0] as MetricType,
|
|
35
|
+
chartType: Object.keys(Chart)[0] as ChartType,
|
|
36
|
+
authorshipType: Object.keys(Authorship)[0] as AuthorshipType,
|
|
37
|
+
animationsEnabled: true,
|
|
38
|
+
setChartType: () => {
|
|
39
|
+
throw new Error("No chartTypeSetter provided")
|
|
40
|
+
},
|
|
41
|
+
setMetricType: () => {
|
|
42
|
+
throw new Error("No metricTypeSetter provided")
|
|
43
|
+
},
|
|
44
|
+
setAuthorshipType: () => {
|
|
45
|
+
throw new Error("No AuthorshipTypeSetter provided")
|
|
46
|
+
},
|
|
47
|
+
setAnimationsEnabled: () => {
|
|
48
|
+
throw new Error("No animationsEnabledSetter provided")
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { createContext, Dispatch, SetStateAction, useContext } from "react"
|
|
2
|
-
import { HydratedGitObject } from "~/analyzer/model"
|
|
3
|
-
|
|
4
|
-
type Search = {
|
|
5
|
-
searchText: string
|
|
6
|
-
setSearchText: Dispatch<SetStateAction<string>>
|
|
7
|
-
searchResults: HydratedGitObject[]
|
|
8
|
-
setSearchResults: Dispatch<SetStateAction<HydratedGitObject[]>>
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const SearchContext = createContext<Search | undefined>(undefined)
|
|
12
|
-
|
|
13
|
-
export function useSearch(): Search {
|
|
14
|
-
const context = useContext(SearchContext)
|
|
15
|
-
if (!context) {
|
|
16
|
-
throw new Error("useSearch must be used within a SearchProvider")
|
|
17
|
-
}
|
|
18
|
-
return context
|
|
19
|
-
}
|
|
1
|
+
import { createContext, Dispatch, SetStateAction, useContext } from "react"
|
|
2
|
+
import { HydratedGitObject } from "~/analyzer/model"
|
|
3
|
+
|
|
4
|
+
type Search = {
|
|
5
|
+
searchText: string
|
|
6
|
+
setSearchText: Dispatch<SetStateAction<string>>
|
|
7
|
+
searchResults: HydratedGitObject[]
|
|
8
|
+
setSearchResults: Dispatch<SetStateAction<HydratedGitObject[]>>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const SearchContext = createContext<Search | undefined>(undefined)
|
|
12
|
+
|
|
13
|
+
export function useSearch(): Search {
|
|
14
|
+
const context = useContext(SearchContext)
|
|
15
|
+
if (!context) {
|
|
16
|
+
throw new Error("useSearch must be used within a SearchProvider")
|
|
17
|
+
}
|
|
18
|
+
return context
|
|
19
|
+
}
|
package/src/lang-map.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare module "lang-map" {
|
|
2
|
-
export function languages(extension: string): string[]
|
|
3
|
-
}
|
|
1
|
+
declare module "lang-map" {
|
|
2
|
+
export function languages(extension: string): string[]
|
|
3
|
+
}
|
package/src/metrics.ts
CHANGED
|
@@ -276,8 +276,10 @@ function setDominanceColor(blob: HydratedGitBlobObject, cache: MetricCache, auth
|
|
|
276
276
|
const defaultColor = "hsl(210, 38%, 85%)"
|
|
277
277
|
const nocreditColor = "teal"
|
|
278
278
|
|
|
279
|
+
const authorUnion = blob.unionedAuthors?.get(authorshipType) ?? {}
|
|
280
|
+
|
|
279
281
|
let creditsum = 0
|
|
280
|
-
for (const [, val] of Object.entries(
|
|
282
|
+
for (const [, val] of Object.entries(authorUnion)) {
|
|
281
283
|
creditsum += val
|
|
282
284
|
}
|
|
283
285
|
|
|
@@ -289,7 +291,6 @@ function setDominanceColor(blob: HydratedGitBlobObject, cache: MetricCache, auth
|
|
|
289
291
|
return
|
|
290
292
|
}
|
|
291
293
|
|
|
292
|
-
const authorUnion = blob.unionedAuthors?.get(authorshipType)
|
|
293
294
|
|
|
294
295
|
if (!authorUnion) throw Error("No unioned authors found")
|
|
295
296
|
switch (Object.keys(authorUnion).length) {
|
package/src/root.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "remix"
|
|
1
|
+
import { ErrorBoundaryComponent, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useCatch } from "remix"
|
|
2
2
|
import type { MetaFunction } from "remix"
|
|
3
3
|
|
|
4
4
|
import appStyles from "~/styles/App.css"
|
|
5
5
|
import varsStyles from "~/styles/vars.css"
|
|
6
6
|
import indexStyles from "~/styles/index.css"
|
|
7
7
|
import chartStyles from "~/styles/Chart.css"
|
|
8
|
+
import { useEffect } from "react"
|
|
9
|
+
import { Code } from "./components/util"
|
|
8
10
|
|
|
9
11
|
export const meta: MetaFunction = () => {
|
|
10
12
|
return { title: "Git Truck 🚛" }
|
|
@@ -36,3 +38,44 @@ export default function App() {
|
|
|
36
38
|
</html>
|
|
37
39
|
)
|
|
38
40
|
}
|
|
41
|
+
|
|
42
|
+
export function CatchBoundary() {
|
|
43
|
+
const caught = useCatch()
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<html>
|
|
47
|
+
<head>
|
|
48
|
+
<title>Oops! An error wasn't handled</title>
|
|
49
|
+
<Meta />
|
|
50
|
+
<Links />
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<h1>
|
|
54
|
+
{caught.status} {caught.statusText}
|
|
55
|
+
</h1>
|
|
56
|
+
<Scripts />
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
console.error(error.message)
|
|
65
|
+
}, [error])
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<html>
|
|
69
|
+
<head>
|
|
70
|
+
<title>Oops! An error wasn't handled</title>
|
|
71
|
+
<Meta />
|
|
72
|
+
<Links />
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<h1>{error.message}</h1>
|
|
76
|
+
<Code>{error.stack}</Code>
|
|
77
|
+
<Scripts />
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
)
|
|
81
|
+
}
|