diff --git a/README.md b/README.md index 27a0db6f..49e0b680 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ This strategy can be used by rendering the `` subcompone # Git Log Data -The array of `GitLogEntry` objects is the source of data used by the `GitLog` component. It has the following properties: +The array of `GitLogEntry` objects is the source of data used by the core `GitLog` components. It has the following properties: | Property | Type | Description | |-----------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------| @@ -211,10 +211,38 @@ The array of `GitLogEntry` objects is the source of data used by the `GitLog` co | `branch` | `string` | The name of the branch this commit belongs to. | | `parents` | `string[]` | An array of parent commit hashes. If this is a merge commit, it will have multiple parents. If it's an initial commit, it will have none. | | `message` | `string` | The commit message describing the changes made in this commit. | -| `author` | `CommitAuthor?` | Details of the user who authored the commit. | | `committerDate` | `string` | The date and time when the commit was applied by the committer. Typically the timestamp when the commit was finalized. | +| `author` | `CommitAuthor?` | *(Optional)* Details of the user who authored the commit. | | `authorDate` | `string?` | *(Optional)* The date and time when the commit was originally authored. May differ from `committerDate` if the commit was rebased or amended. | +You can pass a generic type to the `GitLog` or `GitLogPaged` components to augment the `GitLogEntry` data so that any `Commit` instances surfaced by callback functions will have your custom metadata passed back to you for convenience. For example: + +```typescript jsx +import { GitLog } from "@tomplum/react-git-log" + +interface MyCustomCommit { + myCustomField: string +} + +const YourConsumer = () => { + const { entries, currentBranch } = useYourDataSource() + + return ( + { + console.log(commit.myCustomField) + }} + > + + + + + ) +} +``` + > [!TIP] > Usually you'd be sourcing this data from a backend service like a web-api, but you can extract it from the command line with the following command. diff --git a/packages/library/README.md b/packages/library/README.md index 917ad386..0ff6f733 100644 --- a/packages/library/README.md +++ b/packages/library/README.md @@ -120,7 +120,7 @@ A flexible and interactive React component for visualising Git commit history. D # Git Log Data -The array of `GitLogEntry` objects is the source of data used by the `GitLog` component. It has the following properties: +The array of `GitLogEntry` objects is the source of data used by the core `GitLog` components. It has the following properties: | Property | Type | Description | |-----------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------| @@ -128,10 +128,38 @@ The array of `GitLogEntry` objects is the source of data used by the `GitLog` co | `branch` | `string` | The name of the branch this commit belongs to. | | `parents` | `string[]` | An array of parent commit hashes. If this is a merge commit, it will have multiple parents. If it's an initial commit, it will have none. | | `message` | `string` | The commit message describing the changes made in this commit. | -| `author` | `CommitAuthor?` | Details of the user who authored the commit. | | `committerDate` | `string` | The date and time when the commit was applied by the committer. Typically the timestamp when the commit was finalized. | +| `author` | `CommitAuthor?` | *(Optional)* Details of the user who authored the commit. | | `authorDate` | `string?` | *(Optional)* The date and time when the commit was originally authored. May differ from `committerDate` if the commit was rebased or amended. | +You can pass a generic type to the `GitLog` or `GitLogPaged` components to augment the `GitLogEntry` data so that any `Commit` instances surfaced by callback functions will have your custom metadata passed back to you for convenience. For example: + +```typescript jsx +import { GitLog } from "@tomplum/react-git-log" + +interface MyCustomCommit { + myCustomField: string +} + +const YourConsumer = () => { + const { entries, currentBranch } = useYourDataSource() + + return ( + { + console.log(commit.myCustomField) + }} + > + + + + + ) +} +``` + > [!TIP] > Usually you'd be sourcing this data from a backend service like a web-api, but you can extract it from the command line with the following command. diff --git a/packages/library/src/GitLog.integration.spec.tsx b/packages/library/src/GitLog.integration.spec.tsx index 53346413..7bf28b09 100644 --- a/packages/library/src/GitLog.integration.spec.tsx +++ b/packages/library/src/GitLog.integration.spec.tsx @@ -202,7 +202,7 @@ describe.skip('GitLog Integration', () => { if (columnState.isNode) { const missingNodeMsg = `Expected commit node element in row ${rowIndex}, column ${columnIndex} with hash ${commit.hash}, but it was not found in the graph` const commitNodeTestId = graphColumn.commitNodeId({ hash: commit.hash }) - expect(insideCurrentColumn.getByTestId(commitNodeTestId), missingNodeMsg) + expect(insideCurrentColumn.getByTestId(commitNodeTestId), missingNodeMsg).toBeInTheDocument() debugMetrics['commit-nodes'] = (debugMetrics['commit-nodes'] ?? 0) + 1 // If the commit is a merge commit @@ -238,7 +238,6 @@ describe.skip('GitLog Integration', () => { const isHeadCommit = commit.hash === headCommit?.hash if (!headCommit && (columnState.isColumnAboveEmpty || commit.isBranchTip)) { - console.log(commit.hash, columnState) expect(insideCurrentColumn.getByTestId(graphColumn.bottomHalfVerticalLineId)).toBeInTheDocument() } diff --git a/packages/library/src/GitLog.spec.tsx b/packages/library/src/GitLog.spec.tsx index 7d886d05..44d2832f 100644 --- a/packages/library/src/GitLog.spec.tsx +++ b/packages/library/src/GitLog.spec.tsx @@ -12,6 +12,7 @@ import { act } from 'react' import { Commit } from 'types/Commit' import { table } from 'test/elements/Table' import { createCanvas } from 'canvas' +import { GitLogEntry } from 'types/GitLogEntry' const today = Date.UTC(2025, 2, 24, 18, 0, 0) @@ -22,7 +23,7 @@ const urlBuilderFunction: GitLogUrlBuilder = ({ commit }) => ({ const sleepRepositoryLogEntries = parseGitLogOutput(sleepRepositoryData) -const getSleepRepositoriesLogEntries = (quantity: number) => { +const getSleepRepositoriesLogEntries = (quantity: number): GitLogEntry[] => { const entries = sleepRepositoryLogEntries.slice(0, quantity + 1) entries[quantity - 1].parents = [] return entries @@ -345,6 +346,56 @@ describe('GitLog', () => { }) }) + it('should call onSelectCommit with custom data passed into the log entries', () => { + const handleSelectCommit = vi.fn() + + interface CustomCommit { + customField: string, + moreMetaData: number[] + } + + render( + + showGitIndex + currentBranch='release' + onSelectCommit={handleSelectCommit} + entries={getSleepRepositoriesLogEntries(6).map(entry => ({ + ...entry, + customField: 'testing', + moreMetaData: [678] + }))} + > + + + + ) + + expect(handleSelectCommit).not.toHaveBeenCalled() + + act(() => { + graphColumn.at({ row: 1, column: 0 })?.click() + }) + + expect(handleSelectCommit).toHaveBeenCalledExactlyOnceWith[]>({ + authorDate: '2025-03-24 17:03:58 +0000', + branch: 'refs/remotes/origin/renovate/all-minor-patch', + children: [], + committerDate: '2025-03-24 17:03:58 +0000', + author: { + email: '29139614+renovate[bot]@users.noreply.github.com', + name: 'renovate[bot]', + }, + hash: '2079fb6', + isBranchTip: true, + message: 'fix(deps): update all non-major dependencies', + parents: [ + '1352f4c', + ], + customField: 'testing', + moreMetaData: [678] + }) + }) + it.each([0, 1, 2])('should call onSelectCommit with the commit details when clicking on column index [%s] in a commits row', (columnIndex: number) => { const handleSelectCommit = vi.fn() @@ -526,6 +577,53 @@ describe('GitLog', () => { }) }) + it('should call onPreviewCommit with the custom data passed into the log', () => { + const handlePreviewCommit = vi.fn() + + interface CustomCommit { + test: string + anotherTest: number + } + + render( + + currentBranch='release' + onPreviewCommit={handlePreviewCommit} + entries={getSleepRepositoriesLogEntries(3).map(entry => ({ + ...entry, + test: 'hello', + anotherTest: 5 + }))} + > + + + + ) + + expect(handlePreviewCommit).not.toHaveBeenCalled() + + act(() => { + fireEvent.mouseOver(graphColumn.at({ row: 1, column: 0 })) + }) + + expect(handlePreviewCommit).toHaveBeenCalledExactlyOnceWith[]>({ + authorDate: '2025-03-24 17:03:58 +0000', + branch: 'refs/remotes/origin/renovate/all-minor-patch', + author: { + name: 'renovate[bot]', + email: '29139614+renovate[bot]@users.noreply.github.com', + }, + children: [], + committerDate: '2025-03-24 17:03:58 +0000', + hash: '2079fb6', + isBranchTip: true, + message: 'fix(deps): update all non-major dependencies', + parents: ['1352f4c'], + test: 'hello', + anotherTest: 5 + }) + }) + it('should call onPreviewCommit with the commit details when mousing over on one of the table columns of the index pseudo-commit row', () => { const handlePreviewCommit = vi.fn() diff --git a/packages/library/src/GitLog.tsx b/packages/library/src/GitLog.tsx index e5f93b2e..19294e5d 100644 --- a/packages/library/src/GitLog.tsx +++ b/packages/library/src/GitLog.tsx @@ -5,12 +5,9 @@ import { GraphCanvas2D, GraphHTMLGrid } from './modules/Graph' import { Table } from './modules/Table' import { GitLogCore } from './components/GitLogCore' -export const GitLog = ({ children, ...props }: PropsWithChildren) => { +export const GitLog = ({ children, ...props }: PropsWithChildren>) => { return ( - + {...props} componentName="GitLog"> {children} ) diff --git a/packages/library/src/GitLogPaged.spec.tsx b/packages/library/src/GitLogPaged.spec.tsx index 9900727a..a58ba0de 100644 --- a/packages/library/src/GitLogPaged.spec.tsx +++ b/packages/library/src/GitLogPaged.spec.tsx @@ -1,9 +1,35 @@ import { parseGitLogOutput } from 'test/data/gitLogParser' -import { render } from '@testing-library/react' +import { fireEvent, render } from '@testing-library/react' import sleepRepositoryDataReleaseBranch from 'test/data/sleep-paginated/sleep-release-branch.txt?raw' import { GitLogPaged } from './GitLogPaged' +import { afterEach, beforeEach, describe } from 'vitest' +import { act } from 'react' +import { graphColumn } from 'test/elements/GraphColumn' +import { Commit } from 'types/Commit' +import { table } from 'test/elements/Table' +import { GitLogEntry } from 'types/GitLogEntry' + +const today = Date.UTC(2025, 2, 24, 18, 0, 0) + +const sleepRepositoryLogEntries = parseGitLogOutput(sleepRepositoryDataReleaseBranch) +const headCommitHash = 'e059c28' + +const getSleepRepositoriesLogEntries = (quantity: number): GitLogEntry[] => { + const entries = sleepRepositoryLogEntries.slice(0, quantity + 1) + entries[quantity - 1].parents = [] + return entries +} describe('GitLogPaged', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(today) + }) + + afterEach(() => { + vi.useRealTimers() + }) + const paginationTests = [ { start: 0, end: 20 }, { start: 20, end: 40 }, @@ -72,7 +98,7 @@ describe('GitLogPaged', () => { @@ -91,7 +117,7 @@ describe('GitLogPaged', () => { @@ -110,7 +136,7 @@ describe('GitLogPaged', () => { @@ -129,7 +155,7 @@ describe('GitLogPaged', () => { @@ -148,7 +174,7 @@ describe('GitLogPaged', () => { @@ -160,4 +186,402 @@ describe('GitLogPaged', () => { ' can only have one or child.' ) }) + + describe('onSelectCommit Callback', () => { + it.each([0, 1, 2])('should call onSelectCommit with the commit details when clicking on column index [%s] in the index pseudo-commits row', (columnIndex: number) => { + const handleSelectCommit = vi.fn() + + render( + + + + + ) + + expect(handleSelectCommit).not.toHaveBeenCalled() + + act(() => { + graphColumn.at({ row: 0, column: columnIndex })?.click() + }) + + expect(handleSelectCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-03-24T18:00:00.000Z', + branch: 'release', + children: [], + committerDate: '2025-03-24T18:00:00.000Z', + hash: 'index', + isBranchTip: false, + message: '// WIP', + parents: [ + 'e059c28', + ] + }) + }) + + it('should call onSelectCommit with custom data passed into the log entries', () => { + const handleSelectCommit = vi.fn() + + interface CustomCommit { + customField: string, + moreMetaData: number[] + } + + render( + + showGitIndex + branchName='release' + headCommitHash={headCommitHash} + onSelectCommit={handleSelectCommit} + entries={getSleepRepositoriesLogEntries(6).map(entry => ({ + ...entry, + customField: 'testing', + moreMetaData: [678] + }))} + > + + + + ) + + expect(handleSelectCommit).not.toHaveBeenCalled() + + act(() => { + graphColumn.at({ row: 1, column: 0 })?.click() + }) + + expect(handleSelectCommit).toHaveBeenCalledExactlyOnceWith[]>({ + authorDate: '2025-02-25 17:08:06 +0000', + branch: 'release', + children: [], + committerDate: '2025-02-25 17:08:06 +0000', + hash: 'e059c28', + isBranchTip: true, + author: { + email: 'Thomas.Plumpton@hotmail.co.uk', + name: 'Thomas Plumpton', + }, + message: 'Merge pull request #39 from TomPlum/renovate/vite-6.x', + parents: [ + '0b78e07', + '867c511', + ], + customField: 'testing', + moreMetaData: [678] + }) + }) + + it.each([0, 1])('should call onSelectCommit with the commit details when clicking on column index [%s] in a commits row', (columnIndex: number) => { + const handleSelectCommit = vi.fn() + + render( + + + + + + ) + + expect(handleSelectCommit).not.toHaveBeenCalled() + + act(() => { + graphColumn.at({ row: 1, column: columnIndex })?.click() + }) + + expect(handleSelectCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-02-25 17:08:06 +0000', + branch: 'release', + author: { + name: 'Thomas Plumpton', + email: 'Thomas.Plumpton@hotmail.co.uk', + }, + children: [], + committerDate: '2025-02-25 17:08:06 +0000', + hash: 'e059c28', + isBranchTip: true, + message: 'Merge pull request #39 from TomPlum/renovate/vite-6.x', + parents: ['0b78e07', '867c511'] + }) + }) + + it('should call onSelectCommit with the commit details when clicking on one of the table columns of the index pseudo-commit row', () => { + const handleSelectCommit = vi.fn() + + render( + + + + + ) + + expect(handleSelectCommit).not.toHaveBeenCalled() + + act(() => { + table.row({ row: 0 })?.click() + }) + + expect(handleSelectCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-03-24T18:00:00.000Z', + branch: 'release', + children: [], + committerDate: '2025-03-24T18:00:00.000Z', + hash: 'index', + isBranchTip: false, + message: '// WIP', + parents: [ + 'e059c28', + ] + }) + }) + + it('should call onSelectCommit with the commit details when clicking on one of the table columns', () => { + const handleSelectCommit = vi.fn() + + render( + + + + + + ) + + expect(handleSelectCommit).not.toHaveBeenCalled() + + act(() => { + table.row({ row: 1 })?.click() + }) + + expect(handleSelectCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-02-25 17:08:06 +0000', + branch: 'release', + author: { + email: 'Thomas.Plumpton@hotmail.co.uk', + name: 'Thomas Plumpton', + }, + children: [], + committerDate: '2025-02-25 17:08:06 +0000', + hash: 'e059c28', + isBranchTip: true, + message: 'Merge pull request #39 from TomPlum/renovate/vite-6.x', + parents: ['0b78e07', '867c511'] + }) + }) + }) + + describe('onPreviewCommit Callback', () => { + it.each([0, 1, 2])('should call onPreviewCommit with the commit details when mousing over column index [%s] in the index pseudo-commits row', (columnIndex: number) => { + const handlePreviewCommit = vi.fn() + + render( + + + + + ) + + expect(handlePreviewCommit).not.toHaveBeenCalled() + + fireEvent.mouseOver(graphColumn.at({ row: 0, column: columnIndex })) + + expect(handlePreviewCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-03-24T18:00:00.000Z', + branch: 'release', + children: [], + committerDate: '2025-03-24T18:00:00.000Z', + hash: 'index', + isBranchTip: false, + message: '// WIP', + parents: [ + 'e059c28', + ] + }) + }) + + it.each([0, 1])('should call onPreviewCommit with the commit details when mousing over column index [%s] in a commits row', (columnIndex: number) => { + const handlePreviewCommit = vi.fn() + + render( + + + + + + ) + + expect(handlePreviewCommit).not.toHaveBeenCalled() + + act(() => { + fireEvent.mouseOver(graphColumn.at({ row: 1, column: columnIndex })) + }) + + expect(handlePreviewCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-02-25 17:08:06 +0000', + branch: 'release', + author: { + email: 'Thomas.Plumpton@hotmail.co.uk', + name: 'Thomas Plumpton' + }, + children: [], + committerDate: '2025-02-25 17:08:06 +0000', + hash: 'e059c28', + isBranchTip: true, + message: 'Merge pull request #39 from TomPlum/renovate/vite-6.x', + parents: ['0b78e07', '867c511'] + }) + }) + + it('should call onPreviewCommit with the custom data passed into the log', () => { + const handlePreviewCommit = vi.fn() + + interface CustomCommit { + test: string + anotherTest: number + } + + render( + + branchName='release' + headCommitHash={headCommitHash} + onPreviewCommit={handlePreviewCommit} + entries={getSleepRepositoriesLogEntries(3).map(entry => ({ + ...entry, + test: 'hello', + anotherTest: 5 + }))} + > + + + + ) + + expect(handlePreviewCommit).not.toHaveBeenCalled() + + act(() => { + fireEvent.mouseOver(graphColumn.at({ row: 1, column: 0 })) + }) + + expect(handlePreviewCommit).toHaveBeenCalledExactlyOnceWith[]>({ + authorDate: '2025-02-25 17:08:06 +0000', + branch: 'release', + author: { + email: 'Thomas.Plumpton@hotmail.co.uk', + name: 'Thomas Plumpton', + }, + children: [], + committerDate: '2025-02-25 17:08:06 +0000', + hash: 'e059c28', + isBranchTip: true, + message: 'Merge pull request #39 from TomPlum/renovate/vite-6.x', + parents: ['0b78e07', '867c511'], + test: 'hello', + anotherTest: 5 + }) + }) + + it('should call onPreviewCommit with the commit details when mousing over on one of the table columns of the index pseudo-commit row', () => { + const handlePreviewCommit = vi.fn() + + render( + + + + + ) + + expect(handlePreviewCommit).not.toHaveBeenCalled() + + act(() => { + fireEvent.mouseOver(table.row({ row: 0 })) + }) + + expect(handlePreviewCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-03-24T18:00:00.000Z', + branch: 'release', + children: [], + committerDate: '2025-03-24T18:00:00.000Z', + hash: 'index', + isBranchTip: false, + message: '// WIP', + parents: [ + 'e059c28', + ] + }) + }) + + it('should call onPreviewCommit with the commit details when mousing over on one of the table columns', () => { + const handlePreviewCommit = vi.fn() + + render( + + + + + + ) + + expect(handlePreviewCommit).not.toHaveBeenCalled() + + act(() => { + fireEvent.mouseOver(table.row({ row: 1 })) + }) + + expect(handlePreviewCommit).toHaveBeenCalledExactlyOnceWith({ + authorDate: '2025-02-25 17:08:06 +0000', + branch: 'release', + author: { + email: 'Thomas.Plumpton@hotmail.co.uk', + name: 'Thomas Plumpton', + }, + children: [], + committerDate: '2025-02-25 17:08:06 +0000', + hash: 'e059c28', + isBranchTip: true, + message: 'Merge pull request #39 from TomPlum/renovate/vite-6.x', + parents: ['0b78e07', '867c511'], + }) + }) + }) }) \ No newline at end of file diff --git a/packages/library/src/GitLogPaged.tsx b/packages/library/src/GitLogPaged.tsx index 376bddff..88b62ee3 100644 --- a/packages/library/src/GitLogPaged.tsx +++ b/packages/library/src/GitLogPaged.tsx @@ -5,9 +5,9 @@ import { GraphCanvas2D, GraphHTMLGrid } from './modules/Graph' import { Table } from './modules/Table' import { GitLogCore } from './components/GitLogCore' -export const GitLogPaged = ({ children, branchName, ...props }: PropsWithChildren) => { +export const GitLogPaged = ({ children, branchName, ...props }: PropsWithChildren>) => { return ( - {...props} isServerSidePaginated currentBranch={branchName} diff --git a/packages/library/src/_test/stubs.ts b/packages/library/src/_test/stubs.ts index a823dba9..9411a179 100644 --- a/packages/library/src/_test/stubs.ts +++ b/packages/library/src/_test/stubs.ts @@ -8,7 +8,7 @@ import { GraphColumnState } from 'modules/Graph/strategies/Grid/components/Graph import { GraphContextBag } from 'modules/Graph/context' import { ThemeContextBag } from 'context/ThemeContext' -export const commit = (commit?: Partial): Commit => ({ +export const commit = (commit?: Partial>): Commit => ({ hash: 'aa2c148', committerDate: '2025-02-24T22:06:22+00:00', authorDate: '2025-02-22 22:06:22 +0000', @@ -22,7 +22,7 @@ export const commit = (commit?: Partial): Commit => ({ ], isBranchTip: false, ...commit -}) +}) as Commit export const entry = (entry?: Partial): GitLogEntry => ({ hash: 'aa2c148', diff --git a/packages/library/src/components/GitLogCore/GitLogCore.tsx b/packages/library/src/components/GitLogCore/GitLogCore.tsx index ada55ed3..930cd48c 100644 --- a/packages/library/src/components/GitLogCore/GitLogCore.tsx +++ b/packages/library/src/components/GitLogCore/GitLogCore.tsx @@ -15,7 +15,7 @@ import { useCoreComponents } from 'components/GitLogCore/useCoreComponents' dayjs.extend(utc) -export const GitLogCore = ({ +export const GitLogCore = ({ children, entries, showHeaders = false, @@ -36,13 +36,13 @@ export const GitLogCore = ({ showGitIndex = true, enableSelectedCommitStyling = true, enablePreviewedCommitStyling = true -}: PropsWithChildren) => { +}: PropsWithChildren>) => { const { tags, graph, table } = useCoreComponents({ children, componentName }) - const graphData = useMemo(() => { + const graphData = useMemo>(() => { const { children, parents, hashToCommit } = computeRelationships(entries, headCommitHash) const sortedCommits = temporalTopologicalSort([...hashToCommit.values()], children, hashToCommit) const { graphWidth, positions, edges } = computeNodePositions(sortedCommits, currentBranch, children, parents) @@ -60,25 +60,25 @@ export const GitLogCore = ({ const [nodeSize, setNodeSize] = useState(DEFAULT_NODE_SIZE) const [graphOrientation, setGraphOrientation] = useState('normal') - const [selectedCommit, setSelectedCommit] = useState() - const [previewedCommit, setPreviewedCommit] = useState() + const [selectedCommit, setSelectedCommit] = useState>() + const [previewedCommit, setPreviewedCommit] = useState>() const smallestAvailableGraphWidth = graphData.graphWidth * (nodeSize + (NODE_BORDER_WIDTH * 2)) // TODO: Are we using graphWidth here or just ditching enableResize? const [, setGraphWidth] = useState(defaultGraphWidth ?? smallestAvailableGraphWidth) - const handleSelectCommit = useCallback((commit?: Commit) => { + const handleSelectCommit = useCallback((commit?: Commit) => { setSelectedCommit(commit) onSelectCommit?.(commit) }, [onSelectCommit]) - const handlePreviewCommit = useCallback((commit?: Commit) => { + const handlePreviewCommit = useCallback((commit?: Commit) => { setPreviewedCommit(commit) onPreviewCommit?.(commit) }, [onPreviewCommit]) - const headCommit = useMemo(() => { + const headCommit = useMemo | undefined>(() => { if (isServerSidePaginated) { return graphData.commits.find(it => it.hash === headCommitHash) } @@ -140,7 +140,7 @@ export const GitLogCore = ({ }, [defaultGraphWidth, smallestAvailableGraphWidth]) - const value = useMemo(() => ({ + const value = useMemo>(() => ({ showTable: Boolean(table), showBranchesTags: Boolean(tags), classes, @@ -197,7 +197,7 @@ export const GitLogCore = ({ ]) return ( - + diff --git a/packages/library/src/components/GitLogCore/types.ts b/packages/library/src/components/GitLogCore/types.ts index 7f44833e..ef58c1f1 100644 --- a/packages/library/src/components/GitLogCore/types.ts +++ b/packages/library/src/components/GitLogCore/types.ts @@ -1,6 +1,6 @@ import { GitLogPagedProps, GitLogProps } from '../../types' -export interface GitLogCoreProps extends GitLogProps, Omit { +export interface GitLogCoreProps extends GitLogProps, Omit, 'headCommitHash' | 'branchName'> { componentName: string headCommitHash?: string isServerSidePaginated?: boolean diff --git a/packages/library/src/context/GitContext/types.ts b/packages/library/src/context/GitContext/types.ts index ffde8534..3400d59a 100644 --- a/packages/library/src/context/GitContext/types.ts +++ b/packages/library/src/context/GitContext/types.ts @@ -3,7 +3,7 @@ import { GraphData } from 'data' import { GitLogIndexStatus, GitLogStylingProps, GitLogUrlBuilder } from '../../types' import { GraphOrientation } from 'modules/Graph' -export interface GitContextBag { +export interface GitContextBag { /** * The name of the branch that is * currently checked out. @@ -19,7 +19,7 @@ export interface GitContextBag { * commit (probably due to server-side * pagination being used) */ - headCommit?: Commit + headCommit?: Commit /** * The SHA1 hash of the HEAD commit of @@ -27,7 +27,7 @@ export interface GitContextBag { * out in the repository. * * Only needs to be passed in if you are - * passing in a sub-set of the Git log + * passing in a subset of the Git log * {@link entries} due to managing your * own pagination. * @@ -50,7 +50,7 @@ export interface GitContextBag { * The currently selected commit that * is highlighted in the log. */ - selectedCommit?: Commit + selectedCommit?: Commit /** * Sets the selected commit. Can be @@ -58,7 +58,7 @@ export interface GitContextBag { * * @param commit Details of the selected commit. */ - setSelectedCommit: (commit?: Commit) => void + setSelectedCommit: (commit?: Commit) => void /** * The currently previewed commit that @@ -66,7 +66,7 @@ export interface GitContextBag { * while the user is hovering their cursor * over it. */ - previewedCommit?: Commit + previewedCommit?: Commit /** * Sets the previewed commit. Can be @@ -74,7 +74,7 @@ export interface GitContextBag { * * @param commit Details of the selected commit. */ - setPreviewedCommit: (commit?: Commit) => void + setPreviewedCommit: (commit?: Commit) => void /** * Enables the row styling across the log @@ -182,7 +182,7 @@ export interface GitContextBag { * components such as the graph, table * and tag/branch labels. */ - graphData: GraphData + graphData: GraphData /** * CSS Classes to pass to various underlying diff --git a/packages/library/src/data/computeNodeColumns.ts b/packages/library/src/data/computeNodeColumns.ts index 7b453d04..dc11fbb7 100644 --- a/packages/library/src/data/computeNodeColumns.ts +++ b/packages/library/src/data/computeNodeColumns.ts @@ -13,8 +13,8 @@ import { ActiveNodes } from './ActiveNodes' * @param parents - A map of commit hashes to their parent commits * @returns An object containing commit positions, graph width, and edge connections */ -export const computeNodePositions = ( - commits: Commit[], +export const computeNodePositions = ( + commits: Commit[], currentBranch: string, children: Map, parents: Map diff --git a/packages/library/src/data/computeRelationships.ts b/packages/library/src/data/computeRelationships.ts index 0014dcbb..a2411e89 100644 --- a/packages/library/src/data/computeRelationships.ts +++ b/packages/library/src/data/computeRelationships.ts @@ -1,60 +1,42 @@ import { Commit } from 'types/Commit' import { GitLogEntry } from 'types/GitLogEntry' -type RawCommit = Omit - -export const computeRelationships = (entries: GitLogEntry[], headCommitHash?: string) => { +export const computeRelationships = ( + entries: GitLogEntry[], + headCommitHash?: string +) => { const children = new Map() const parents = new Map() - const hashToRawCommit = new Map() + const hashToEntry = new Map>() entries.forEach(entry => { - // Initialise all git log entries with no children children.set(entry.hash, []) - - hashToRawCommit.set(entry.hash, { - hash: entry.hash, - committerDate: entry.committerDate, - authorDate: entry.authorDate, - message: entry.message, - parents: entry.parents, - branch: entry.branch, - author: entry.author - }) + hashToEntry.set(entry.hash, entry) }) - // Use parent hashes to calculate children. - // I.e. find the inverse relationship. - entries.forEach((entry) => { + entries.forEach(entry => { const hash = entry.hash const parentHashes = entry.parents parents.set(hash, parentHashes) parentHashes.forEach(parentHash => { const currentChildren = children.get(parentHash) - - // If we have a children entry for the current parent hash, - // then map this entries commit has to it. - // The only time we won't have a children mapped is if the log has - // been passed an incomplete set of entry data (like via - // server-side pagination) and so the parent hash does not - // exist in the hash -> children array map. if (currentChildren) { currentChildren.push(hash) } }) }) - // Now that we've computed the relationships - // we can decorate the raw commits with supplemental - // information and build the final map of commits. - const hashToCommit = new Map() - for (const [hash, rawCommit] of hashToRawCommit) { + const hashToCommit = new Map>() + + for (const [hash, entry] of hashToEntry) { hashToCommit.set(hash, { - ...rawCommit, + ...entry, children: children.get(hash) ?? [], - isBranchTip: headCommitHash ? hash === headCommitHash : children.get(hash)?.length === 0 - }) + isBranchTip: headCommitHash + ? hash === headCommitHash + : (children.get(hash)?.length ?? 0) === 0 + } as Commit) } return { parents, children, hashToCommit } diff --git a/packages/library/src/data/temporalTopologicalSort.ts b/packages/library/src/data/temporalTopologicalSort.ts index 1e144ac5..06fa8ac3 100644 --- a/packages/library/src/data/temporalTopologicalSort.ts +++ b/packages/library/src/data/temporalTopologicalSort.ts @@ -1,14 +1,14 @@ import { Commit } from 'types/Commit' -export const temporalTopologicalSort = ( - commits: Commit[], +export const temporalTopologicalSort = ( + commits: Commit[], children: Map, - hashToCommit: Map + hashToCommit: Map> ) => { - const sorted: Commit[] = [] + const sorted: Commit[] = [] const seen = new Map() - const depthFirstSearch = (commit: Commit) => { + const depthFirstSearch = (commit: Commit) => { if (seen.has(commit.hash)) { return } diff --git a/packages/library/src/data/types.ts b/packages/library/src/data/types.ts index 30ba0b10..5918b0f4 100644 --- a/packages/library/src/data/types.ts +++ b/packages/library/src/data/types.ts @@ -1,7 +1,7 @@ import { Commit } from 'types/Commit' import DataIntervalTree from 'node-interval-tree' -export interface GraphData { +export interface GraphData { /** * A map of the SHA1 commit hash * to an array of commit hashes @@ -22,7 +22,7 @@ export interface GraphData { * A map of the SHA1 commit hash * to the details of that commit. */ - hashToCommit: Map + hashToCommit: Map> /** * The width of the graph. A number @@ -52,7 +52,7 @@ export interface GraphData { * An array of commit details that have been * sorted temporally by committer date. */ - commits: Commit[] + commits: Commit[] } /** diff --git a/packages/library/src/modules/Graph/GraphHTMLGrid.tsx b/packages/library/src/modules/Graph/GraphHTMLGrid.tsx index 7f663573..70e67c15 100644 --- a/packages/library/src/modules/Graph/GraphHTMLGrid.tsx +++ b/packages/library/src/modules/Graph/GraphHTMLGrid.tsx @@ -2,9 +2,9 @@ import { GraphCore } from 'modules/Graph/core' import { HTMLGridGraph } from 'modules/Graph/strategies/Grid' import { HTMLGridGraphProps } from './types' -export const GraphHTMLGrid = (props: HTMLGridGraphProps) => { +export const GraphHTMLGrid = (props: HTMLGridGraphProps) => { return ( - + {...props}> ) diff --git a/packages/library/src/modules/Graph/context/types.ts b/packages/library/src/modules/Graph/context/types.ts index d7e4d050..2df076f7 100644 --- a/packages/library/src/modules/Graph/context/types.ts +++ b/packages/library/src/modules/Graph/context/types.ts @@ -20,7 +20,7 @@ export interface GraphContextBag { /** * A custom commit node implementation. */ - node?: CustomCommitNode + node?: CustomCommitNode /** * The height, in pixels, of the background @@ -69,7 +69,7 @@ export interface GraphContextBag { * rendered on the graph relative to the * pagination configuration. */ - visibleCommits: Commit[] + visibleCommits: Commit[] /** * A map of row indices to their diff --git a/packages/library/src/modules/Graph/core/GraphCore.tsx b/packages/library/src/modules/Graph/core/GraphCore.tsx index 505e2da2..a2767916 100644 --- a/packages/library/src/modules/Graph/core/GraphCore.tsx +++ b/packages/library/src/modules/Graph/core/GraphCore.tsx @@ -6,8 +6,9 @@ import { useResize } from 'hooks/useResize' import { DEFAULT_NODE_SIZE } from 'constants/constants' import { GraphContext, GraphContextBag } from '../context' import { GraphCoreProps } from 'modules/Graph/core/types' +import { CustomCommitNode } from 'modules/Graph' -export const GraphCore = ({ +export const GraphCore = ({ node, children, nodeSize = DEFAULT_NODE_SIZE, @@ -17,7 +18,7 @@ export const GraphCore = ({ showCommitNodeHashes = false, showCommitNodeTooltips = false, highlightedBackgroundHeight -}: PropsWithChildren) => { +}: PropsWithChildren>) => { const { paging, setNodeSize, @@ -47,7 +48,7 @@ export const GraphCore = ({ }) const contextValue = useMemo(() => ({ - node, + node: node as CustomCommitNode, showCommitNodeTooltips, showCommitNodeHashes, nodeTheme, @@ -60,7 +61,7 @@ export const GraphCore = ({ }), [node, showCommitNodeTooltips, showCommitNodeHashes, nodeTheme, nodeSize, graphWidth, virtualColumns, orientation, visibleCommits, columnData, highlightedBackgroundHeight]) return ( - +
{children} @@ -71,6 +72,6 @@ export const GraphCore = ({ /> )}
-
+ ) } \ No newline at end of file diff --git a/packages/library/src/modules/Graph/core/types.ts b/packages/library/src/modules/Graph/core/types.ts index 267a02ea..c5a9acba 100644 --- a/packages/library/src/modules/Graph/core/types.ts +++ b/packages/library/src/modules/Graph/core/types.ts @@ -1,3 +1,3 @@ import { Canvas2DGraphProps, HTMLGridGraphProps } from 'modules/Graph' -export type GraphCoreProps = HTMLGridGraphProps & Canvas2DGraphProps \ No newline at end of file +export type GraphCoreProps = HTMLGridGraphProps & Canvas2DGraphProps \ No newline at end of file diff --git a/packages/library/src/modules/Graph/strategies/Grid/types.ts b/packages/library/src/modules/Graph/strategies/Grid/types.ts index 083cc191..75c46053 100644 --- a/packages/library/src/modules/Graph/strategies/Grid/types.ts +++ b/packages/library/src/modules/Graph/strategies/Grid/types.ts @@ -5,14 +5,14 @@ import { Commit } from 'types/Commit' * A function that returns a custom implementation * for the commit node in the graph. */ -export type CustomCommitNode = (props: CustomCommitNodeProps) => ReactElement +export type CustomCommitNode = (props: CustomCommitNodeProps) => ReactElement -export interface CustomCommitNodeProps { +export interface CustomCommitNodeProps { /** * Details of the commit that this node * represents. */ - commit: Commit + commit: Commit /** * The colour of the node based on the diff --git a/packages/library/src/modules/Graph/types.ts b/packages/library/src/modules/Graph/types.ts index dcaba242..6b898be3 100644 --- a/packages/library/src/modules/Graph/types.ts +++ b/packages/library/src/modules/Graph/types.ts @@ -5,7 +5,7 @@ export type GraphOrientation = 'normal' | 'flipped' export type Canvas2DGraphProps = GraphPropsCommon -export interface HTMLGridGraphProps extends GraphPropsCommon { +export interface HTMLGridGraphProps extends GraphPropsCommon { /** * Whether to show the commit hash * to the side of the node in the graph. @@ -22,7 +22,7 @@ export interface HTMLGridGraphProps extends GraphPropsCommon { * Overrides the commit nodes with a * custom implementation. */ - node?: CustomCommitNode + node?: CustomCommitNode /** * The height, in pixels, of the background diff --git a/packages/library/src/modules/Table/Table.spec.tsx b/packages/library/src/modules/Table/Table.spec.tsx index 9e0cb842..1d46936e 100644 --- a/packages/library/src/modules/Table/Table.spec.tsx +++ b/packages/library/src/modules/Table/Table.spec.tsx @@ -4,8 +4,21 @@ import { table } from 'test/elements/Table' import { Table } from 'modules/Table/Table' import * as gitContext from 'context/GitContext' import { commit, gitContextBag, graphData } from 'test/stubs' +import { afterEach, beforeEach } from 'vitest' +import { Commit } from 'types/Commit' + +const today = Date.UTC(2025, 2, 24, 18, 0, 0) describe('Table', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(today) + }) + + afterEach(() => { + vi.useRealTimers() + }) + it('should pass the given table class to the git log table element', () => { render( { expect(asFragment()).toMatchSnapshot() }) + + it('should inject custom commit fields into the row function', () => { + const customRowFunction = vi.fn() + + interface CustomCommit { + customField: string + } + + const selectedCommit = commit({ + hash: '1', + customField: 'test' + }) + + const previewedCommit = commit({ + hash: '2', + parents: ['1'] + }) + + vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ + isIndexVisible: false, + paging: { + startIndex: 0, + endIndex: 2, + }, + selectedCommit, + previewedCommit, + graphData: graphData({ + positions: new Map([ + ['1', [0, 1]], + ['2', [1, 1]], + ]), + commits: [ + selectedCommit, + previewedCommit + ] + }) + })) + + render( +
{ + customRowFunction(commit) + return
+ }} + /> + ) + + expect(customRowFunction).toHaveBeenCalledWith[]>({ + authorDate: '2025-02-22 22:06:22 +0000', + branch: 'refs/remotes/origin/gh-pages', + children: [ + '30ee0ba', + ], + committerDate: '2025-02-24T22:06:22+00:00', + customField: 'test', + hash: '1', + isBranchTip: false, + message: 'feat(graph): example commit message', + parents: [ + 'afdb263', + ] + }) + }) }) \ No newline at end of file diff --git a/packages/library/src/types.ts b/packages/library/src/types.ts index ede01e90..c9d6c6e2 100644 --- a/packages/library/src/types.ts +++ b/packages/library/src/types.ts @@ -3,12 +3,12 @@ import { ThemeColours, ThemeMode } from './hooks/useTheme/types' import { GitLogEntry } from './types/GitLogEntry' import { Commit } from './types/Commit' -interface GitLogCommonProps { +interface GitLogCommonProps { /** * The git log entries to visualise * on the graph. */ - entries: GitLogEntry[] + entries: GitLogEntry[] /** * A list of SHA1 commit hashes that belong @@ -104,7 +104,7 @@ interface GitLogCommonProps { * * @param commit Details of the selected commit. */ - onSelectCommit?: (commit?: Commit) => void + onSelectCommit?: (commit?: Commit) => void /** * A callback function invoked when a commit @@ -116,7 +116,7 @@ interface GitLogCommonProps { * * @param commit Details of the previewed commit. */ - onPreviewCommit?: (commit?: Commit) => void + onPreviewCommit?: (commit?: Commit) => void /** * Enables the row styling across the log @@ -142,7 +142,7 @@ interface GitLogCommonProps { classes?: GitLogStylingProps } -export interface GitLogProps extends GitLogCommonProps { +export interface GitLogProps extends GitLogCommonProps { /** * The name of the branch that is * currently checked out. @@ -168,7 +168,7 @@ export interface GitLogProps extends GitLogCommonProps { paging?: GitLogPaging } -export interface GitLogPagedProps extends GitLogCommonProps { +export interface GitLogPagedProps extends GitLogCommonProps { /** * The name of the branch in which the Git log * entries belong to. diff --git a/packages/library/src/types/Commit.ts b/packages/library/src/types/Commit.ts index 30132566..0cdc59de 100644 --- a/packages/library/src/types/Commit.ts +++ b/packages/library/src/types/Commit.ts @@ -1,7 +1,4 @@ -/** - * Represents a commit in the Git history. - */ -export interface Commit { +export interface CommitBase { /** * The unique hash (SHA) identifying the commit. */ @@ -55,7 +52,7 @@ export interface Commit { /** * Indicates whether this commit is the - * tip (latest commit) of its branch. + * tip (the latest commit) of its branch. */ isBranchTip: boolean } @@ -73,4 +70,9 @@ export interface CommitAuthor { * The email address of the commit author. */ email?: string; -} \ No newline at end of file +} + +/** + * Represents a commit in the Git history. + */ +export type Commit = CommitBase & T \ No newline at end of file diff --git a/packages/library/src/types/GitLogEntry.ts b/packages/library/src/types/GitLogEntry.ts index 3fdb9a0c..3b584908 100644 --- a/packages/library/src/types/GitLogEntry.ts +++ b/packages/library/src/types/GitLogEntry.ts @@ -1,9 +1,6 @@ import { CommitAuthor } from './Commit' -/** - * Represents a single entry in the git log. - */ -export interface GitLogEntry { +export interface GitLogEntryBase { /** * The unique hash identifier of the commit. */ @@ -36,7 +33,7 @@ export interface GitLogEntry { /** * The date and time when the commit was applied by the committer. * - * This is typically the timestamp when the commit was finalized. + * This is typically the timestamp when the commit was finalised. */ committerDate: string @@ -46,4 +43,13 @@ export interface GitLogEntry { * This may differ from `committerDate` if the commit was rebased or amended. */ authorDate?: string -} \ No newline at end of file +} + +/** + * Represents a single entry in the git log. + * + * You can pass extra information in the generic + * type, and it will be passed back to you in any + * relevant callback functions. + */ +export type GitLogEntry = GitLogEntryBase & T \ No newline at end of file