From 38869a42153b32df041f213302858df7b74941af Mon Sep 17 00:00:00 2001 From: Nicholas Nelson Date: Tue, 19 Jan 2021 17:55:31 -0800 Subject: [PATCH] Basic GitGraph visualization (#292) [Features] * Initial GitGraph component using react-flow-renderer * GitGraph components generate commit graphs for repositories * Graph layout optimizer using dagre.js [Bugfixes] * Migrate from .toBe to .toHaveValue in Browser component tests * Additional React documentation links for useMemo and useState hooks * Migrate .toBeNull to .not.toBeInTheDocument in CardComponent tests * Migrate .toBe to .toHaveValue in Editor component tests * Updated singlequotes for DiffPickerDialog tests * Migrate .toHaveLength to .toBeInDocument in Explorer component tests * Migrate .toBeNull to .not.toBeInTheDocument in NewCardButton tests * Remove .not.toBeNull in favor of .toBeInTheDocument in NewCardDialog component tests * Minor formatting fixes for style.css and useStyles hook in CardComponent * Fixed max-len for comments in reduxStoreMock.old.ts * Updated singlequotes for errorReducer tests * Disable max-len and singlequote ESLint rules for git.oldspec.ts * Disable max-len and no-commented-out-tests ESLint rules for git.spec.ts * Updated singlequotes for pako.oldspec.ts * Updated singlequotes for io.spec.ts * Multiple fixes for singlequotes and max-len violations per ESLint --- CONTRIBUTING.md | 2 + __test__/Browser.spec.tsx | 238 +++++++++++------------ __test__/CardComponent.spec.tsx | 18 +- __test__/DiffPickerDialog.spec.tsx | 8 +- __test__/Editor.spec.tsx | 6 +- __test__/Explorer.spec.tsx | 8 +- __test__/NewCardButton.spec.tsx | 2 +- __test__/NewCardDialog.spec.tsx | 10 +- __test__/__mocks__/reduxStoreMock.old.ts | 3 +- __test__/errorReducer.spec.ts | 4 +- __test__/git.oldspec.ts | 4 +- __test__/git.spec.ts | 2 + __test__/io.spec.ts | 27 +-- __test__/pako.oldspec.ts | 64 ++++-- __test__/setupTests.ts | 1 + jest.config.js | 5 +- package.json | 3 + src/assets/style.css | 6 + src/components/BranchStatusComponent.tsx | 3 +- src/components/Browser.tsx | 11 +- src/components/CanvasComponent.tsx | 14 +- src/components/CardComponent.tsx | 8 +- src/components/Diff.tsx | 12 +- src/components/DiffPickerDialog.tsx | 3 +- src/components/Explorer.tsx | 12 +- src/components/GitGraph.tsx | 54 +++++ src/components/GitGraphButton.tsx | 60 ++++++ src/components/GitNode.tsx | 38 ++++ src/components/MergeDialog.tsx | 7 +- src/components/NewCardDialog.tsx | 18 +- src/components/RepoBranchList.tsx | 6 +- src/containers/builds.ts | 16 +- src/containers/git.ts | 16 ++ src/containers/io.ts | 16 +- src/containers/layout.ts | 20 ++ src/containers/metafiles.ts | 33 ++-- src/containers/repos.ts | 2 +- src/store/hooks/useDirectory.ts | 3 +- src/store/hooks/useGitHistory.ts | 47 +++++ src/types.d.ts | 3 +- yarn.lock | 190 ++++++++++++++++-- 41 files changed, 745 insertions(+), 258 deletions(-) create mode 100644 __test__/setupTests.ts create mode 100644 src/components/GitGraph.tsx create mode 100644 src/components/GitGraphButton.tsx create mode 100644 src/components/GitNode.tsx create mode 100644 src/containers/layout.ts create mode 100644 src/store/hooks/useGitHistory.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89d65dd40..efbfedf49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,8 @@ React and Redux: * [Hooks API Reference](https://reactjs.org/docs/hooks-reference.html) * [Using the React useContext Hook](https://medium.com/digio-australia/using-the-react-usecontext-hook-9f55461c4eae) * [Idiomatic Redux: Why use action creators?](https://blog.isquaredsoftware.com/2016/10/idiomatic-redux-why-use-action-creators/) +* [ES6 Map with React.useState](https://medium.com/@jalalazimi/es6-map-with-react-usestate-9175cd7b409b) +* [Understanding the React useMemo Hook](https://www.digitalocean.com/community/tutorials/react-usememo) Jest Testing: * [Taking Advantage of Jest Matchers (Part 1)](https://benmccormick.org/2017/08/15/jest-matchers-1/) diff --git a/__test__/Browser.spec.tsx b/__test__/Browser.spec.tsx index 6175928de..7376ff2cb 100644 --- a/__test__/Browser.spec.tsx +++ b/__test__/Browser.spec.tsx @@ -10,126 +10,126 @@ import { mockStore } from './__mocks__/reduxStoreMock'; import { wrapInReduxContext } from './__mocks__/dndReduxMock'; describe('BrowserComponent', () => { - const store = mockStore({ - canvas: { - id: v4(), - created: DateTime.fromISO('1991-12-26T08:00:00.000-08:00'), - repos: [], - cards: [], - stacks: [] - }, - stacks: {}, - cards: {}, - filetypes: {}, - metafiles: {}, - repos: {}, - errors: {} - }); - - const BrowserComponentContext = wrapInReduxContext(BrowserComponent, store); - - it('BrowserComponent allows the user to enter/edit a URL', async () => { - render(); - const textBox = screen.getByRole('textbox') as HTMLInputElement; - - expect(textBox.value).toBe("https://epiclab.github.io/"); - - // Type URL into text box - userEvent.clear(screen.getByRole('textbox')); - await userEvent.type(screen.getByRole('textbox'), 'https://google.com'); - - // Hit Enter button - fireEvent.keyDown(textBox, { key: 'Enter', keyCode: 13, which: 13 }); - - expect(textBox.value).toBe("https://google.com"); - }); - - it('BrowserComponent allows the user to navigate backwards and forwards in history', async () => { - render(); - const backButton = screen.getAllByRole('button')[0]; - const forwardButton = screen.getAllByRole('button')[1]; - const textBox = screen.getByRole('textbox') as HTMLInputElement; - - expect(textBox.value).toBe("https://epiclab.github.io/"); - textBox.focus(); - - // Type URL into text box - userEvent.clear(screen.getByRole('textbox')); - await userEvent.type(screen.getByRole('textbox'), 'https://google.com'); - - // Press Enter key - fireEvent.keyDown(textBox, { key: 'Enter', keyCode: 13, which: 13 }); - expect(screen.getByRole('textbox')).toHaveValue('https://google.com'); - - // Go back in history - fireEvent.click(backButton); - expect(textBox.value).toBe("https://epiclab.github.io/"); - - // Go forward in history - fireEvent.click(forwardButton); - expect(textBox.value).toBe("https://google.com/"); - }); - - it('BrowserComponent does not change the page URL when the refresh button is clicked', async () => { - render(); - const textBox = screen.getByRole('textbox') as HTMLInputElement; - const refreshButton = screen.getAllByRole('button')[2]; - - expect(textBox.value).toBe("https://epiclab.github.io/"); - fireEvent.click(refreshButton); - expect(textBox.value).toBe("https://epiclab.github.io/"); - - textBox.focus(); - - // Type URL into text box - userEvent.clear(screen.getByRole('textbox')); - await userEvent.type(screen.getByRole('textbox'), 'https://google.com'); - - // Press Enter key - fireEvent.keyDown(textBox, { key: 'Enter', keyCode: 13, which: 13 }); - - expect(textBox.value).toBe("https://google.com"); - fireEvent.click(refreshButton); - expect(textBox.value).toBe("https://google.com"); - }); + const store = mockStore({ + canvas: { + id: v4(), + created: DateTime.fromISO('1991-12-26T08:00:00.000-08:00'), + repos: [], + cards: [], + stacks: [] + }, + stacks: {}, + cards: {}, + filetypes: {}, + metafiles: {}, + repos: {}, + errors: {} + }); + + const BrowserComponentContext = wrapInReduxContext(BrowserComponent, store); + + it('BrowserComponent allows the user to enter/edit a URL', async () => { + render(); + const textBox = screen.getByRole('textbox') as HTMLInputElement; + + expect(textBox).toHaveValue('https://epiclab.github.io/'); + + // Type URL into text box + userEvent.clear(screen.getByRole('textbox')); + await userEvent.type(screen.getByRole('textbox'), 'https://google.com'); + + // Hit Enter button + fireEvent.keyDown(textBox, { key: 'Enter', keyCode: 13, which: 13 }); + + expect(textBox).toHaveValue('https://google.com'); + }); + + it('BrowserComponent allows the user to navigate backwards and forwards in history', async () => { + render(); + const backButton = screen.getAllByRole('button')[0]; + const forwardButton = screen.getAllByRole('button')[1]; + const textBox = screen.getByRole('textbox') as HTMLInputElement; + + expect(textBox).toHaveValue('https://epiclab.github.io/'); + textBox.focus(); + + // Type URL into text box + userEvent.clear(screen.getByRole('textbox')); + await userEvent.type(screen.getByRole('textbox'), 'https://google.com'); + + // Press Enter key + fireEvent.keyDown(textBox, { key: 'Enter', keyCode: 13, which: 13 }); + expect(screen.getByRole('textbox')).toHaveValue('https://google.com'); + + // Go back in history + fireEvent.click(backButton); + expect(textBox).toHaveValue('https://epiclab.github.io/'); + + // Go forward in history + fireEvent.click(forwardButton); + expect(textBox).toHaveValue('https://google.com/'); + }); + + it('BrowserComponent does not change the page URL when the refresh button is clicked', async () => { + render(); + const textBox = screen.getByRole('textbox') as HTMLInputElement; + const refreshButton = screen.getAllByRole('button')[2]; + + expect(textBox).toHaveValue('https://epiclab.github.io/'); + fireEvent.click(refreshButton); + expect(textBox).toHaveValue('https://epiclab.github.io/'); + + textBox.focus(); + + // Type URL into text box + userEvent.clear(screen.getByRole('textbox')); + await userEvent.type(screen.getByRole('textbox'), 'https://google.com'); + + // Press Enter key + fireEvent.keyDown(textBox, { key: 'Enter', keyCode: 13, which: 13 }); + + expect(textBox).toHaveValue('https://google.com'); + fireEvent.click(refreshButton); + expect(textBox).toHaveValue('https://google.com'); + }); }); describe('BrowserButton', () => { - const store = mockStore({ - canvas: { - id: v4(), - created: DateTime.fromISO('1991-12-26T08:00:00.000-08:00'), - repos: [], - cards: [], - stacks: [] - }, - stacks: {}, - cards: {}, - filetypes: {}, - metafiles: { - 199: { - id: '199', - name: 'Browser', - handler: 'Browser', - modified: DateTime.fromISO('2019-11-19T19:19:47.572-08:00') - }, - }, - repos: {}, - errors: {} - }); - - const BrowserButtonContext = wrapInReduxContext(BrowserButton, store); - - it('BrowserButton calls onClick when clicked', () => { - const onClick = jest.fn(); - render(); - - const button = screen.getByRole('button'); - if (button) { - button.addEventListener('click', onClick); - fireEvent.click(button); - } - - expect(onClick).toHaveBeenCalled(); - }); + const store = mockStore({ + canvas: { + id: v4(), + created: DateTime.fromISO('1991-12-26T08:00:00.000-08:00'), + repos: [], + cards: [], + stacks: [] + }, + stacks: {}, + cards: {}, + filetypes: {}, + metafiles: { + 199: { + id: '199', + name: 'Browser', + handler: 'Browser', + modified: DateTime.fromISO('2019-11-19T19:19:47.572-08:00') + }, + }, + repos: {}, + errors: {} + }); + + const BrowserButtonContext = wrapInReduxContext(BrowserButton, store); + + it('BrowserButton calls onClick when clicked', () => { + const onClick = jest.fn(); + render(); + + const button = screen.getByRole('button'); + if (button) { + button.addEventListener('click', onClick); + fireEvent.click(button); + } + + expect(onClick).toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/__test__/CardComponent.spec.tsx b/__test__/CardComponent.spec.tsx index 8f61d486e..ff28c21b1 100644 --- a/__test__/CardComponent.spec.tsx +++ b/__test__/CardComponent.spec.tsx @@ -161,13 +161,13 @@ describe('CardComponent', () => { render(); let backsideID = screen.queryByText(/ID:/i); - expect(backsideID).toBeNull(); + expect(backsideID).not.toBeInTheDocument(); - const flipButton = screen.getAllByRole('button')[0]; + const flipButton = screen.getByRole('button', { name: /button-flip/i }); fireEvent.click(flipButton); backsideID = screen.queryByText(/ID:/i); - expect(backsideID).not.toBeNull(); + expect(backsideID).toBeInTheDocument(); }); it('Explorer Card renders a reverse side when the flip button is clicked', () => { @@ -175,13 +175,13 @@ describe('CardComponent', () => { render(); let backsideName = screen.queryByText(/Name:/i); - expect(backsideName).toBeNull(); + expect(backsideName).not.toBeInTheDocument(); const flipButton = screen.getAllByRole('button')[0]; fireEvent.click(flipButton); backsideName = screen.queryByText(/Name:/i); - expect(backsideName).not.toBeNull(); + expect(backsideName).toBeInTheDocument(); }); it('Diff Card renders a reverse side when the flip button is clicked', () => { @@ -189,13 +189,13 @@ describe('CardComponent', () => { render(); let backsideName = screen.queryByText(/Name:/i); - expect(backsideName).toBeNull(); + expect(backsideName).not.toBeInTheDocument(); const flipButton = screen.getAllByRole('button')[0]; fireEvent.click(flipButton); backsideName = screen.queryByText(/Name:/i); - expect(backsideName).not.toBeNull(); + expect(backsideName).toBeInTheDocument(); }); it('Browser Card renders a reverse side when the flip button is clicked', () => { @@ -219,12 +219,12 @@ describe('CardComponent', () => { render(); let icon = screen.queryByRole('img'); - expect(icon).not.toBeNull(); + expect(icon).toBeInTheDocument(); const flipButton = screen.getAllByRole('button')[0]; fireEvent.click(flipButton); icon = screen.queryByRole('img'); - expect(icon).toBeNull(); + expect(icon).not.toBeInTheDocument(); }); }); \ No newline at end of file diff --git a/__test__/DiffPickerDialog.spec.tsx b/__test__/DiffPickerDialog.spec.tsx index fd3b849ad..358af4ca8 100644 --- a/__test__/DiffPickerDialog.spec.tsx +++ b/__test__/DiffPickerDialog.spec.tsx @@ -137,7 +137,7 @@ describe('DiffPickerDialog', () => { if (runDiffButton) fireEvent.click(runDiffButton); // check the parameters of onClose fn to verify they match the card UUIDs - expect(onClose).toHaveBeenCalledWith(false, ["33", "14"]); + expect(onClose).toHaveBeenCalledWith(false, ['33', '14']); }); it('DiffPickerDialog returns cancelled if no cards are selected', () => { @@ -152,7 +152,7 @@ describe('DiffPickerDialog', () => { fireEvent.keyDown(dialog, { key: 'Escape', keyCode: 27, which: 27 }); // check the parameters of onClose fn to verify that canceled parameter is true - expect(onClose).toHaveBeenCalledWith(true, ["", ""]); + expect(onClose).toHaveBeenCalledWith(true, ['', '']); }); it('DiffPickerDialog returns cancelled if only the left card is selected', () => { @@ -178,7 +178,7 @@ describe('DiffPickerDialog', () => { fireEvent.keyDown(dialog, { key: 'Escape', keyCode: 27, which: 27 }); // check the parameters of onClose fn to verify that canceled parameter is true - expect(onClose).toHaveBeenCalledWith(true, ["", ""]); + expect(onClose).toHaveBeenCalledWith(true, ['', '']); }); it('DiffPickerDialog returns cancelled if only the right card is selected', () => { @@ -204,7 +204,7 @@ describe('DiffPickerDialog', () => { fireEvent.keyDown(dialog, { key: 'Escape', keyCode: 27, which: 27 }); // check the parameters of onClose fn to verify that canceled parameter is true - expect(onClose).toHaveBeenCalledWith(true, ["", ""]); + expect(onClose).toHaveBeenCalledWith(true, ['', '']); }); }); diff --git a/__test__/Editor.spec.tsx b/__test__/Editor.spec.tsx index 9cd8af88e..976166266 100644 --- a/__test__/Editor.spec.tsx +++ b/__test__/Editor.spec.tsx @@ -86,17 +86,17 @@ describe('Editor', () => { render(); const textBox = screen.queryByRole('textbox') as HTMLInputElement; - expect(textBox.value).toBe(""); + expect(textBox).toHaveValue(''); textBox.focus(); fireEvent.change(textBox, { target: { value: 'var foo = 5;' } }); - expect(textBox.value).toBe("var foo = 5;"); + expect(textBox).toHaveValue('var foo = 5;'); }); it('Editor component should have a working reverse side', () => { const EditorReverseContext = wrapInReduxContext(EditorReverse, store); - const card = store.getState().cards?.["57"]; + const card = store.getState().cards?.['57']; const wrapper = mount(, mountOptions); const component = wrapper.find(EditorReverse).first(); expect(component).toBeDefined(); diff --git a/__test__/Explorer.spec.tsx b/__test__/Explorer.spec.tsx index 4c5f4d011..17214bd39 100644 --- a/__test__/Explorer.spec.tsx +++ b/__test__/Explorer.spec.tsx @@ -68,8 +68,8 @@ describe('DirectoryComponent', () => { const DirectoryContext = wrapInReduxContext(DirectoryComponent, store); render(); - expect(screen.getAllByRole('treeitem')).toHaveLength(1); - expect(screen.queryAllByText('bar.js')).toHaveLength(0); + expect(screen.getByRole('treeitem')).toBeInTheDocument(); + expect(screen.queryByText('bar.js')).not.toBeInTheDocument(); }); it('DirectoryComponent expands to display child files and directories', () => { @@ -83,12 +83,12 @@ describe('DirectoryComponent', () => { const DirectoryContext = wrapInReduxContext(DirectoryComponent, store); render(); - expect(screen.queryAllByText('bar.js')).toHaveLength(0); + expect(screen.queryByText('bar.js')).not.toBeInTheDocument(); const component = screen.queryByText('foo'); act(() => { if (component) fireEvent.click(component); }); - expect(screen.queryAllByText('bar.js')).toHaveLength(1); + expect(screen.queryByText('bar.js')).toBeInTheDocument(); }); }); diff --git a/__test__/NewCardButton.spec.tsx b/__test__/NewCardButton.spec.tsx index fe0302f9e..8a5a78aee 100644 --- a/__test__/NewCardButton.spec.tsx +++ b/__test__/NewCardButton.spec.tsx @@ -43,7 +43,7 @@ describe('NewCardButton', () => { it('NewCardButton does not render dialog on initial state', () => { render(); - expect(screen.queryByText(/Create New Card/i)).toBeNull(); + expect(screen.queryByText(/Create New Card/i)).not.toBeInTheDocument(); expect(wrapper.find(NewCardDialog).props().open).toBe(false); }); diff --git a/__test__/NewCardDialog.spec.tsx b/__test__/NewCardDialog.spec.tsx index b84476e4b..56fa23ec6 100644 --- a/__test__/NewCardDialog.spec.tsx +++ b/__test__/NewCardDialog.spec.tsx @@ -37,7 +37,7 @@ const store = mockStore({ stacks: {}, cards: {}, filetypes: { - "55": newFiletype + '55': newFiletype }, metafiles: {}, repos: {}, @@ -59,7 +59,6 @@ describe('NewCardDialog', () => { render(); const dialog = screen.queryAllByRole('dialog')[0]; - expect(dialog).not.toBeNull(); expect(dialog).toBeInTheDocument(); fireEvent.keyDown(dialog, { key: 'Escape', keyCode: 27, which: 27 }); @@ -70,7 +69,6 @@ describe('NewCardDialog', () => { render(); const dialog = screen.queryAllByRole('dialog')[0]; - expect(dialog).not.toBeNull(); expect(dialog).toBeInTheDocument(); const backdrop = document.querySelector('.MuiBackdrop-root'); @@ -83,7 +81,7 @@ describe('NewCardDialog', () => { expect(newCardDialog.prop('fileName')).toBeUndefined(); expect(newCardDialog.prop('filetype')).toBeUndefined(); expect(newCardDialog.html()).toContain(''); - expect(wrapper.find(TextField).props().value).toEqual(""); + expect(wrapper.find(TextField).props().value).toEqual(''); }); it('NewCardDialog does not change Redux state when invalid information is entered', () => { @@ -102,12 +100,12 @@ describe('NewCardDialog', () => { render(); const inputBox = screen.getByRole('textbox') as HTMLInputElement; - expect(inputBox.value).toBe(""); + expect(inputBox).toHaveValue(''); // Enter file name into text box if (inputBox) fireEvent.change(inputBox, { target: { value: 'foo' } }); - expect(inputBox.value).toBe("foo"); + expect(inputBox).toHaveValue('foo'); }); it('NewCardDialog allows the user to pick a file type', () => { diff --git a/__test__/__mocks__/reduxStoreMock.old.ts b/__test__/__mocks__/reduxStoreMock.old.ts index 79ffafcee..9b136f7a8 100644 --- a/__test__/__mocks__/reduxStoreMock.old.ts +++ b/__test__/__mocks__/reduxStoreMock.old.ts @@ -1,5 +1,6 @@ /** - * @deprecated This testing infrastructure file will be removed in upcoming commits, prior to any new releases being created from the `development` branch. Please use the new `reduxStoreMock.ts` implementation instead. + * @deprecated This testing infrastructure file will be removed in upcoming commits, prior to any new releases being created from the + * `development` branch. Please use the new `reduxStoreMock.ts` implementation instead. */ import { DateTime } from 'luxon'; import { v4 } from 'uuid'; diff --git a/__test__/errorReducer.spec.ts b/__test__/errorReducer.spec.ts index f63b9452d..04766def8 100644 --- a/__test__/errorReducer.spec.ts +++ b/__test__/errorReducer.spec.ts @@ -8,7 +8,7 @@ describe('errorReducer', () => { id: '92', type: 'RepositoryMissingError', target: '23', - message: `Repository missing for metafile 'sampleUser/myRepo'` + message: 'Repository missing for metafile \'sampleUser/myRepo\'' } } @@ -16,7 +16,7 @@ describe('errorReducer', () => { id: '94', type: 'HandlerMissingError', target: '19', - message: `Metafile 'sampleUser/myRepo' missing handler to resolve filetype: 'TypeScript'` + message: 'Metafile \'sampleUser/myRepo\' missing handler to resolve filetype: \'TypeScript\'' } it('errorReducer returns default state when current state is blank', () => { diff --git a/__test__/git.oldspec.ts b/__test__/git.oldspec.ts index b5d9a2498..5ef1c3b6a 100644 --- a/__test__/git.oldspec.ts +++ b/__test__/git.oldspec.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import mock from 'mock-fs'; import * as git from '../src/containers/git-experimental'; @@ -33,6 +34,7 @@ beforeEach(() => { }, objects: { 'f4': { + // eslint-disable-next-line quotes 'ef1ec2ca088438c2b8fd7a0666d90ee856d278': `b'x\x01+)JMU01f040031Q()JL\xceNM\xd1M\xcb\xccI\xd5\xcb*fP=\x10\xf79y\xae\xae:_\xfd\xf3\xd4ee\x19\x1ffpj\x1f\x05\x00\xa8\xdb\x12\xa5'` }, '4c': { @@ -78,7 +80,7 @@ beforeEach(() => { '7bb34b0807ebf1b91bb66a4c147430cde4f08f': Buffer.from([98, 108, 111, 98, 32, 50, 53, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10]), }, '42': { - '2a8a27eebd3798c661f2c0788dc8d6dfe597a1': `blob 26\x00My data fits on line line\n` + '2a8a27eebd3798c661f2c0788dc8d6dfe597a1': 'blob 26\x00My data fits on line line\n' } } }, diff --git a/__test__/git.spec.ts b/__test__/git.spec.ts index 517375d61..52c13a2e5 100644 --- a/__test__/git.spec.ts +++ b/__test__/git.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable jest/no-commented-out-tests */ +/* eslint-disable max-len */ import mock from 'mock-fs'; import * as path from 'path'; import { homedir } from 'os'; diff --git a/__test__/io.spec.ts b/__test__/io.spec.ts index cb8ac7879..fc5e56f0f 100644 --- a/__test__/io.spec.ts +++ b/__test__/io.spec.ts @@ -119,7 +119,8 @@ describe('io.decompressBinaryObject', () => { mock({ 'plainfile.txt': 'no compression was used', 'e2': { - '7bb34b0807ebf1b91bb66a4c147430cde4f08f': Buffer.from([98, 108, 111, 98, 32, 50, 53, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10]), + '7bb34b0807ebf1b91bb66a4c147430cde4f08f': Buffer.from([98, 108, 111, 98, 32, 50, 53, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, + 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10]), } }); }); @@ -282,12 +283,14 @@ describe('io.filterReadArray', () => { afterAll(mock.restore); it('filterReadArray returns only child directories', () => { - const paths: fs.PathLike[] = ["foo/bar", "foo/baz", "foo/zap/zed/beq", "foo/zap/zed/bup", "foo/zap/zed", "foo/zap/zip", "foo/zap", "foo"]; + const paths: fs.PathLike[] = ['foo/bar', 'foo/baz', 'foo/zap/zed/beq', 'foo/zap/zed/bup', 'foo/zap/zed', + 'foo/zap/zip', 'foo/zap', 'foo']; return expect(io.filterReadArray(paths)).resolves.toHaveLength(3); }); it('filterReadArray returns only child files', () => { - const paths: fs.PathLike[] = ["foo/bar", "foo/baz", "foo/zap/zed/beq", "foo/zap/zed/bup", "foo/zap/zed", "foo/zap/zip", "foo/zap", "foo"]; + const paths: fs.PathLike[] = ['foo/bar', 'foo/baz', 'foo/zap/zed/beq', 'foo/zap/zed/bup', 'foo/zap/zed', + 'foo/zap/zip', 'foo/zap', 'foo']; return expect(io.filterReadArray(paths, true)).resolves.toHaveLength(5); }); }); @@ -322,7 +325,7 @@ describe('io.writeFileAsync', () => { const testPath = 'foo/bar/fileC.txt'; await io.writeFileAsync(testPath, Buffer.from([1, 2, 3]), { encoding: 'hex' }); await expect(fs.ensureFile(testPath)).resolves.not.toThrow(); - await expect(io.readFileAsync(testPath, { encoding: 'hex' })).resolves.toBe("010203"); + await expect(io.readFileAsync(testPath, { encoding: 'hex' })).resolves.toBe('010203'); }); it('writeFileAsync to resolve and overwrite an existing file with content', async () => { @@ -334,8 +337,8 @@ describe('io.writeFileAsync', () => { }); describe('io.validateFileName', () => { - const exts = ["ts", "html"]; - const configExts = [".gitignore", ".htaccess"]; + const exts = ['ts', 'html']; + const configExts = ['.gitignore', '.htaccess']; it('validateFileName returns false for an invalid file name and true for a valid file name', () => { expect(io.validateFileName('<.ts', configExts, exts)).toEqual(false); @@ -373,26 +376,26 @@ describe('io.replaceExt', () => { } it('replaceFileType appends a normal extension to file name with no pre-existing extension', () => { - expect(io.replaceExt("foo", newFiletype)).toBe("foo.js"); + expect(io.replaceExt('foo', newFiletype)).toBe('foo.js'); }); it('replaceFileType appends a normal extension to file name with a pre-existing extension', () => { - expect(io.replaceExt("foo.html", newFiletype)).toBe("foo.js"); + expect(io.replaceExt('foo.html', newFiletype)).toBe('foo.js'); }); it('replaceFileType appends a normal extension to file name with a trailing "."', () => { - expect(io.replaceExt("foo.", newFiletype)).toBe("foo.js"); + expect(io.replaceExt('foo.', newFiletype)).toBe('foo.js'); }); it('replaceFileType appends a .config extension to file name with no pre-existing extension', () => { - expect(io.replaceExt("foo", configFiletype)).toBe("foo.htaccess"); + expect(io.replaceExt('foo', configFiletype)).toBe('foo.htaccess'); }); it('replaceFileType appends a .config extension to file name with a pre-existing extension', () => { - expect(io.replaceExt("foo.html", configFiletype)).toBe("foo.htaccess"); + expect(io.replaceExt('foo.html', configFiletype)).toBe('foo.htaccess'); }); it('replaceFileType appends a .config extension to file name with a trailing "."', () => { - expect(io.replaceExt("foo.", configFiletype)).toBe("foo.htaccess"); + expect(io.replaceExt('foo.', configFiletype)).toBe('foo.htaccess'); }); }); \ No newline at end of file diff --git a/__test__/pako.oldspec.ts b/__test__/pako.oldspec.ts index fcdff464d..557bbb3be 100644 --- a/__test__/pako.oldspec.ts +++ b/__test__/pako.oldspec.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/no-commented-out-tests */ import mock from 'mock-fs'; // import sha1 from 'sha1'; // import pako from 'pako'; @@ -10,33 +11,62 @@ beforeAll(() => { 'git_example': { 'my_file.txt': 'My data fits on one line', '.git': { - config: '[core]\nrepositoryformatversion = 0\nfilemode = true\nbare = false\nlogallrefupdates = true\nignorecase = true\nprecomposeunicode = true\n[remote "origin"]\nurl = git@github.com:test/test.git\nfetch = +refs / heads/*:refs/remotes/origin/*\n[branch "master"]\nremote = origin\nmerge = refs/heads/master', + config: '[core]\nrepositoryformatversion = 0\nfilemode = true\nbare = false\nlogallrefupdates = true\nignorecase = true\n' + + 'precomposeunicode = true\n[remote "origin"]\nurl = git@github.com:test/test.git\nfetch = +refs / ' + + 'heads/*:refs/remotes/origin/*\n[branch "master"]\nremote = origin\nmerge = refs/heads/master', objects: { 'd6': { - '784742fd61d82720f3ed4e8d533fd03fe9b75c': Buffer.from([99, 111, 109, 109, 105, 116, 32, 49, 57, 55, 0, 116, 114, 101, 101, 32, 99, 99, 51, 48, 98, 48, 48, 101, 54, 48, 48, 56, 49, 55, 99, 53, 101, 55, 100, 52, 101, 99, 56, 53, 49, 102, 102, 55, 53, 57, 98, 51, 56, 98, 54, 53, 101, 102, 97, 53, 10, 97, 117, 116, 104, 111, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, 52, 49, 52, 57, 52, 52, 32, 45, 48, 55, 48, 48, 10, 99, 111, 109, 109, 105, 116, 116, 101, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, 52, 49, 52, 57, 52, 52, 32, 45, 48, 55, 48, 48, 10, 10, 102, 105, 114, 115, 116, 32, 99, 111, 109, 109, 105, 116, 10]), + '784742fd61d82720f3ed4e8d533fd03fe9b75c': Buffer.from([99, 111, 109, 109, 105, 116, 32, 49, 57, 55, 0, 116, 114, 101, 101, 32, + 99, 99, 51, 48, 98, 48, 48, 101, 54, 48, 48, 56, 49, 55, 99, 53, 101, 55, 100, 52, 101, 99, 56, 53, 49, 102, 102, 55, 53, + 57, 98, 51, 56, 98, 54, 53, 101, 102, 97, 53, 10, 97, 117, 116, 104, 111, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, + 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, + 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, 52, 49, 52, 57, 52, 52, 32, 45, 48, 55, 48, 48, 10, 99, 111, 109, 109, + 105, 116, 116, 101, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, + 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, + 52, 49, 52, 57, 52, 52, 32, 45, 48, 55, 48, 48, 10, 10, 102, 105, 114, 115, 116, 32, 99, 111, 109, 109, 105, 116, 10]), }, 'bc': { - '4791fb13234461586a9cd874655432efc3691a': Buffer.from([99, 111, 109, 109, 105, 116, 32, 50, 52, 54, 0, 116, 114, 101, 101, 32, 57, 55, 54, 49, 101, 52, 56, 52, 102, 100, 100, 99, 102, 52, 54, 55, 48, 52, 48, 51, 101, 56, 49, 100, 50, 102, 49, 56, 50, 53, 53, 55, 51, 57, 98, 100, 56, 98, 98, 52, 10, 112, 97, 114, 101, 110, 116, 32, 100, 54, 55, 56, 52, 55, 52, 50, 102, 100, 54, 49, 100, 56, 50, 55, 50, 48, 102, 51, 101, 100, 52, 101, 56, 100, 53, 51, 51, 102, 100, 48, 51, 102, 101, 57, 98, 55, 53, 99, 10, 97, 117, 116, 104, 111, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, 52, 49, 52, 57, 57, 54, 32, 45, 48, 55, 48, 48, 10, 99, 111, 109, 109, 105, 116, 116, 101, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, 52, 49, 52, 57, 57, 54, 32, 45, 48, 55, 48, 48, 10, 10, 115, 101, 99, 111, 110, 100, 32, 99, 111, 109, 109, 105, 116, 10]), + '4791fb13234461586a9cd874655432efc3691a': Buffer.from([99, 111, 109, 109, 105, 116, 32, 50, 52, 54, 0, 116, 114, 101, 101, 32, + 57, 55, 54, 49, 101, 52, 56, 52, 102, 100, 100, 99, 102, 52, 54, 55, 48, 52, 48, 51, 101, 56, 49, 100, 50, 102, 49, 56, 50, + 53, 53, 55, 51, 57, 98, 100, 56, 98, 98, 52, 10, 112, 97, 114, 101, 110, 116, 32, 100, 54, 55, 56, 52, 55, 52, 50, 102, 100, + 54, 49, 100, 56, 50, 55, 50, 48, 102, 51, 101, 100, 52, 101, 56, 100, 53, 51, 51, 102, 100, 48, 51, 102, 101, 57, 98, 55, 53, + 99, 10, 97, 117, 116, 104, 111, 114, 32, 78, 105, 99, 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, + 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, + 53, 56, 57, 52, 49, 52, 57, 57, 54, 32, 45, 48, 55, 48, 48, 10, 99, 111, 109, 109, 105, 116, 116, 101, 114, 32, 78, 105, 99, + 104, 111, 108, 97, 115, 32, 78, 101, 108, 115, 111, 110, 32, 60, 110, 101, 108, 115, 111, 110, 110, 105, 64, 111, 114, 101, + 103, 111, 110, 115, 116, 97, 116, 101, 46, 101, 100, 117, 62, 32, 49, 53, 56, 57, 52, 49, 52, 57, 57, 54, 32, 45, 48, 55, 48, + 48, 10, 10, 115, 101, 99, 111, 110, 100, 32, 99, 111, 109, 109, 105, 116, 10]), }, 'e2': { - '7bb34b0807ebf1b91bb66a4c147430cde4f08f': Buffer.from([98, 108, 111, 98, 32, 50, 53, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10]), + '7bb34b0807ebf1b91bb66a4c147430cde4f08f': Buffer.from([98, 108, 111, 98, 32, 50, 53, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, + 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10]), }, '75': { - 'af65d580b62fe4dc1e9cc6922bca9eef08b209': Buffer.from([98, 108, 111, 98, 32, 53, 55, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10, 66, 117, 116, 32, 97, 110, 111, 116, 104, 101, 114, 32, 108, 105, 110, 101, 32, 104, 97, 115, 32, 98, 101, 101, 110, 32, 97, 100, 100, 101, 100, 10]), + 'af65d580b62fe4dc1e9cc6922bca9eef08b209': Buffer.from([98, 108, 111, 98, 32, 53, 55, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, + 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10, 66, 117, 116, 32, 97, 110, 111, 116, 104, 101, + 114, 32, 108, 105, 110, 101, 32, 104, 97, 115, 32, 98, 101, 101, 110, 32, 97, 100, 100, 101, 100, 10]), }, '97': { - '61e484fddcf4670403e81d2f18255739bd8bb4': Buffer.from([116, 114, 101, 101, 32, 51, 57, 0, 49, 48, 48, 54, 52, 52, 32, 109, 121, 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 117, 175, 101, 213, 128, 182, 47, 228, 220, 30, 156, 198, 146, 43, 202, 158, 239, 8, 178, 9]), + '61e484fddcf4670403e81d2f18255739bd8bb4': Buffer.from([116, 114, 101, 101, 32, 51, 57, 0, 49, 48, 48, 54, 52, 52, 32, 109, 121, + 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 117, 175, 101, 213, 128, 182, 47, 228, 220, 30, 156, 198, 146, 43, 202, 158, + 239, 8, 178, 9]), }, 'cc': { - '30b00e600817c5e7d4ec851ff759b38b65efa5': Buffer.from([116, 114, 101, 101, 32, 51, 57, 0, 49, 48, 48, 54, 52, 52, 32, 109, 121, 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 226, 123, 179, 75, 8, 7, 235, 241, 185, 27, 182, 106, 76, 20, 116, 48, 205, 228, 240, 143]), + '30b00e600817c5e7d4ec851ff759b38b65efa5': Buffer.from([116, 114, 101, 101, 32, 51, 57, 0, 49, 48, 48, 54, 52, 52, 32, 109, 121, + 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 226, 123, 179, 75, 8, 7, 235, 241, 185, 27, 182, 106, 76, 20, 116, 48, 205, + 228, 240, 143]), }, '6d': { - '67f18060a4a86a1524a7623b9bc29712feda1c': Buffer.from([98, 108, 111, 98, 32, 53, 50, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10, 66, 117, 116, 32, 97, 110, 111, 116, 104, 101, 114, 32, 104, 97, 115, 32, 98, 101, 101, 110, 32, 97, 100, 100, 101, 100, 10]), + '67f18060a4a86a1524a7623b9bc29712feda1c': Buffer.from([98, 108, 111, 98, 32, 53, 50, 0, 77, 121, 32, 100, 97, 116, 97, 32, 102, + 105, 116, 115, 32, 111, 110, 32, 111, 110, 101, 32, 108, 105, 110, 101, 10, 66, 117, 116, 32, 97, 110, 111, 116, 104, 101, + 114, 32, 104, 97, 115, 32, 98, 101, 101, 110, 32, 97, 100, 100, 101, 100, 10]), } }, HEAD: 'ref: refs/heads/master', info: { - exclude: '# git ls-files --others --exclude-from=.git/info/exclude\n# Lines that start with \'#\' are comments.\n# For a project mostly in C, the following would be a good set of\n# exclude patterns(uncomment them if you want to use them):\n# *.[oa]\n# * ~\n.DS_Store' + exclude: '# git ls-files --others --exclude-from=.git/info/exclude\n# Lines that start with \'#\' are comments.\n# For a ' + + 'project mostly in C, the following would be a good set of\n# exclude patterns(uncomment them if you want to use them):\n' + + '# *.[oa]\n# * ~\n.DS_Store' }, description: 'Unnamed repository; edit this file \'description\' to name the repository.', hooks: {}, @@ -44,7 +74,10 @@ beforeAll(() => { heads: {}, tags: {} }, - index: Buffer.from([68, 73, 82, 67, 0, 0, 0, 2, 0, 0, 0, 1, 94, 181, 141, 83, 2, 210, 75, 158, 94, 181, 141, 83, 2, 210, 75, 158, 1, 0, 0, 4, 3, 3, 122, 34, 0, 0, 129, 164, 0, 0, 1, 245, 0, 0, 0, 20, 0, 0, 0, 25, 226, 123, 179, 75, 8, 7, 235, 241, 185, 27, 182, 106, 76, 20, 116, 48, 205, 228, 240, 143, 0, 11, 109, 121, 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 0, 0, 0, 0, 0, 0, 51, 165, 87, 80, 230, 155, 125, 201, 27, 115, 55, 47, 202, 45, 29, 175, 250, 184, 156, 85]), + index: Buffer.from([68, 73, 82, 67, 0, 0, 0, 2, 0, 0, 0, 1, 94, 181, 141, 83, 2, 210, 75, 158, 94, 181, 141, 83, 2, 210, 75, 158, + 1, 0, 0, 4, 3, 3, 122, 34, 0, 0, 129, 164, 0, 0, 1, 245, 0, 0, 0, 20, 0, 0, 0, 25, 226, 123, 179, 75, 8, 7, 235, 241, 185, 27, + 182, 106, 76, 20, 116, 48, 205, 228, 240, 143, 0, 11, 109, 121, 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 0, 0, 0, 0, 0, 0, + 51, 165, 87, 80, 230, 155, 125, 201, 27, 115, 55, 47, 202, 45, 29, 175, 250, 184, 156, 85]), branches: {} }, }, @@ -61,7 +94,10 @@ describe('git.checkout', () => { // ref: 'develop' // }); // const binaryIndex = await git.readGitObjectToUint8Array('git_example/.git/index'); - // expect(binaryIndex).toStrictEqual(Buffer.from([68, 73, 82, 67, 0, 0, 0, 2, 0, 0, 0, 1, 94, 188, 131, 190, 8, 66, 41, 203, 94, 188, 131, 190, 8, 66, 41, 203, 1, 0, 0, 4, 3, 3, 122, 34, 0, 0, 129, 164, 0, 0, 1, 245, 0, 0, 0, 20, 0, 0, 0, 52, 109, 103, 241, 128, 96, 164, 168, 106, 21, 36, 167, 98, 59, 155, 194, 151, 18, 254, 218, 28, 0, 11, 109, 121, 95, 102, 105, 108, 101, 46, 116, 120, 116, 0, 0, 0, 0, 0, 0, 0, 61, 171, 225, 83, 150, 186, 146, 86, 19, 14, 238, 57, 0, 148, 126, 228, 253, 22, 99, 179])); + // expect(binaryIndex).toStrictEqual(Buffer.from([68, 73, 82, 67, 0, 0, 0, 2, 0, 0, 0, 1, 94, 188, 131, 190, 8, 66, 41, 203, 94, 188, + // 131, 190, 8, 66, 41, 203, 1, 0, 0, 4, 3, 3, 122, 34, 0, 0, 129, 164, 0, 0, 1, 245, 0, 0, 0, 20, 0, 0, 0, 52, 109, 103, 241, 128, + // 96, 164, 168, 106, 21, 36, 167, 98, 59, 155, 194, 151, 18, 254, 218, 28, 0, 11, 109, 121, 95, 102, 105, 108, 101, 46, 116, 120, + // 116, 0, 0, 0, 0, 0, 0, 0, 61, 171, 225, 83, 150, 186, 146, 86, 19, 14, 238, 57, 0, 148, 126, 228, 253, 22, 99, 179])); // }); it.each([ @@ -82,7 +118,8 @@ describe('git.checkout', () => { // const fsObjects = await io.readDirAsyncDeep(incomingDir, false); // const files = await io.filterReadArray(fsObjects, true); // const gitFiles = await git.explodeGitFiles(files); - // gitFiles.map(f => process.stdout.write(`FILE: ${f.file}\ntargetHash: ${f.targetHash}\nhash: ${f.hash}\ndecoded: [${f.decoded}]\nbyte array:\n[${f.binary}]\n\n`)); + // gitFiles.map(f => process.stdout.write(`FILE: ${f.file}\ntargetHash: ${f.targetHash}\nhash: ${f.hash}\ndecoded: + // [${f.decoded}]\nbyte array:\n[${f.binary}]\n\n`)); // expect(true).toBe(true); // }); @@ -93,7 +130,8 @@ describe('git.checkout', () => { // it('checkoutFile fails with a CommitNotFetchedError on non-local branches', async () => { // // CommitNotFetchedError is thrown when a the latest commit for a branch is not available locally // // (i.e. the branch needs to be updated via git fetch) - // return expect(git.checkoutFile('baz/some-file.js', 'remote-only', true)).rejects.toThrow(/Failed to checkout .* because commit .* is not available locally/); + // return expect(git.checkoutFile('baz/some-file.js', 'remote-only', true)).rejects.toThrow(/Failed to checkout .* + // because commit .* is not available locally/); // }); }); diff --git a/__test__/setupTests.ts b/__test__/setupTests.ts new file mode 100644 index 000000000..a6785180a --- /dev/null +++ b/__test__/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect'; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 569c0d8aa..30783705b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,9 @@ module.exports = { testEnvironment: 'enzyme', - setupFilesAfterEnv: ['jest-enzyme'], + setupFilesAfterEnv: [ + 'jest-enzyme', + '/__test__/setupTests.ts' + ], testEnvironmentOptions: { enzymeAdapter: 'react16' }, diff --git a/package.json b/package.json index 56bfde6f8..3ae9a38fd 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@types/react-loadable": "^5.5.4", "@types/react-redux": "^7.1.15", "@types/sha1": "^1.1.2", + "dagre": "^0.8.5", "diff": "^5.0.0", "electron-squirrel-startup": "^1.0.0", "file-type": "^16.2.0", @@ -57,6 +58,7 @@ "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "^16.14.0", + "react-flow-renderer": "^8.3.2", "react-hot-loader": "^4.13.0", "react-loadable": "^5.5.0", "react-redux": "^7.2.2", @@ -79,6 +81,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.3", "@testing-library/react-hooks": "^5.0.0", + "@types/dagre": "^0.7.44", "@types/enzyme": "^3.10.8", "@types/enzyme-adapter-react-16": "^1.0.6", "@types/git-config-path": "^2.0.1", diff --git a/src/assets/style.css b/src/assets/style.css index 1f5238024..372814694 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -407,4 +407,10 @@ div.version-tracker { #git-info-dialog-title { margin-bottom: -25px; + width: 100%; + padding-left: 10px; +} + +.git-flow { + z-index: -1; } diff --git a/src/components/BranchStatusComponent.tsx b/src/components/BranchStatusComponent.tsx index fc97bea0f..7dce87697 100644 --- a/src/components/BranchStatusComponent.tsx +++ b/src/components/BranchStatusComponent.tsx @@ -8,7 +8,8 @@ const BranchComponent: React.FunctionComponent<{ repo: UUID, branch: string }> = const { cards, modified, status } = useBranchStatus(props.repo, props.branch); return ( - cards.map(async c => await status(c))} /> + cards.map(async c => await status(c))} /> ); }; diff --git a/src/components/Browser.tsx b/src/components/Browser.tsx index 5f421c150..8b6e10364 100644 --- a/src/components/Browser.tsx +++ b/src/components/Browser.tsx @@ -17,7 +17,10 @@ type BrowserState = { export const BrowserComponent: React.FunctionComponent = () => { const [webviewKey, setWebviewKey] = useState(0); const [urlInput, setUrlInput] = useState('https://epiclab.github.io/'); - const [browserState, setBrowserState] = useState({ history: [new URL('https://epiclab.github.io/')], current: new URL('https://epiclab.github.io/'), index: 0 }); + const [browserState, setBrowserState] = useState({ + history: [new URL('https://epiclab.github.io/')], + current: new URL('https://epiclab.github.io/'), index: 0 + }); const go = (e: React.KeyboardEvent) => { if (e.keyCode != 13) return; @@ -51,10 +54,12 @@ export const BrowserComponent: React.FunctionComponent = () => { - + + {Object.values(stacks).map(stack => )} {Object.values(cards).filter(card => !card.captured).map(card => )} {errors.map(error => )} diff --git a/src/components/CardComponent.tsx b/src/components/CardComponent.tsx index 0746a5102..58d3cdca7 100644 --- a/src/components/CardComponent.tsx +++ b/src/components/CardComponent.tsx @@ -15,7 +15,7 @@ export const useStyles = makeStyles({ root: { color: 'rgba(171, 178, 191, 1.0)', fontSize: 'small', - fontFamily: `'Lato', Georgia, Serif` + fontFamily: '\'Lato\', Georgia, Serif' } }); @@ -81,12 +81,14 @@ const CardComponent: React.FunctionComponent = props => { return (
-
<> - {flipped ?
:
} + {flipped ? +
: +
}
diff --git a/src/components/Diff.tsx b/src/components/Diff.tsx index aefae7199..e1fa577c0 100644 --- a/src/components/Diff.tsx +++ b/src/components/Diff.tsx @@ -25,8 +25,10 @@ const extractMarkers = (diffOutput: string): IMarker[] => { const Diff: React.FunctionComponent<{ metafileId: UUID }> = props => { const metafile = useSelector((state: RootState) => state.metafiles[props.metafileId]); - const original = useSelector((state: RootState) => (metafile.targets ? state.metafiles[state.cards[metafile.targets[0]]?.metafile] : undefined)); - const updated = useSelector((state: RootState) => (metafile.targets ? state.metafiles[state.cards[metafile.targets[1]]?.metafile] : undefined)); + const original = useSelector((state: RootState) => (metafile.targets ? + state.metafiles[state.cards[metafile.targets[0]]?.metafile] : undefined)); + const updated = useSelector((state: RootState) => (metafile.targets ? + state.metafiles[state.cards[metafile.targets[1]]?.metafile] : undefined)); const [diffOutput, setDiffOutput] = useState(diff(original?.content ? original.content : '', updated?.content ? updated.content : '')); const [markers, setMarkers] = useState(extractMarkers(diffOutput)); @@ -54,8 +56,10 @@ export const DiffReverse: React.FunctionComponent = props => { return ( <> Name:{metafile.name} - Original:{original ? original.name : '[Cannot locate original card]'} (...{original ? original.id.slice(-5) : '[uuid]'}) - Updated:{updated ? updated.name : '[Cannot locate updated card]'} (...{updated ? updated.id.slice(-5) : '[uuid]'}) + Original:{original ? original.name : + '[Cannot locate original card]'} (...{original ? original.id.slice(-5) : '[uuid]'}) + Updated:{updated ? updated.name : + '[Cannot locate updated card]'} (...{updated ? updated.id.slice(-5) : '[uuid]'}) ); }; diff --git a/src/components/DiffPickerDialog.tsx b/src/components/DiffPickerDialog.tsx index 40c03371d..e9063e797 100644 --- a/src/components/DiffPickerDialog.tsx +++ b/src/components/DiffPickerDialog.tsx @@ -82,7 +82,8 @@ const DiffPickerButton: React.FunctionComponent = () => { return ( <> - + ); diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx index eafc5de91..937dca9bc 100644 --- a/src/components/Explorer.tsx +++ b/src/components/Explorer.tsx @@ -32,7 +32,8 @@ export const DirectoryComponent: React.FunctionComponent<{ root: PathLike }> = p return ( {directories.map(dir => )} - {files?.map(file => dispatch(loadCard({ filepath: file }))} />)} + {files?.map(file => + dispatch(loadCard({ filepath: file }))} />)} ); }; @@ -53,11 +54,14 @@ const Explorer: React.FunctionComponent<{ rootId: UUID }> = props => { classes={cssClasses} defaultParentIcon={Folder} defaultEndIcon={
File
} - defaultCollapseIcon={<>
openFolder} - defaultExpandIcon={<>
Folder} + defaultCollapseIcon={<>
+ openFolder} + defaultExpandIcon={<>
+ Folder} > {directories.map(dir => )} - {files.map(file => dispatch(loadCard({ filepath: file }))} />)} + {files.map(file => + dispatch(loadCard({ filepath: file }))} />)} ); diff --git a/src/components/GitGraph.tsx b/src/components/GitGraph.tsx new file mode 100644 index 000000000..7118e1f97 --- /dev/null +++ b/src/components/GitGraph.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import ReactFlow, { addEdge, ArrowHeadType, Connection, Edge, FlowElement, Node } from 'react-flow-renderer'; + +import { Repository } from '../types'; +import { nodeTypes } from './GitNode'; +import { useGitHistory } from '../store/hooks/useGitHistory'; +import { ReadCommitResult } from 'isomorphic-git'; +import { layoutOptimizer } from '../containers/layout'; + +export const GitGraph: React.FunctionComponent<{ repo: Repository }> = props => { + const [elements, setElements] = useState>([]); + const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els)); + const { commits, heads, update } = useGitHistory(props.repo); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { update() }, [props.repo]); + + useEffect(() => { + if (commits.size > 0 && heads.size > 0) { + console.log(`REPO => repo: ${props.repo.name}, commits: ${commits.size}`); + console.log(JSON.stringify([...heads.entries()], null, 2)); + } + }, [commits, heads, props.repo.name]); + + useEffect(() => { + const newElements = [...commits.values()].reduce((prev: Array, curr: ReadCommitResult): Array => { + const node: Node = { + id: curr.oid, + type: 'gitNode', + data: { text: '', tooltip: `${curr.oid.slice(0, 7)}\n${curr.commit.message}` }, + position: { x: 0, y: 0 } + }; + const edges: Edge[] = curr.commit.parent.map(parent => { + return { + id: `e${parent.slice(0, 7)}-${curr.oid.slice(0, 7)}`, + source: parent, + target: curr.oid, + arrowHeadType: ArrowHeadType.ArrowClosed + }; + }); + return [node, ...prev, ...edges]; + }, []); + const optimizedNewElements = layoutOptimizer(newElements); + setElements(optimizedNewElements); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [commits, heads]); + + return ( console.log(node.id)} + className='git-flow' />); +} \ No newline at end of file diff --git a/src/components/GitGraphButton.tsx b/src/components/GitGraphButton.tsx new file mode 100644 index 000000000..17ad49f3b --- /dev/null +++ b/src/components/GitGraphButton.tsx @@ -0,0 +1,60 @@ +import { createStyles, FormControl, InputLabel, makeStyles, MenuItem, Select, Theme } from '@material-ui/core'; +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../store/root'; +import { Repository, UUID } from '../types'; +import { GitGraph } from './GitGraph'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '25ch', + }, + formControl1: { + margin: theme.spacing(1), + backgroundColor: theme.palette.background.paper, + position: 'absolute', + top: '0', + right: '0', + width: '25ch' + }, + }), +); + +export const GitGraphButton: React.FunctionComponent = () => { + const classes = useStyles(); + const repos = useSelector((state: RootState) => Object.values(state.repos)); + const [repo, setRepo] = useState(); + + const repoChange = (event: React.ChangeEvent<{ value: unknown }>) => { + const foundRepo = repos.find(r => r.id === event.target.value as UUID); + if (foundRepo) { + console.log(`foundRepo: ${foundRepo.name}`); + setRepo(foundRepo); + } + }; + + return ( + <> + + Repository + + + { repo ? + + : null + } + + ); +} \ No newline at end of file diff --git a/src/components/GitNode.tsx b/src/components/GitNode.tsx new file mode 100644 index 000000000..7ef137573 --- /dev/null +++ b/src/components/GitNode.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Handle, Position } from 'react-flow-renderer'; +import { Theme, Tooltip, withStyles } from '@material-ui/core'; + +const customNodeStyles = { + height: 10, + width: 10, + borderRadius: '50%', + background: '#9CABB3', + color: '#FFF', + padding: 10, +}; + +const LightTooltip = withStyles((theme: Theme) => ({ + tooltip: { + backgroundColor: theme.palette.common.white, + color: 'rgba(0, 0, 0, 0.87)', + boxShadow: theme.shadows[1], + fontFamily: 'Lato, Georgia, Serif', + fontSize: 11, + }, +}))(Tooltip); + +export const GitNode: React.FunctionComponent<{ data: { text: string, tooltip: string } }> = ({ data }) => { + return ( + +
+ +
{data.text}
+ +
+
+ ) +} + +export const nodeTypes = { + gitNode: GitNode +} \ No newline at end of file diff --git a/src/components/MergeDialog.tsx b/src/components/MergeDialog.tsx index ec067ce1f..73b26c34c 100644 --- a/src/components/MergeDialog.tsx +++ b/src/components/MergeDialog.tsx @@ -62,13 +62,13 @@ const useStyles = makeStyles((theme: Theme) => }, timeline: { margin: theme.spacing(1), - "& > :last-child .MuiTimelineItem-content": { + '& > :last-child .MuiTimelineItem-content': { height: 28 } }, tl_item: { padding: theme.spacing(0, 2), - "&:before": { + '&:before': { flex: 0, padding: theme.spacing(0) } @@ -148,7 +148,8 @@ const TimelineComponent: React.FunctionComponent = props => { Checking for merge conflicts... - {(branchConflicts[1] !== undefined) ? Missing git-config: {JSON.stringify(branchConflicts[1])} : null} + {(branchConflicts[1] !== undefined) ? + Missing git-config: {JSON.stringify(branchConflicts[1])} : null} : null diff --git a/src/components/NewCardDialog.tsx b/src/components/NewCardDialog.tsx index 3bee696b4..94664818a 100644 --- a/src/components/NewCardDialog.tsx +++ b/src/components/NewCardDialog.tsx @@ -20,7 +20,8 @@ export const NewCardDialog: React.FunctionComponent = props const filetypes = useSelector((state: RootState) => Object.values(state.filetypes)); const exts: string[] = flattenArray(filetypes.map(filetype => filetype.extensions)); // List of all valid extensions found w/in filetypes // configExts is a list of all .config extensions found within exts: - const configExts: string[] = flattenArray((filetypes.map(filetype => filetype.extensions.filter(ext => ext.charAt(0) === '.'))).filter(arr => arr.length > 0)); + const configExts: string[] = flattenArray((filetypes.map(filetype => + filetype.extensions.filter(ext => ext.charAt(0) === '.'))).filter(arr => arr.length > 0)); const [fileName, setFileName] = React.useState(''); const [filetype, setFiletype] = React.useState(''); @@ -39,7 +40,7 @@ export const NewCardDialog: React.FunctionComponent = props const handleFileNameChange = (event: React.ChangeEvent) => { setFileName(event.target.value); // currExt takes the current extension found within the filename, or is an empty string if no extension is found - const currExt = event.target.value.indexOf('.') !== -1 ? io.extractExtension(event.target.value) : ""; + const currExt = event.target.value.indexOf('.') !== -1 ? io.extractExtension(event.target.value) : ''; // newExt matches currExt to an existing extension within filetypes. If none is found, it returns undefined const newExt = filetypes.find(filetype => filetype.extensions.find(extension => currExt === extension || '.' + currExt === extension)); @@ -85,14 +86,19 @@ export const NewCardDialog: React.FunctionComponent = props <>
- {"Create New Card"} + {'Create New Card'} Enter File Name: - + Select Filetype: - {filetypes.map(filetype => {filetype.filetype})} - +
diff --git a/src/components/RepoBranchList.tsx b/src/components/RepoBranchList.tsx index 6f6006ec1..fa91a0d22 100644 --- a/src/components/RepoBranchList.tsx +++ b/src/components/RepoBranchList.tsx @@ -36,8 +36,10 @@ export const VersionStatusComponent: React.FunctionComponent = () => { } defaultEndIcon={
Branch
} - defaultCollapseIcon={<>
Repo} - defaultExpandIcon={<>
Repo} + defaultCollapseIcon={<>
+ Repo} + defaultExpandIcon={<>
+ Repo} > {repos.length == 0 && } {repos.length > 0 && repos.map(repo => )} diff --git a/src/containers/builds.ts b/src/containers/builds.ts index ac79a4618..d7cdf5168 100644 --- a/src/containers/builds.ts +++ b/src/containers/builds.ts @@ -32,8 +32,8 @@ export const build = async (repo: Repository, base: string, compare: string): Pr let [installCode, buildCode] = [-1, -1]; try { const installResults = promiseExec(`${packageManager} install`, { cwd: cloneRoot }); - installResults.child.stdout?.on('data', data => console.log(`INSTALL: ` + data)); - installResults.child.stderr?.on('data', data => console.log(`INSTALL error: ` + data)); + installResults.child.stdout?.on('data', data => console.log('INSTALL: ' + data)); + installResults.child.stderr?.on('data', data => console.log('INSTALL error: ' + data)); installResults.child.on('close', code => { console.log(`INSTALL 'close' listener found code: ${code}`); (code ? (installCode = code) : null); @@ -43,12 +43,12 @@ export const build = async (repo: Repository, base: string, compare: string): Pr (code ? (installCode = code) : null); }); installResults.child.on('error', error => { - console.log(`INSTALL 'error' listener found error:`); + console.log('INSTALL \'error\' listener found error:'); console.log({ error }); }); await installResults; } catch (e) { - console.log(`INSTALL ERROR`); + console.log('INSTALL ERROR'); console.log(e); } @@ -56,8 +56,8 @@ export const build = async (repo: Repository, base: string, compare: string): Pr const packageManagerBuildScript = packageManager === 'yarn' ? 'run' : 'run-script'; try { const buildResults = promiseExec(`${packageManager} ${packageManagerBuildScript} build`, { cwd: cloneRoot }); - buildResults.child.stdout?.on('data', data => console.log(`BUILD: ` + data)); - buildResults.child.stderr?.on('data', data => console.log(`BUILD error: ` + data)); + buildResults.child.stdout?.on('data', data => console.log('BUILD: ' + data)); + buildResults.child.stderr?.on('data', data => console.log('BUILD error: ' + data)); buildResults.child.on('close', code => { console.log(`BUILD 'close' listener found code: ${code}`); (code ? (buildCode = code) : null); @@ -67,12 +67,12 @@ export const build = async (repo: Repository, base: string, compare: string): Pr (code ? (buildCode = code) : null); }); buildResults.child.on('error', error => { - console.log(`BUILD 'error' listener found error:`); + console.log('BUILD \'error\' listener found error:'); console.log({ error }); }); await buildResults; } catch (e) { - console.log(`BUILD ERROR`); + console.log('BUILD ERROR'); console.error(e); } } diff --git a/src/containers/git.ts b/src/containers/git.ts index 286fedda0..3ffad6d3c 100644 --- a/src/containers/git.ts +++ b/src/containers/git.ts @@ -61,6 +61,22 @@ export const gitLog = async (dir: fs.PathLike, branch: string, depth: number): P return isogit.log({ fs: fs, dir: dir.toString(), ref: `heads/${branch}`, depth: depth }); } +/** + * Get commit descriptions from the git history; this function is a wrapper to inject the fs parameter in to the + * *isomorphic-git/log* function. + * @param dir The working tree directory path. + * @param ref The commit to begin walking backwards through the history from. + * @param depth Limit the number of commits returned. No limit by default. + * @param since Return history newer than the given date. Can be combined with `depth` to get whichever is shorter. + * @return A Promise object containing an array of `ReadCommitResult` objects (per https://isomorphic-git.org/docs/en/log). + */ +export const log = ({ dir, ref = 'HEAD', depth, since }: { + dir: fs.PathLike; + ref?: string; + depth?: number; + since?: Date; +}): Promise => isogit.log({ fs: fs, dir: dir.toString(), ref: ref, depth: depth, since: since }); + /** * Show the commit delta (i.e. the commits not contained in the overlapping subset) between two branches. Although git refers * to rev-lists of commits in a branch as trees, they are actually Directed Acyclic Graphs (DAG) where the directed paths can diff --git a/src/containers/io.ts b/src/containers/io.ts index 277ab7733..73b1b0e64 100644 --- a/src/containers/io.ts +++ b/src/containers/io.ts @@ -274,7 +274,7 @@ export const writeFileAsync = ( */ export const validateFileName = (fileName: string, configExts: string[], exts: string[]): boolean => { const index = fileName.lastIndexOf('.'); // Get index of last '.' in the file name - const ext = index !== -1 ? fileName.substring(fileName.lastIndexOf('.')) : ""; // grabs the extension plus the '.' before it + const ext = index !== -1 ? fileName.substring(fileName.lastIndexOf('.')) : ''; // grabs the extension plus the '.' before it const name = index !== -1 ? fileName.substr(0, index) : fileName; // grabs the file name w/o the extension and the '.' before it /*Regex below matches all occurences of invalid file name characters in the set: <, >, \, /, |, ?, *, @@ -284,7 +284,7 @@ export const validateFileName = (fileName: string, configExts: string[], exts: s /* If ext is a .config extension, just check for invalid chars and that the final char is not '.' or ' ' in file name Otherwise, check everything above AND that the file name is not empty and that the extension is valid */ - return (configExts.find(configExt => configExt === ext)) ? flag : name !== "" && exts.includes(ext.substr(1)) && flag; + return (configExts.find(configExt => configExt === ext)) ? flag : name !== '' && exts.includes(ext.substr(1)) && flag; } /** @@ -295,23 +295,23 @@ export const validateFileName = (fileName: string, configExts: string[], exts: s * @return A string with the new extension correctly appended to the original file name. */ export const replaceExt = (fileName: string, newFiletype: Filetype): string => { - const currentExtension = fileName.indexOf('.') !== -1 ? extractExtension(fileName) : ""; // get current file extension + const currentExtension = fileName.indexOf('.') !== -1 ? extractExtension(fileName) : ''; // get current file extension const configExtension = newFiletype.extensions.find(extension => extension.includes('.')); // get config file extension, otherwise undefined - let finalFileName = ""; + let finalFileName = ''; if (configExtension) { - if (!currentExtension && fileName.slice(-1) !== ".") { + if (!currentExtension && fileName.slice(-1) !== '.') { finalFileName = fileName + configExtension; // add config file extension to filename when no extension exists and last character is not a '.' - } else if (!currentExtension && fileName.slice(-1) === ".") { + } else if (!currentExtension && fileName.slice(-1) === '.') { finalFileName = fileName.slice(0, -1) + configExtension; // add config file extension when filename has a trailing '.' character } else { finalFileName = fileName.slice(0, -currentExtension.length - 1) + configExtension; // replace the old file extension with the config file extension } } else { - if (!currentExtension && fileName.slice(-1) !== ".") { + if (!currentExtension && fileName.slice(-1) !== '.') { finalFileName = fileName + '.' + newFiletype.extensions[0]; // add new file extension when no current extension exists and last character is not a '.' - } else if (!currentExtension && fileName.slice(-1) === ".") { + } else if (!currentExtension && fileName.slice(-1) === '.') { finalFileName = fileName + newFiletype.extensions[0]; // add new file extension when filename has a trailing '.' character } else { finalFileName = fileName.slice(0, -currentExtension.length) + newFiletype.extensions[0]; // replace the old file extension with the new file extension diff --git a/src/containers/layout.ts b/src/containers/layout.ts new file mode 100644 index 000000000..5f584e3b6 --- /dev/null +++ b/src/containers/layout.ts @@ -0,0 +1,20 @@ +import dagre from 'dagre'; +import { FlowElement, isNode, isEdge } from 'react-flow-renderer'; + +export const layoutOptimizer = (rfGraph: Array): Array => { + const graph = new dagre.graphlib.Graph(); + graph.setGraph({}); + graph.setDefaultEdgeLabel(() => { return {}; }); + + rfGraph.filter(isNode).map(node => graph.setNode(node.id, { width: 10, height: 10 })); + rfGraph.filter(isEdge).map(edge => graph.setEdge(edge.source, edge.target)); + dagre.layout(graph); + + return rfGraph.map(els => { + if (isNode(els)) { + const graphNode = graph.node(els.id); + return { ...els, position: { x: graphNode.x, y: graphNode.y } }; + } + return els; + }); +} \ No newline at end of file diff --git a/src/containers/metafiles.ts b/src/containers/metafiles.ts index 3f23e9e39..44528f6e8 100644 --- a/src/containers/metafiles.ts +++ b/src/containers/metafiles.ts @@ -189,43 +189,50 @@ export const updateAll = (id: UUID): ThunkAction, RootState, un type MetafileGettableFields = { id: UUID, filepath?: never, virtual?: never } | { id?: never, filepath: PathLike, virtual?: never } | - { id?: never, filepath?: never, virtual: Required> & Omit }; + { + id?: never, filepath?: never, virtual: + Required> & Omit + }; /** - * Thunk Action Creator for retrieving a `Metafile` object associated with one of three different paremeter sets: (1) retrieve existing metafile by - * UUID, (2) get a existing or new metafile associated with a particular file path, or (3) get a new or existing virtual metafile by name and handler. + * Thunk Action Creator for retrieving a `Metafile` object associated with one of three different paremeter sets: (1) retrieve existing + * metafile by UUID, (2) get a existing or new metafile associated with a particular file path, or (3) get a new or existing virtual + * metafile by name and handler. * - * If no existing metafile is found under (1), then a `MetafileMissingError` error is thrown and undefined is returned. A `Metafile` object is always - * returned under (2) and (3), since either an existing metafile is returned or a new metafile is created and returned. All fields within the metafile - * are updated in the Redux store before being returned. + * If no existing metafile is found under (1), then a `MetafileMissingError` error is thrown and undefined is returned. A `Metafile` object + * is always returned under (2) and (3), since either an existing metafile is returned or a new metafile is created and returned. All + * fields within the metafile are updated in the Redux store before being returned. * @param id The UUID corresponding to the metafile that should be updated and returned. * @param filepath The relative or absolute path to a file or directory that should be represented by an updated metafile. - * @param virtual A named object containing at least the `name` and `handler` fields of a valid metafile (existing or new), and any other metafile - * fields except for `id` and `modified` (which are auto-generated on metafile creation). + * @param virtual A named object containing at least the `name` and `handler` fields of a valid metafile (existing or new), and any other + * metafile fields except for `id` and `modified` (which are auto-generated on metafile creation). * @return A Thunk that can be executed to simultaneously dispatch Redux updates and retrieve a `Metafile` object, if the * metafile cannot be added or retrieved from the Redux store then `undefined` is returned instead. */ export const getMetafile = (param: MetafileGettableFields): ThunkAction, RootState, undefined, Action> => { return async (dispatch, getState) => { if (param.id) { - // searches by UUID for existing Metafile in Redux store, dispatching an Error and returning undefined if no match, or updates Metafile otherwise + // searches by UUID for existing Metafile in Redux store, dispatching an Error and returning undefined if no match, or updates + // Metafile otherwise const existing = await dispatch(updateAll(param.id)); if (!existing) dispatch(metafilesError(param.id, `Cannot update non-existing metafile for id: '${param.id}'`)); return existing ? getState().metafiles[param.id] : undefined; } if (param.filepath) { - // searches by filepath (and git branch if available) for existing Metafile in the Redux store, creating a new Metafile if no match, or updates - // Metafile otherwise + // searches by filepath (and git branch if available) for existing Metafile in the Redux store, creating a new Metafile if no match, + // or updates Metafile otherwise const metafiles = Object.values(getState().metafiles); const root = await git.getRepoRoot(param.filepath); const branch = root ? (await git.currentBranch({ dir: root.toString(), fullname: false })) : undefined; - const existing = branch ? metafiles.find(m => m.path == param.filepath && m.branch == branch) : metafiles.find(m => m.path == param.filepath); + const existing = branch ? metafiles.find(m => m.path == param.filepath && m.branch == branch) : + metafiles.find(m => m.path == param.filepath); const id = existing ? existing.id : dispatch(addMetafile(io.extractFilename(param.filepath), { path: param.filepath })).id; const updated = await dispatch(updateAll(id)); return updated ? getState().metafiles[id] : undefined; } if (param.virtual) { - // searches by name and handler for existing Metafile in the Redux store, creates a new Metafile if no match, or returns Metafile otherwise + // searches by name and handler for existing Metafile in the Redux store, creates a new Metafile if no match, or returns + // Metafile otherwise const metafiles = Object.values(getState().metafiles); const existing = metafiles.find(m => m.name == param.virtual.name && m.handler == param.virtual.handler); const id = existing ? existing.id : dispatch(addMetafile(param.virtual.name, param.virtual)).id; diff --git a/src/containers/repos.ts b/src/containers/repos.ts index c2a5a06aa..6bc66469d 100644 --- a/src/containers/repos.ts +++ b/src/containers/repos.ts @@ -160,7 +160,7 @@ export const checkoutBranch = (cardId: UUID, metafileId: UUID, branch: string, p // change the repository branch back to the old branch await isogit.checkout({ fs: fs, dir: repo.root.toString(), ref: baseRef }); - if (progress) console.log(`checkout complete...`); + if (progress) console.log('checkout complete...'); } return getState().metafiles[metafile.id]; }; diff --git a/src/store/hooks/useDirectory.ts b/src/store/hooks/useDirectory.ts index c04eea63c..b465ef3cb 100644 --- a/src/store/hooks/useDirectory.ts +++ b/src/store/hooks/useDirectory.ts @@ -43,7 +43,8 @@ export const useDirectory = (initialRoot: Metafile | PathLike): useDirectoryHook if (!root) { // since not root exists, use `initialRoot` to get a metafile and update `root` - rootMetafile = isMetafile(initialRoot) ? await dispatch(getMetafile({ id: initialRoot.id })) : await dispatch(getMetafile({ filepath: initialRoot })); + rootMetafile = isMetafile(initialRoot) ? await dispatch(getMetafile({ id: initialRoot.id })) : + await dispatch(getMetafile({ filepath: initialRoot })); setRoot(rootMetafile); } diff --git a/src/store/hooks/useGitHistory.ts b/src/store/hooks/useGitHistory.ts new file mode 100644 index 000000000..cf6740f70 --- /dev/null +++ b/src/store/hooks/useGitHistory.ts @@ -0,0 +1,47 @@ +import { useCallback, useState } from 'react'; +import { ReadCommitResult } from 'isomorphic-git'; + +import { Repository } from '../../types'; +import { log } from '../../containers/git'; + +type useGitHistoryHook = { + commits: Map, + heads: Map, + update: () => Promise +} + +/** + * Custom React Hook for managing commit histories for all known branches (local and remote) within a git repository. Resolves a list of + * unique commits that exist across all branches, and mappings from branch names (encoded in the form `[scope]/[branch]`, + * i.e. `local/master`) to the SHA-1 hash of the commit referenced by the head of that branch. The initial state of the hook is empty, and + * will only be populated upon an update. The update method is optimized to collect caches of the commits and head refs for each branch + * before updating the observable maps of commits and head refs. Therefore, a React rerender will only occur after all branches have been + * evaluated. + * @param repo The Repository that contains local and remote branches that should be tracked. + * @return The states of `commits`, `heads`, and the `update` function. Both `commits` and `heads` are maps, where `commits` maps SHA-1 + * commit hashes to commits and `heads` maps scoped branch names to the SHA-1 hash of the commit pointed to by HEAD on that branch. + */ +export const useGitHistory = (repo: Repository): useGitHistoryHook => { + const [commits, setCommits] = useState(new Map()); + const [heads, setHeads] = useState(new Map()); + + const update = useCallback(async () => { + const commitsCache = new Map(); + const headsCache = new Map(); + + const processCommits = async (branch: string, scope: 'local' | 'remote'): Promise => { + const branchCommits = await log({ dir: repo.root.toString(), ref: branch }); + // append to the caches only if no entries exist for the new commit or branch + branchCommits.map(commit => (!commitsCache.has(commit.oid)) ? commitsCache.set(commit.oid, commit) : null); + if (!headsCache.has(`${scope}/${branch}`)) headsCache.set(`${scope}/${branch}`, branchCommits[0].oid); + } + + await Promise.all(repo.local.map(async branch => processCommits(branch, 'local'))); + await Promise.all(repo.local.map(async branch => processCommits(branch, 'remote'))); + // replace the `commits` and `heads` states every time, since deep comparisons for all commits is computationally expensive + setCommits(commitsCache); + setHeads(headsCache); + }, [repo.local, repo.root]); + + return { commits, heads, update }; +} \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts index 40a38e5f9..8d1140a54 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -5,7 +5,8 @@ import parsePath from 'parse-path'; export type UUID = string; export type CardType = 'Editor' | 'Diff' | 'Explorer' | 'Browser' | 'Tracker' | 'Merge'; -export type GitStatus = "modified" | "ignored" | "unmodified" | "*modified" | "*deleted" | "*added" | "absent" | "deleted" | "added" | "*unmodified" | "*absent" | "*undeleted" | "*undeletemodified"; +export type GitStatus = 'modified' | 'ignored' | 'unmodified' | '*modified' | '*deleted' | '*added' + | 'absent' | 'deleted' | 'added' | '*unmodified' | '*absent' | '*undeleted' | '*undeletemodified'; export type Card = { readonly id: UUID; diff --git a/yarn.lock b/yarn.lock index 6344148f4..e294543f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1894,16 +1894,19 @@ "@webassemblyjs/wasm-parser" "1.9.0" "@webassemblyjs/wast-printer" "1.9.0" -"@webassemblyjs/wasm-gen@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe" - integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" +"@webassemblyjs/wasm-edit@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.1.tgz#e27a6bdbf78e5c72fa812a2fc3cbaad7c3e37578" + integrity sha512-S2IaD6+x9B2Xi8BCT0eGsrXXd8UxAh2LVJpg1ZMtHXnrDcsTtIX2bDjHi40Hio6Lc62dWHmKdvksI+MClCYbbw== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-buffer" "1.9.1" + "@webassemblyjs/helper-wasm-bytecode" "1.9.1" + "@webassemblyjs/helper-wasm-section" "1.9.1" + "@webassemblyjs/wasm-gen" "1.9.1" + "@webassemblyjs/wasm-opt" "1.9.1" + "@webassemblyjs/wasm-parser" "1.9.1" + "@webassemblyjs/wast-printer" "1.9.1" "@webassemblyjs/wasm-gen@1.9.0": version "1.9.0" @@ -3088,6 +3091,16 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classcat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/classcat/-/classcat-4.1.0.tgz#e8fd8623e5625187b58adf49bb669a13b6c520f4" + integrity sha512-RA8O5oCi1I1CF6rR4cRBROh8MtZzM4w7xKLm0jd+S6UN2G4FIto+9DVOeFc46JEZFN5PVe/EZWLQO1VU/AUH4A== + +classnames@^2.2.5: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + clean-css@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" @@ -3590,6 +3603,68 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-dispatch@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" + integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== + +d3-drag@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-2.0.0.tgz#9eaf046ce9ed1c25c88661911c1d5a4d8eb7ea6d" + integrity sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w== + dependencies: + d3-dispatch "1 - 2" + d3-selection "2" + +"d3-ease@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-2.0.0.tgz#fd1762bfca00dae4bacea504b1d628ff290ac563" + integrity sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ== + +"d3-interpolate@1 - 2": + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +d3-selection@2, d3-selection@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" + integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== + +"d3-timer@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" + integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== + +d3-transition@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-2.0.0.tgz#366ef70c22ef88d1e34105f507516991a291c94c" + integrity sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog== + dependencies: + d3-color "1 - 2" + d3-dispatch "1 - 2" + d3-ease "1 - 2" + d3-interpolate "1 - 2" + d3-timer "1 - 2" + +d3-zoom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-2.0.0.tgz#f04d0afd05518becce879d04709c47ecd93fba54" + integrity sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw== + dependencies: + d3-dispatch "1 - 2" + d3-drag "2" + d3-interpolate "1 - 2" + d3-selection "2" + d3-transition "2" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3989,6 +4064,20 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +easy-peasy@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/easy-peasy/-/easy-peasy-4.0.1.tgz#8b3ab1ebb43509a62dc2c37b4269a9141e33d918" + integrity sha512-aTvB48M2ej6dM/wllUm1F7CTWGnYOYh82SHBkvJtOZhJ/9L8Gmg/nIVqDPwJeojOWZe+gbLtpyi8DhN6fPNBYg== + dependencies: + immer "7.0.9" + is-plain-object "^5.0.0" + memoizerific "^1.11.3" + redux "^4.0.5" + redux-thunk "^2.3.0" + symbol-observable "^2.0.3" + ts-toolbelt "^8.0.7" + use-memo-one "^1.1.1" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -4135,9 +4224,9 @@ electron-squirrel-startup@^1.0.0: debug "^2.2.0" electron-to-chromium@^1.3.621: - version "1.3.633" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe" - integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA== + version "1.3.634" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.634.tgz#82ea400f520f739c4f6ff00c1f7524827a917d25" + integrity sha512-QPrWNYeE/A0xRvl/QP3E0nkaEvYUvH3gM04ZWYtIa6QlSpEetRlRI1xvQ7hiMIySHHEV+mwDSX8Kj4YZY6ZQAw== electron-winstaller@^4.0.1: version "4.0.1" @@ -4838,7 +4927,7 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -5214,9 +5303,9 @@ fsevents@^1.2.7: nan "^2.12.1" fsevents@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.2.1.tgz#1fb02ded2036a8ac288d507a65962bd87b97628d" - integrity sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" + integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== fsevents@~2.1.2: version "2.1.3" @@ -5867,6 +5956,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immer@7.0.9: + version "7.0.9" + resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e" + integrity sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A== + immutability-helper@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.1.1.tgz#2b86b2286ed3b1241c9e23b7b21e0444f52f77b7" @@ -6224,6 +6318,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" @@ -7565,6 +7664,11 @@ map-obj@^1.0.0, map-obj@^1.0.1: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= +map-or-similar@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" + integrity sha1-beJlMXSt+12e3DPGnT6Sobdvrwg= + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -7609,6 +7713,13 @@ memfs@^3.1.2: dependencies: fs-monkey "1.0.1" +memoizerific@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" + integrity sha1-fIekZGREwy11Q4VwkF8tvRsagFo= + dependencies: + map-or-similar "^1.5.0" + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -8995,7 +9106,7 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@^15.5.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9253,6 +9364,27 @@ react-error-boundary@^3.1.0: dependencies: "@babel/runtime" "^7.12.5" +react-draggable@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3" + integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w== + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + +react-flow-renderer@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/react-flow-renderer/-/react-flow-renderer-8.3.2.tgz#9538d5ffbc2559b115fc732e215be4ce110cce82" + integrity sha512-VeZ28E8yD1aoKsTNBqIVUvYGv3znaGMwR616XclXRNKPXPGk3vxVRGb6Jhsdty/IeGyiBS/9yj5MS5b0jobTHA== + dependencies: + "@babel/runtime" "^7.12.5" + classcat "^4.1.0" + d3-selection "^2.0.0" + d3-zoom "^2.0.0" + easy-peasy "^4.0.1" + fast-deep-equal "^3.1.3" + react-draggable "^4.4.3" + react-hot-loader@^4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.13.0.tgz#c27e9408581c2a678f5316e69c061b226dc6a202" @@ -10580,6 +10712,11 @@ symbol-observable@1.2.0, symbol-observable@^1.2.0: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== +symbol-observable@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-2.0.3.tgz#5b521d3d07a43c351055fa43b8355b62d33fd16a" + integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA== + symbol-tree@^3.2.2, symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -10945,7 +11082,11 @@ ts-jest@^26.4.4: semver "7.x" yargs-parser "20.x" +<<<<<<< HEAD ts-loader@^8.0.14: +======= +ts-loader@^8.0.13: +>>>>>>> Initial GitGraph component using react-flow-renderer version "8.0.14" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.14.tgz#e46ac1f8dcb88808d0b1335d2eae65b74bd78fe8" integrity sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA== @@ -10955,6 +11096,14 @@ ts-loader@^8.0.14: loader-utils "^2.0.0" micromatch "^4.0.0" semver "^7.3.4" +<<<<<<< HEAD +======= + +ts-toolbelt@^8.0.7: + version "8.0.7" + resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-8.0.7.tgz#4dad2928831a811ee17dbdab6eb1919fc0a295bf" + integrity sha512-KICHyKxc5Nu34kyoODrEe2+zvuQQaubTJz7pnC5RQ19TH/Jged1xv+h8LBrouaSD310m75oAljYs59LNHkLDkQ== +>>>>>>> Initial GitGraph component using react-flow-renderer ts-util-is@^1.1.3: version "1.2.1" @@ -11177,6 +11326,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"