From 45c210eb41f52ddc8cb67abb424c4bd786c4e0ea Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Wed, 31 Mar 2021 21:36:11 -0400 Subject: [PATCH] SSH/SCP/HTTPS git URLs with/without .git suffix; update isomorphic-git Co-authored-by: Opender Singh Co-authored-by: Opender Singh --- .../sync/git/__tests__/git-rollback.test.js | 26 +- .../app/sync/git/__tests__/git-vcs.test.js | 141 +++++----- .../app/sync/git/__tests__/mem-client.test.js | 241 ++++++++++++++++++ .../app/sync/git/__tests__/mem-plugin.test.js | 241 ------------------ ...db-plugin.test.js => ne-db-client.test.js} | 50 ++-- .../git/__tests__/routable-fs-client.test.js | 28 ++ .../git/__tests__/routable-fs-plugin.test.js | 28 -- .../app/sync/git/__tests__/utils.test.js | 58 +++++ .../insomnia-app/app/sync/git/fs-client.js | 42 +++ .../insomnia-app/app/sync/git/fs-plugin.js | 73 ------ .../insomnia-app/app/sync/git/git-rollback.js | 2 +- packages/insomnia-app/app/sync/git/git-vcs.js | 184 +++++++------ .../insomnia-app/app/sync/git/http-client.js | 35 +++ packages/insomnia-app/app/sync/git/http.js | 36 --- .../sync/git/{mem-plugin.js => mem-client.js} | 6 +- .../git/{ne-db-plugin.js => ne-db-client.js} | 12 +- ...ble-fs-plugin.js => routable-fs-client.js} | 9 +- packages/insomnia-app/app/sync/git/utils.js | 46 ++++ .../components/dropdowns/git-sync-dropdown.js | 10 +- .../components/modals/git-branches-modal.js | 2 +- .../app/ui/components/modals/git-log-modal.js | 22 +- .../modals/git-repository-settings-modal.js | 1 - .../ui/components/modals/git-staging-modal.js | 4 +- .../app/ui/components/wrapper-home.js | 87 ++++--- .../insomnia-app/app/ui/components/wrapper.js | 2 +- .../insomnia-app/app/ui/containers/app.js | 56 ++-- packages/insomnia-app/package-lock.json | 62 +---- packages/insomnia-app/package.json | 2 +- 28 files changed, 795 insertions(+), 711 deletions(-) create mode 100644 packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js delete mode 100644 packages/insomnia-app/app/sync/git/__tests__/mem-plugin.test.js rename packages/insomnia-app/app/sync/git/__tests__/{ne-db-plugin.test.js => ne-db-client.test.js} (74%) create mode 100644 packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js delete mode 100644 packages/insomnia-app/app/sync/git/__tests__/routable-fs-plugin.test.js create mode 100644 packages/insomnia-app/app/sync/git/__tests__/utils.test.js create mode 100644 packages/insomnia-app/app/sync/git/fs-client.js delete mode 100644 packages/insomnia-app/app/sync/git/fs-plugin.js create mode 100644 packages/insomnia-app/app/sync/git/http-client.js delete mode 100644 packages/insomnia-app/app/sync/git/http.js rename packages/insomnia-app/app/sync/git/{mem-plugin.js => mem-client.js} (99%) rename packages/insomnia-app/app/sync/git/{ne-db-plugin.js => ne-db-client.js} (95%) rename packages/insomnia-app/app/sync/git/{routable-fs-plugin.js => routable-fs-client.js} (84%) create mode 100644 packages/insomnia-app/app/sync/git/utils.js diff --git a/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js b/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js index 375362aabd..4291c1cea6 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js @@ -1,8 +1,8 @@ // @flow import path from 'path'; -import GitVCS, { GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs'; +import { GitVCS, GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs'; import { setupDateMocks } from './util'; -import { MemPlugin } from '../mem-plugin'; +import { MemClient } from '../mem-client'; import type { FileWithStatus } from '../git-rollback'; import { gitRollback } from '../git-rollback'; @@ -107,14 +107,14 @@ describe('git rollback', () => { it('should rollback files as expected', async () => { const originalContent = 'original'; - const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); - await fs.promises.writeFile(fooTxt, 'foo'); - await fs.promises.writeFile(barTxt, 'bar'); - await fs.promises.writeFile(bazTxt, originalContent); + const fsClient = MemClient.createClient(); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); + await fsClient.promises.writeFile(fooTxt, 'foo'); + await fsClient.promises.writeFile(barTxt, 'bar'); + await fsClient.promises.writeFile(bazTxt, originalContent); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); // Commit await vcs.setAuthor('Karen Brown', 'karen@example.com'); @@ -122,7 +122,7 @@ describe('git rollback', () => { await vcs.commit('First commit!'); // Edit file - await fs.promises.writeFile(bazTxt, 'changedContent'); + await fsClient.promises.writeFile(bazTxt, 'changedContent'); // foo is staged, bar is unstaged, but both are untracked (thus, new to git) await vcs.add(`${GIT_INSOMNIA_DIR}/bar.txt`); @@ -148,15 +148,15 @@ describe('git rollback', () => { expect(await vcs.status(barTxt)).toBe('absent'); expect(await vcs.status(bazTxt)).toBe('unmodified'); - // Ensure the two files have been removed from the fs (memplugin) - await expect(fs.promises.readFile(fooTxt)).rejects.toThrowError( + // Ensure the two files have been removed from the fs (memClient) + await expect(fsClient.promises.readFile(fooTxt)).rejects.toThrowError( `ENOENT: no such file or directory, scandir '${fooTxt}'`, ); - await expect(fs.promises.readFile(barTxt)).rejects.toThrowError( + await expect(fsClient.promises.readFile(barTxt)).rejects.toThrowError( `ENOENT: no such file or directory, scandir '${barTxt}'`, ); - expect((await fs.promises.readFile(bazTxt)).toString()).toBe(originalContent); + expect((await fsClient.promises.readFile(bazTxt)).toString()).toBe(originalContent); }); }); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js b/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js index 48c27b1868..b503ca028d 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js @@ -1,6 +1,6 @@ -import GitVCS, { GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs'; +import { GitVCS, GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs'; import { setupDateMocks } from './util'; -import { MemPlugin } from '../mem-plugin'; +import { MemClient } from '../mem-client'; import path from 'path'; import * as git from 'isomorphic-git'; @@ -17,10 +17,10 @@ describe('Git-VCS', () => { describe('common operations', () => { it('listFiles()', async () => { - const fs = MemPlugin.createPlugin(); + const fsClient = MemClient.createClient(); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); // No files exist yet @@ -28,22 +28,22 @@ describe('Git-VCS', () => { expect(files1).toEqual([]); // File does not exist in git index - await fs.promises.writeFile('foo.txt', 'bar'); + await fsClient.promises.writeFile('foo.txt', 'bar'); const files2 = await vcs.listFiles(); expect(files2).toEqual([]); }); it('stage and unstage file', async () => { - const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); - await fs.promises.writeFile(fooTxt, 'foo'); - await fs.promises.writeFile(barTxt, 'bar'); + const fsClient = MemClient.createClient(); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); + await fsClient.promises.writeFile(fooTxt, 'foo'); + await fsClient.promises.writeFile(barTxt, 'bar'); // Files outside namespace should be ignored - await fs.promises.writeFile('/other.txt', 'other'); + await fsClient.promises.writeFile('/other.txt', 'other'); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); expect(await vcs.status(barTxt)).toBe('*added'); @@ -59,24 +59,23 @@ describe('Git-VCS', () => { }); it('Returns empty log without first commit', async () => { - const fs = MemPlugin.createPlugin(); + const fsClient = MemClient.createClient(); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - expect(await vcs.log()).toEqual([]); }); it('commit file', async () => { - const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); - await fs.promises.writeFile(fooTxt, 'foo'); - await fs.promises.writeFile(barTxt, 'bar'); + const fsClient = MemClient.createClient(); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); + await fsClient.promises.writeFile(fooTxt, 'foo'); + await fsClient.promises.writeFile(barTxt, 'bar'); - await fs.promises.writeFile('other.txt', 'should be ignored'); + await fsClient.promises.writeFile('other.txt', 'should be ignored'); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(fooTxt); await vcs.commit('First commit!'); @@ -86,26 +85,34 @@ describe('Git-VCS', () => { expect(await vcs.log()).toEqual([ { - author: { - email: 'karen@example.com', - name: 'Karen Brown', - timestamp: 1000000000, - timezoneOffset: 0, + commit: { + author: { + email: 'karen@example.com', + name: 'Karen Brown', + timestamp: 1000000000, + timezoneOffset: 0, + }, + committer: { + email: 'karen@example.com', + name: 'Karen Brown', + timestamp: 1000000000, + timezoneOffset: 0, + }, + message: 'First commit!\n', + parent: [], + tree: '14819d8019f05edb70a29850deb09a4314ad0afc', }, - committer: { - email: 'karen@example.com', - name: 'Karen Brown', - timestamp: 1000000000, - timezoneOffset: 0, - }, - message: 'First commit!\n', oid: '76f804a23eef9f52017bf93f4bc0bfde45ec8a93', - parent: [], - tree: '14819d8019f05edb70a29850deb09a4314ad0afc', + payload: `tree 14819d8019f05edb70a29850deb09a4314ad0afc +author Karen Brown 1000000000 +0000 +committer Karen Brown 1000000000 +0000 + +First commit! +`, }, ]); - await fs.promises.unlink(fooTxt); + await fsClient.promises.unlink(fooTxt); expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('*deleted'); @@ -119,13 +126,13 @@ describe('Git-VCS', () => { }); it('create branch', async () => { - const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); - await fs.promises.writeFile(fooTxt, 'foo'); - await fs.promises.writeFile(barTxt, 'bar'); + const fsClient = MemClient.createClient(); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); + await fsClient.promises.writeFile(fooTxt, 'foo'); + await fsClient.promises.writeFile(barTxt, 'bar'); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(fooTxt); await vcs.commit('First commit!'); @@ -163,15 +170,15 @@ describe('Git-VCS', () => { const folderBarTxt = path.join(folder, 'bar.txt'); const originalContent = 'content'; - const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); - await fs.promises.writeFile(fooTxt, originalContent); + const fsClient = MemClient.createClient(); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); + await fsClient.promises.writeFile(fooTxt, originalContent); - await fs.promises.mkdir(folder); - await fs.promises.writeFile(folderBarTxt, originalContent); + await fsClient.promises.mkdir(folder); + await fsClient.promises.writeFile(folderBarTxt, originalContent); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); // Commit await vcs.setAuthor('Karen Brown', 'karen@example.com'); @@ -180,8 +187,8 @@ describe('Git-VCS', () => { await vcs.commit('First commit!'); // Change the file - await fs.promises.writeFile(fooTxt, 'changedContent'); - await fs.promises.writeFile(folderBarTxt, 'changedContent'); + await fsClient.promises.writeFile(fooTxt, 'changedContent'); + await fsClient.promises.writeFile(folderBarTxt, 'changedContent'); expect(await vcs.status(fooTxt)).toBe('*modified'); expect(await vcs.status(folderBarTxt)).toBe('*modified'); @@ -193,8 +200,8 @@ describe('Git-VCS', () => { expect(await vcs.status(folderBarTxt)).toBe('unmodified'); // Expect original doc to have reverted - expect((await fs.promises.readFile(fooTxt)).toString()).toBe(originalContent); - expect((await fs.promises.readFile(folderBarTxt)).toString()).toBe(originalContent); + expect((await fsClient.promises.readFile(fooTxt)).toString()).toBe(originalContent); + expect((await fsClient.promises.readFile(folderBarTxt)).toString()).toBe(originalContent); }); it('should remove pending changes from select tracked files', async () => { @@ -205,13 +212,13 @@ describe('Git-VCS', () => { const originalContent = 'content'; const changedContent = 'changedContent'; - const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); + const fsClient = MemClient.createClient(); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); // Write to all files - await Promise.all(files.map(f => fs.promises.writeFile(f, originalContent))); + await Promise.all(files.map(f => fsClient.promises.writeFile(f, originalContent))); // Commit all files await vcs.setAuthor('Karen Brown', 'karen@example.com'); @@ -219,7 +226,7 @@ describe('Git-VCS', () => { await vcs.commit('First commit!'); // Change all files - await Promise.all(files.map(f => fs.promises.writeFile(f, changedContent))); + await Promise.all(files.map(f => fsClient.promises.writeFile(f, changedContent))); await Promise.all(files.map(f => expect(vcs.status(foo1Txt)).resolves.toBe('*modified'))); // Undo foo1 and foo2, but not foo3 @@ -229,42 +236,42 @@ describe('Git-VCS', () => { expect(await vcs.status(foo2Txt)).toBe('unmodified'); // Expect original doc to have reverted for foo1 and foo2 - expect((await fs.promises.readFile(foo1Txt)).toString()).toBe(originalContent); - expect((await fs.promises.readFile(foo2Txt)).toString()).toBe(originalContent); + expect((await fsClient.promises.readFile(foo1Txt)).toString()).toBe(originalContent); + expect((await fsClient.promises.readFile(foo2Txt)).toString()).toBe(originalContent); // Expect changed content for foo3 expect(await vcs.status(foo3Txt)).toBe('*modified'); - expect((await fs.promises.readFile(foo3Txt)).toString()).toBe(changedContent); + expect((await fsClient.promises.readFile(foo3Txt)).toString()).toBe(changedContent); }); }); describe('readObjectFromTree()', () => { it('reads an object from tree', async () => { - const fs = MemPlugin.createPlugin(); + const fsClient = MemClient.createClient(); const dir = path.join(GIT_INSOMNIA_DIR, 'dir'); const dirFooTxt = path.join(dir, 'foo.txt'); - await fs.promises.mkdir(GIT_INSOMNIA_DIR); - await fs.promises.mkdir(dir); - await fs.promises.writeFile(dirFooTxt, 'foo'); + await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); + await fsClient.promises.mkdir(dir); + await fsClient.promises.writeFile(dirFooTxt, 'foo'); const vcs = new GitVCS(); - await vcs.init(GIT_CLONE_DIR, fs); + await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(dirFooTxt); await vcs.commit('First'); - await fs.promises.writeFile(dirFooTxt, 'foo bar'); + await fsClient.promises.writeFile(dirFooTxt, 'foo bar'); await vcs.add(dirFooTxt); await vcs.commit('Second'); const log = await vcs.log(); - expect(await vcs.readObjFromTree(log[0].tree, dirFooTxt)).toBe('foo bar'); - expect(await vcs.readObjFromTree(log[1].tree, dirFooTxt)).toBe('foo'); + expect(await vcs.readObjFromTree(log[0].commit.tree, dirFooTxt)).toBe('foo bar'); + expect(await vcs.readObjFromTree(log[1].commit.tree, dirFooTxt)).toBe('foo'); // Some extra checks - expect(await vcs.readObjFromTree(log[1].tree, 'missing')).toBe(null); + expect(await vcs.readObjFromTree(log[1].commit.tree, 'missing')).toBe(null); expect(await vcs.readObjFromTree('missing', 'missing')).toBe(null); }); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js b/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js new file mode 100644 index 0000000000..c28151441d --- /dev/null +++ b/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js @@ -0,0 +1,241 @@ +import { assertAsyncError, setupDateMocks } from './util'; +import { MemClient } from '../mem-client'; +import path from 'path'; +import { GIT_CLONE_DIR } from '../git-vcs'; + +describe('MemClient', () => { + afterAll(() => jest.restoreAllMocks()); + beforeEach(setupDateMocks); + + const fooTxt = 'foo.txt'; + const barTxt = 'bar.txt'; + + describe('readfile()', () => { + it('fails to read', async () => { + const fsClient = new MemClient(); + await assertAsyncError(fsClient.readFile(fooTxt), 'ENOENT'); + }); + + it('reads a file', async () => { + const fsClient = new MemClient(); + await fsClient.writeFile(fooTxt, 'Hello World!'); + expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World!'); + }); + }); + + describe('writeFile()', () => { + it('fails to write over directory', async () => { + const fsClient = new MemClient(); + const dirName = 'foo'; + + await fsClient.mkdir(dirName); + await assertAsyncError(fsClient.writeFile(dirName, 'Hello World 2!'), 'EISDIR'); + }); + + it('overwrites file', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'Hello World!'); + await fsClient.writeFile(fooTxt, 'Hello World 2!'); + expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World 2!'); + }); + + it('flag "a" file', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'Hello World!', { flag: 'a' }); + await fsClient.writeFile(fooTxt, 'xxx', { flag: 'a' }); + expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World!xxx'); + }); + + it('flags "ax" and "wx" fail if path exists', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'Hello World!'); + await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'ax' }), 'EEXIST'); + await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'wx' }), 'EEXIST'); + }); + + it('fails if flag "r"', async () => { + const fsClient = new MemClient(); + await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'r' }), 'EBADF'); + }); + + it('fails if dir missing', async () => { + const fsClient = new MemClient(); + + await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'r' }), 'EBADF'); + }); + + it('works with flags', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'Hello World!', { flag: 'a' }); + await fsClient.writeFile(fooTxt, 'xxx', { flag: 'a' }); + expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World!xxx'); + }); + }); + + describe('unlink()', () => { + it('unlinks file', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'xxx'); + await fsClient.unlink(fooTxt); + await assertAsyncError(fsClient.readFile(fooTxt), 'ENOENT'); + }); + + it('fails to unlinks missing file', async () => { + const fsClient = new MemClient(); + + await assertAsyncError(fsClient.unlink(path.join('not', 'exist.txt')), 'ENOENT'); + }); + }); + + describe('readdir()', () => { + it('lists dir', async () => { + const fsClient = new MemClient(); + + // Root dir should always exist + expect(await fsClient.readdir(GIT_CLONE_DIR)).toEqual([]); + + // Write a file and list it again + await fsClient.writeFile(fooTxt, 'Hello World!'); + await fsClient.writeFile(barTxt, 'Bar!'); + expect(await fsClient.readdir(GIT_CLONE_DIR)).toEqual(['bar.txt', 'foo.txt']); + }); + + it('errors on file', async () => { + const fsClient = new MemClient(); + await fsClient.writeFile(fooTxt, 'Bar!'); + await assertAsyncError(fsClient.readdir(fooTxt), 'ENOTDIR'); + }); + + it('errors on missing directory', async () => { + const fsClient = new MemClient(); + await assertAsyncError(fsClient.readdir(path.join('/', 'invalid')), 'ENOENT'); + }); + }); + + describe('mkdir()', () => { + const fooDir = 'foo'; + const fooBarDir = path.join(fooDir, 'bar'); + const cloneFooDir = path.join(GIT_CLONE_DIR, 'foo'); + const cloneFooBarDir = path.join(GIT_CLONE_DIR, 'foo', 'bar'); + const cloneFooBarBazDir = path.join(GIT_CLONE_DIR, 'foo', 'bar', 'baz'); + + it('creates directory', async () => { + const fsClient = new MemClient(); + + await fsClient.mkdir(fooDir); + await fsClient.mkdir(fooBarDir); + + expect(await fsClient.readdir(GIT_CLONE_DIR)).toEqual(['foo']); + expect(await fsClient.readdir(cloneFooDir)).toEqual(['bar']); + }); + + it('creates directory non-recursively', async () => { + const fsClient = new MemClient(); + + await fsClient.mkdir(cloneFooDir, { recursive: true }); + await fsClient.mkdir(cloneFooBarDir); + expect(await fsClient.readdir(cloneFooBarDir)).toEqual([]); + }); + + it('creates directory recursively', async () => { + const fsClient = new MemClient(); + + await fsClient.mkdir(cloneFooBarBazDir, { recursive: true }); + expect(await fsClient.readdir(cloneFooBarBazDir)).toEqual([]); + }); + + it('fails to create if no parent', async () => { + const fsClient = new MemClient(); + + await assertAsyncError(fsClient.mkdir(cloneFooBarBazDir), 'ENOENT'); + }); + }); + + describe('rmdir()', () => { + const abDir = path.join('a', 'b'); + const abcDir = path.join('a', 'b', 'c'); + + it('removes a dir', async () => { + const fsClient = new MemClient(); + + await fsClient.mkdir(abcDir, { recursive: true }); + expect(await fsClient.readdir(abDir)).toEqual(['c']); + await fsClient.rmdir(abcDir); + expect(await fsClient.readdir(abDir)).toEqual([]); + }); + + it('fails on non-empty dir', async () => { + const fsClient = new MemClient(); + + await fsClient.mkdir(abcDir, { recursive: true }); + await fsClient.writeFile(path.join(abcDir, 'foo.txt'), 'xxx'); + + await assertAsyncError(fsClient.rmdir(abDir), 'ENOTEMPTY'); + await assertAsyncError(fsClient.rmdir(abcDir), 'ENOTEMPTY'); + }); + + it('fails on file', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'xxx'); + await assertAsyncError(fsClient.rmdir(fooTxt), 'ENOTDIR'); + }); + }); + + describe('stat()', () => { + it('stats root dir', async () => { + const fsClient = new MemClient(); + + const stat = await fsClient.stat(GIT_CLONE_DIR); + + expect(stat).toEqual({ + ctimeMs: 1000000000000, + mtimeMs: 1000000000000, + dev: 1, + gid: 1, + ino: 0, + mode: 0o777, + size: 0, + type: 'dir', + uid: 1, + }); + + expect(stat.isDirectory()).toBe(true); + expect(stat.isFile()).toBe(false); + expect(stat.isSymbolicLink()).toBe(false); + }); + + it('stats file', async () => { + const fsClient = new MemClient(); + + await fsClient.writeFile(fooTxt, 'xxx'); + const stat = await fsClient.stat(fooTxt); + + expect(stat).toEqual({ + ctimeMs: 1000000000001, + mtimeMs: 1000000000001, + dev: 1, + gid: 1, + ino: 0, + mode: 0o777, + size: 4, + type: 'file', + uid: 1, + }); + + expect(stat.isDirectory()).toBe(false); + expect(stat.isFile()).toBe(true); + expect(stat.isSymbolicLink()).toBe(false); + }); + + it('fails to stat missing', async () => { + const fsClient = new MemClient(); + await assertAsyncError(fsClient.stat(barTxt), 'ENOENT'); + }); + }); +}); diff --git a/packages/insomnia-app/app/sync/git/__tests__/mem-plugin.test.js b/packages/insomnia-app/app/sync/git/__tests__/mem-plugin.test.js deleted file mode 100644 index fd2b92df27..0000000000 --- a/packages/insomnia-app/app/sync/git/__tests__/mem-plugin.test.js +++ /dev/null @@ -1,241 +0,0 @@ -import { assertAsyncError, setupDateMocks } from './util'; -import { MemPlugin } from '../mem-plugin'; -import path from 'path'; -import { GIT_CLONE_DIR } from '../git-vcs'; - -describe('MemPlugin', () => { - afterAll(() => jest.restoreAllMocks()); - beforeEach(setupDateMocks); - - const fooTxt = 'foo.txt'; - const barTxt = 'bar.txt'; - - describe('readfile()', () => { - it('fails to read', async () => { - const p = new MemPlugin(); - await assertAsyncError(p.readFile(fooTxt), 'ENOENT'); - }); - - it('reads a file', async () => { - const p = new MemPlugin(); - await p.writeFile(fooTxt, 'Hello World!'); - expect((await p.readFile(fooTxt)).toString()).toBe('Hello World!'); - }); - }); - - describe('writeFile()', () => { - it('fails to write over directory', async () => { - const p = new MemPlugin(); - const dirName = 'foo'; - - await p.mkdir(dirName); - await assertAsyncError(p.writeFile(dirName, 'Hello World 2!'), 'EISDIR'); - }); - - it('overwrites file', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'Hello World!'); - await p.writeFile(fooTxt, 'Hello World 2!'); - expect((await p.readFile(fooTxt)).toString()).toBe('Hello World 2!'); - }); - - it('flag "a" file', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'Hello World!', { flag: 'a' }); - await p.writeFile(fooTxt, 'xxx', { flag: 'a' }); - expect((await p.readFile(fooTxt)).toString()).toBe('Hello World!xxx'); - }); - - it('flags "ax" and "wx" fail if path exists', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'Hello World!'); - await assertAsyncError(p.writeFile(fooTxt, 'aaa', { flag: 'ax' }), 'EEXIST'); - await assertAsyncError(p.writeFile(fooTxt, 'aaa', { flag: 'wx' }), 'EEXIST'); - }); - - it('fails if flag "r"', async () => { - const p = new MemPlugin(); - await assertAsyncError(p.writeFile(fooTxt, 'aaa', { flag: 'r' }), 'EBADF'); - }); - - it('fails if dir missing', async () => { - const p = new MemPlugin(); - - await assertAsyncError(p.writeFile(fooTxt, 'aaa', { flag: 'r' }), 'EBADF'); - }); - - it('works with flags', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'Hello World!', { flag: 'a' }); - await p.writeFile(fooTxt, 'xxx', { flag: 'a' }); - expect((await p.readFile(fooTxt)).toString()).toBe('Hello World!xxx'); - }); - }); - - describe('unlink()', () => { - it('unlinks file', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'xxx'); - await p.unlink(fooTxt); - await assertAsyncError(p.readFile(fooTxt), 'ENOENT'); - }); - - it('fails to unlinks missing file', async () => { - const p = new MemPlugin(); - - await assertAsyncError(p.unlink(path.join('not', 'exist.txt')), 'ENOENT'); - }); - }); - - describe('readdir()', () => { - it('lists dir', async () => { - const p = new MemPlugin(); - - // Root dir should always exist - expect(await p.readdir(GIT_CLONE_DIR)).toEqual([]); - - // Write a file and list it again - await p.writeFile(fooTxt, 'Hello World!'); - await p.writeFile(barTxt, 'Bar!'); - expect(await p.readdir(GIT_CLONE_DIR)).toEqual(['bar.txt', 'foo.txt']); - }); - - it('errors on file', async () => { - const p = new MemPlugin(); - await p.writeFile(fooTxt, 'Bar!'); - await assertAsyncError(p.readdir(fooTxt), 'ENOTDIR'); - }); - - it('errors on missing directory', async () => { - const p = new MemPlugin(); - await assertAsyncError(p.readdir(path.join('/', 'invalid')), 'ENOENT'); - }); - }); - - describe('mkdir()', () => { - const fooDir = 'foo'; - const fooBarDir = path.join(fooDir, 'bar'); - const cloneFooDir = path.join(GIT_CLONE_DIR, 'foo'); - const cloneFooBarDir = path.join(GIT_CLONE_DIR, 'foo', 'bar'); - const cloneFooBarBazDir = path.join(GIT_CLONE_DIR, 'foo', 'bar', 'baz'); - - it('creates directory', async () => { - const p = new MemPlugin(); - - await p.mkdir(fooDir); - await p.mkdir(fooBarDir); - - expect(await p.readdir(GIT_CLONE_DIR)).toEqual(['foo']); - expect(await p.readdir(cloneFooDir)).toEqual(['bar']); - }); - - it('creates directory non-recursively', async () => { - const p = new MemPlugin(); - - await p.mkdir(cloneFooDir, { recursive: true }); - await p.mkdir(cloneFooBarDir); - expect(await p.readdir(cloneFooBarDir)).toEqual([]); - }); - - it('creates directory recursively', async () => { - const p = new MemPlugin(); - - await p.mkdir(cloneFooBarBazDir, { recursive: true }); - expect(await p.readdir(cloneFooBarBazDir)).toEqual([]); - }); - - it('fails to create if no parent', async () => { - const p = new MemPlugin(); - - await assertAsyncError(p.mkdir(cloneFooBarBazDir), 'ENOENT'); - }); - }); - - describe('rmdir()', () => { - const abDir = path.join('a', 'b'); - const abcDir = path.join('a', 'b', 'c'); - - it('removes a dir', async () => { - const p = new MemPlugin(); - - await p.mkdir(abcDir, { recursive: true }); - expect(await p.readdir(abDir)).toEqual(['c']); - await p.rmdir(abcDir); - expect(await p.readdir(abDir)).toEqual([]); - }); - - it('fails on non-empty dir', async () => { - const p = new MemPlugin(); - - await p.mkdir(abcDir, { recursive: true }); - await p.writeFile(path.join(abcDir, 'foo.txt'), 'xxx'); - - await assertAsyncError(p.rmdir(abDir), 'ENOTEMPTY'); - await assertAsyncError(p.rmdir(abcDir), 'ENOTEMPTY'); - }); - - it('fails on file', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'xxx'); - await assertAsyncError(p.rmdir(fooTxt), 'ENOTDIR'); - }); - }); - - describe('stat()', () => { - it('stats root dir', async () => { - const p = new MemPlugin(); - - const stat = await p.stat(GIT_CLONE_DIR); - - expect(stat).toEqual({ - ctimeMs: 1000000000000, - mtimeMs: 1000000000000, - dev: 1, - gid: 1, - ino: 0, - mode: 0o777, - size: 0, - type: 'dir', - uid: 1, - }); - - expect(stat.isDirectory()).toBe(true); - expect(stat.isFile()).toBe(false); - expect(stat.isSymbolicLink()).toBe(false); - }); - - it('stats file', async () => { - const p = new MemPlugin(); - - await p.writeFile(fooTxt, 'xxx'); - const stat = await p.stat(fooTxt); - - expect(stat).toEqual({ - ctimeMs: 1000000000001, - mtimeMs: 1000000000001, - dev: 1, - gid: 1, - ino: 0, - mode: 0o777, - size: 4, - type: 'file', - uid: 1, - }); - - expect(stat.isDirectory()).toBe(false); - expect(stat.isFile()).toBe(true); - expect(stat.isSymbolicLink()).toBe(false); - }); - - it('fails to stat missing', async () => { - const p = new MemPlugin(); - await assertAsyncError(p.stat(barTxt), 'ENOENT'); - }); - }); -}); diff --git a/packages/insomnia-app/app/sync/git/__tests__/ne-db-plugin.test.js b/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.js similarity index 74% rename from packages/insomnia-app/app/sync/git/__tests__/ne-db-plugin.test.js rename to packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.js index e83cdfe474..8634ce5b08 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/ne-db-plugin.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.js @@ -4,11 +4,11 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import * as models from '../../../models'; import * as db from '../../../common/database'; import { assertAsyncError, setupDateMocks } from './util'; -import NeDBPlugin from '../ne-db-plugin'; +import { NeDBClient } from '../ne-db-client'; import path from 'path'; import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../git-vcs'; -describe('NeDBPlugin', () => { +describe('NeDBClient', () => { afterAll(() => jest.restoreAllMocks()); beforeEach(async () => { @@ -27,12 +27,12 @@ describe('NeDBPlugin', () => { describe('readdir()', () => { it('reads model IDs from model type folders', async () => { - const pNeDB = new NeDBPlugin('wrk_1'); + const neDbClient = new NeDBClient('wrk_1'); const reqDir = path.join(GIT_INSOMNIA_DIR, models.request.type); const wrkDir = path.join(GIT_INSOMNIA_DIR, models.workspace.type); - expect(await pNeDB.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]); - expect(await pNeDB.readdir(GIT_INSOMNIA_DIR)).toEqual([ + expect(await neDbClient.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]); + expect(await neDbClient.readdir(GIT_INSOMNIA_DIR)).toEqual([ models.apiSpec.type, models.environment.type, models.grpcRequest.type, @@ -45,8 +45,8 @@ describe('NeDBPlugin', () => { models.workspace.type, ]); - expect(await pNeDB.readdir(reqDir)).toEqual(['req_1.yml', 'req_2.yml']); - expect(await pNeDB.readdir(wrkDir)).toEqual(['wrk_1.yml']); + expect(await neDbClient.readdir(reqDir)).toEqual(['req_1.yml', 'req_2.yml']); + expect(await neDbClient.readdir(wrkDir)).toEqual(['wrk_1.yml']); }); }); @@ -56,7 +56,7 @@ describe('NeDBPlugin', () => { const req1Yml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_1.yml'); const reqXYml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_x.yml'); - const pNeDB = new NeDBPlugin('wrk_1'); + const pNeDB = new NeDBClient('wrk_1'); expect(YAML.parse(await pNeDB.readFile(wrk1Yml, 'utf8'))).toEqual( expect.objectContaining({ _id: 'wrk_1', parentId: null }), @@ -80,15 +80,15 @@ describe('NeDBPlugin', () => { const fileType = expect.objectContaining({ type: 'file' }); // Act - const pNeDB = new NeDBPlugin('wrk_1'); + const neDbClient = new NeDBClient('wrk_1'); // Assert - expect(await pNeDB.stat(GIT_CLONE_DIR)).toEqual(dirType); - expect(await pNeDB.stat(GIT_INSOMNIA_DIR)).toEqual(dirType); - expect(await pNeDB.stat(reqDir)).toEqual(dirType); + expect(await neDbClient.stat(GIT_CLONE_DIR)).toEqual(dirType); + expect(await neDbClient.stat(GIT_INSOMNIA_DIR)).toEqual(dirType); + expect(await neDbClient.stat(reqDir)).toEqual(dirType); - expect(await pNeDB.stat(path.join(wrkDir, 'wrk_1.yml'))).toEqual(fileType); - expect(await pNeDB.stat(path.join(reqDir, 'req_2.yml'))).toEqual(fileType); + expect(await neDbClient.stat(path.join(wrkDir, 'wrk_1.yml'))).toEqual(fileType); + expect(await neDbClient.stat(path.join(reqDir, 'req_2.yml'))).toEqual(fileType); }); }); @@ -97,13 +97,13 @@ describe('NeDBPlugin', () => { // Assemble const upsertSpy = jest.spyOn(db, 'upsert'); const workspaceId = 'wrk_1'; - const pNeDB = new NeDBPlugin(workspaceId); + const neDbClient = new NeDBClient(workspaceId); const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; const filePath = path.join('anotherDir', env.type, `${env._id}.yml`); // Act - await pNeDB.writeFile(filePath, YAML.stringify(env)); + await neDbClient.writeFile(filePath, YAML.stringify(env)); // Assert expect(upsertSpy).not.toBeCalled(); @@ -115,14 +115,14 @@ describe('NeDBPlugin', () => { it('should write files in GIT_INSOMNIA_DIR directory to db', async () => { // Assemble const workspaceId = 'wrk_1'; - const pNeDB = new NeDBPlugin(workspaceId); + const neDbClient = new NeDBClient(workspaceId); const upsertSpy = jest.spyOn(db, 'upsert'); const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; const filePath = path.join(GIT_INSOMNIA_DIR, env.type, `${env._id}.yml`); // Act - await pNeDB.writeFile(filePath, YAML.stringify(env)); + await neDbClient.writeFile(filePath, YAML.stringify(env)); // Assert expect(upsertSpy).toHaveBeenCalledTimes(1); @@ -135,13 +135,13 @@ describe('NeDBPlugin', () => { it('should throw error if id does not match', async () => { // Assemble const workspaceId = 'wrk_1'; - const pNeDB = new NeDBPlugin(workspaceId); + const neDbClient = new NeDBClient(workspaceId); const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; const filePath = path.join(GIT_INSOMNIA_DIR, env.type, `env_2.yml`); // Act - const promiseResult = pNeDB.writeFile(filePath, YAML.stringify(env)); + const promiseResult = neDbClient.writeFile(filePath, YAML.stringify(env)); // Assert await expect(promiseResult).rejects.toThrowError( @@ -152,13 +152,13 @@ describe('NeDBPlugin', () => { it('should throw error if type does not match', async () => { // Assemble const workspaceId = 'wrk_1'; - const pNeDB = new NeDBPlugin(workspaceId); + const neDbClient = new NeDBClient(workspaceId); const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; const filePath = path.join(GIT_INSOMNIA_DIR, models.request.type, `${env._id}.yml`); // Act - const promiseResult = pNeDB.writeFile(filePath, YAML.stringify(env)); + const promiseResult = neDbClient.writeFile(filePath, YAML.stringify(env)); // Assert await expect(promiseResult).rejects.toThrowError( @@ -170,11 +170,11 @@ describe('NeDBPlugin', () => { describe('mkdir()', () => { it('should throw error', async () => { const workspaceId = 'wrk_1'; - const pNeDB = new NeDBPlugin(workspaceId); + const neDbClient = new NeDBClient(workspaceId); - const promiseResult = pNeDB.mkdir('', ''); + const promiseResult = neDbClient.mkdir('', ''); - await expect(promiseResult).rejects.toThrowError('NeDBPlugin is not writable'); + await expect(promiseResult).rejects.toThrowError('NeDBClient is not writable'); }); }); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js b/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js new file mode 100644 index 0000000000..dfcaacb588 --- /dev/null +++ b/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js @@ -0,0 +1,28 @@ +import { MemClient } from '../mem-client'; +import { routableFSClient } from '../routable-fs-client'; +import { GIT_CLONE_DIR } from '../git-vcs'; + +describe('routableFSClient', () => { + afterAll(() => jest.restoreAllMocks()); + it('routes .git and other files to separate places', async () => { + const pGit = MemClient.createClient(); + const pDir = MemClient.createClient(); + + const fsClient = routableFSClient(pDir, { '/.git': pGit }).promises; + + await fsClient.mkdir('/.git'); + await fsClient.mkdir('/other'); + + await fsClient.writeFile('/other/a.txt', 'a'); + await fsClient.writeFile('/.git/b.txt', 'b'); + + expect(await pGit.promises.readdir('/.git')).toEqual(['b.txt']); + expect(await pDir.promises.readdir('/other')).toEqual(['a.txt']); + + // Kind of an edge case, but reading the root dir will not list the .git folder + expect(await pDir.promises.readdir(GIT_CLONE_DIR)).toEqual(['other']); + + expect((await fsClient.readFile('/other/a.txt')).toString()).toBe('a'); + expect((await fsClient.readFile('/.git/b.txt')).toString()).toBe('b'); + }); +}); diff --git a/packages/insomnia-app/app/sync/git/__tests__/routable-fs-plugin.test.js b/packages/insomnia-app/app/sync/git/__tests__/routable-fs-plugin.test.js deleted file mode 100644 index d37a23803b..0000000000 --- a/packages/insomnia-app/app/sync/git/__tests__/routable-fs-plugin.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import { MemPlugin } from '../mem-plugin'; -import { routableFSPlugin } from '../routable-fs-plugin'; -import { GIT_CLONE_DIR } from '../git-vcs'; - -describe('routableFSPlugin', () => { - afterAll(() => jest.restoreAllMocks()); - it('routes .git and other files to separate places', async () => { - const pGit = MemPlugin.createPlugin(); - const pDir = MemPlugin.createPlugin(); - - const p = routableFSPlugin(pDir, { '/.git': pGit }).promises; - - await p.mkdir('/.git'); - await p.mkdir('/other'); - - await p.writeFile('/other/a.txt', 'a'); - await p.writeFile('/.git/b.txt', 'b'); - - expect(await pGit.promises.readdir('/.git')).toEqual(['b.txt']); - expect(await pDir.promises.readdir('/other')).toEqual(['a.txt']); - - // Kind of an edge case, but reading the root dir will not list the .git folder - expect(await pDir.promises.readdir(GIT_CLONE_DIR)).toEqual(['other']); - - expect((await p.readFile('/other/a.txt')).toString()).toBe('a'); - expect((await p.readFile('/.git/b.txt')).toString()).toBe('b'); - }); -}); diff --git a/packages/insomnia-app/app/sync/git/__tests__/utils.test.js b/packages/insomnia-app/app/sync/git/__tests__/utils.test.js new file mode 100644 index 0000000000..af9f676616 --- /dev/null +++ b/packages/insomnia-app/app/sync/git/__tests__/utils.test.js @@ -0,0 +1,58 @@ +import { translateSSHtoHTTP, addDotGit } from '../utils'; + +const links = { + scp: { + bare: 'git@github.com:a/b', + dotGit: 'git@github.com:a/b.git', + }, + ssh: { + bare: 'ssh://a@github.com/b', + dotGit: 'ssh://a@github.com/b.git', + }, + http: { + bare: 'http://github.com/a/b', + dotGit: 'http://github.com/a/b.git', + }, + https: { + bare: 'https://github.com/a/b', + dotGit: 'https://github.com/a/b.git', + }, +}; + +describe('translateSSHtoHTTP', () => { + it('fixes up scp-style', () => { + expect(translateSSHtoHTTP(links.scp.bare)).toEqual(links.https.bare); + expect(translateSSHtoHTTP(links.scp.dotGit)).toEqual(links.https.dotGit); + }); + + it('fixes up ssh-style', () => { + expect(translateSSHtoHTTP(links.ssh.bare)).toEqual('https://a@github.com/b'); + expect(translateSSHtoHTTP(links.ssh.dotGit)).toEqual('https://a@github.com/b.git'); + }); + + it('leaves http alone', () => { + expect(translateSSHtoHTTP(links.http.bare)).toEqual(links.http.bare); + expect(translateSSHtoHTTP(links.http.dotGit)).toEqual(links.http.dotGit); + }); + + it('leaves https alone', () => { + expect(translateSSHtoHTTP(links.https.bare)).toEqual(links.https.bare); + expect(translateSSHtoHTTP(links.https.dotGit)).toEqual(links.https.dotGit); + }); +}); + +describe('addDotGit', () => { + it('adds the .git to bare links', () => { + expect(addDotGit({ url: links.scp.bare })).toEqual(links.scp.dotGit); + expect(addDotGit({ url: links.ssh.bare })).toEqual(links.ssh.dotGit); + expect(addDotGit({ url: links.http.bare })).toEqual(links.http.dotGit); + expect(addDotGit({ url: links.https.bare })).toEqual(links.https.dotGit); + }); + + it('leaves links that already have .git alone', () => { + expect(addDotGit({ url: links.scp.dotGit })).toEqual(links.scp.dotGit); + expect(addDotGit({ url: links.ssh.dotGit })).toEqual(links.ssh.dotGit); + expect(addDotGit({ url: links.http.dotGit })).toEqual(links.http.dotGit); + expect(addDotGit({ url: links.https.dotGit })).toEqual(links.https.dotGit); + }); +}); diff --git a/packages/insomnia-app/app/sync/git/fs-client.js b/packages/insomnia-app/app/sync/git/fs-client.js new file mode 100644 index 0000000000..da5bf9675f --- /dev/null +++ b/packages/insomnia-app/app/sync/git/fs-client.js @@ -0,0 +1,42 @@ +// @flow +import fs from 'fs'; +import path from 'path'; +import mkdirp from 'mkdirp'; + +type FSWraps = + | fs.FSPromise.readFile + | fs.FSPromise.writeFile + | fs.FSPromise.unlink + | fs.FSPromise.readdir + | fs.FSPromise.mkdir + | fs.FSPromise.rmdir + | fs.FSPromise.stat + | fs.FSPromise.lstat + | fs.FSPromise.readlink + | fs.FSPromise.symlink; + +/** This is a client for isomorphic-git. {@link https://isomorphic-git.org/docs/en/fs} */ +export const fsClient = (basePath: string) => { + console.log(`[fsClient] Created in ${basePath}`); + mkdirp.sync(basePath); + + const wrap = (fn: FSWraps) => async (filePath: string, ...args: Array): Promise => { + const modifiedPath = path.join(basePath, path.normalize(filePath)); + return fn(modifiedPath, ...args); + }; + + return { + promises: { + readFile: wrap(fs.promises.readFile), + writeFile: wrap(fs.promises.writeFile), + unlink: wrap(fs.promises.unlink), + readdir: wrap(fs.promises.readdir), + mkdir: wrap(fs.promises.mkdir), + rmdir: wrap(fs.promises.rmdir), + stat: wrap(fs.promises.stat), + lstat: wrap(fs.promises.lstat), + readlink: wrap(fs.promises.readlink), + symlink: wrap(fs.promises.symlink), + }, + }; +}; diff --git a/packages/insomnia-app/app/sync/git/fs-plugin.js b/packages/insomnia-app/app/sync/git/fs-plugin.js deleted file mode 100644 index 69ba3f3668..0000000000 --- a/packages/insomnia-app/app/sync/git/fs-plugin.js +++ /dev/null @@ -1,73 +0,0 @@ -// @flow -import fs from 'fs'; -import path from 'path'; -import mkdirp from 'mkdirp'; - -export default class FSPlugin { - _basePath: string; - - constructor(basePath?: string = '/') { - mkdirp.sync(basePath); - this._basePath = basePath; - console.log(`[FSPlugin] Created in ${basePath}`); - } - - static createPlugin(basePath?: string = '/') { - return { - promises: new FSPlugin(basePath), - }; - } - - async readFile(filePath: string, ...x: Array): Promise { - return this._callbackAsPromise(fs.readFile, filePath, ...x); - } - - async writeFile(filePath: string, data: Buffer | string, ...x: Array) { - return this._callbackAsPromise(fs.writeFile, filePath, data, ...x); - } - - async unlink(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.unlink, filePath, ...x); - } - - async readdir(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.readdir, filePath, ...x); - } - - async mkdir(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.mkdir, filePath, ...x); - } - - async rmdir(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.rmdir, filePath, ...x); - } - - async stat(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.stat, filePath, ...x); - } - - async lstat(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.lstat, filePath, ...x); - } - - async readlink(filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.readlink, filePath, ...x); - } - - async symlink(targetPath: string, filePath: string, ...x: Array) { - return this._callbackAsPromise(fs.symlink, filePath, ...x); - } - - _callbackAsPromise(fn: Function, filePath: string, ...args: Array): Promise { - return new Promise((resolve, reject) => { - filePath = path.join(this._basePath, path.normalize(filePath)); - const callback = args.find(arg => typeof arg === 'function'); - const newArgs = args.filter(arg => arg !== callback); - - fn(filePath, ...newArgs, (err, result) => { - if (err) reject(err); - else resolve(result); - }); - }); - } -} diff --git a/packages/insomnia-app/app/sync/git/git-rollback.js b/packages/insomnia-app/app/sync/git/git-rollback.js index 1dfeabe45a..08ab43b1d7 100644 --- a/packages/insomnia-app/app/sync/git/git-rollback.js +++ b/packages/insomnia-app/app/sync/git/git-rollback.js @@ -1,5 +1,5 @@ // @flow -import GitVCS from './git-vcs'; +import { GitVCS } from './git-vcs'; export type FileWithStatus = { filePath: string, status: string }; diff --git a/packages/insomnia-app/app/sync/git/git-vcs.js b/packages/insomnia-app/app/sync/git/git-vcs.js index 9e802f6e1a..97cbfa05ae 100644 --- a/packages/insomnia-app/app/sync/git/git-vcs.js +++ b/packages/insomnia-app/app/sync/git/git-vcs.js @@ -1,10 +1,10 @@ // @flow import * as git from 'isomorphic-git'; import { trackEvent } from '../../common/analytics'; -import { httpPlugin } from './http'; +import { httpClient } from './http-client'; import { convertToOsSep, convertToPosixSep } from './path-sep'; import path from 'path'; -import EventEmitter from 'events'; +import { gitCallbacks } from './utils'; export type GitAuthor = {| name: string, @@ -28,13 +28,24 @@ type GitCredentialsToken = { export type GitCredentials = GitCredentialsPassword | GitCredentialsToken; +export type GitHash = string; +export type GitRef = GitHash | string; + +export type GitTimestamp = { + timezoneOffset: number, + timestamp: number, +}; + export type GitLogEntry = {| oid: string, - message: string, - tree: string, - author: GitAuthor & { - timestamp: number, + commit: { + message: string, + tree: GitRef, + author: GitAuthor & GitTimestamp, + committer: GitAuthor & GitTimestamp, + parent: Array, }, + payload: string, |}; export type PushResponse = { @@ -43,78 +54,95 @@ export type PushResponse = { headers?: object, }; -// isomorphic-git internally will default an empty ('') clone directory to '.' -// Ref: https://github.com/isomorphic-git/isomorphic-git/blob/4e66704d05042624bbc78b85ee5110d5ee7ec3e2/src/utils/normalizePath.js#L10 -// We should set this explicitly (even if set to an empty string), because we have other code (such as fs plugins -// and unit tests) that depend on the clone directory. +type InitOptions = { + directory?: string, + fs?: Object, + gitDirectory?: string, +}; + +type InitFromCloneOptions = { + url: string, + gitCredentials: GitCredentials, + directory: string, + fs: Object, + gitDirectory: string, +}; + +/** + * isomorphic-git internally will default an empty ('') clone directory to '.' + * + * Ref: https://github.com/isomorphic-git/isomorphic-git/blob/4e66704d05042624bbc78b85ee5110d5ee7ec3e2/src/utils/normalizePath.js#L10 + * + * We should set this explicitly (even if set to an empty string), because we have other code (such as fs clients and unit tests) that depend on the clone directory. + */ export const GIT_CLONE_DIR = '.'; -const _gitInternalDirName = 'git'; +const gitInternalDirName = 'git'; export const GIT_INSOMNIA_DIR_NAME = '.insomnia'; -export const GIT_INTERNAL_DIR = path.join(GIT_CLONE_DIR, _gitInternalDirName); +export const GIT_INTERNAL_DIR = path.join(GIT_CLONE_DIR, gitInternalDirName); export const GIT_INSOMNIA_DIR = path.join(GIT_CLONE_DIR, GIT_INSOMNIA_DIR_NAME); -export default class GitVCS { - _git: Object; - _baseOpts: { dir: string, gitdir?: string, noGitSuffix: boolean }; - _initialized: boolean; +export class GitVCS { + _baseOpts: { + dir: string, + gitdir?: string, + fs: Object, + http: Object, + onMessage: (message: string) => void, + onAuthFailure: (message: string) => void, + onAuthSuccess: (message: string) => void, + onAuth: () => void, + } = gitCallbacks(); + + initialized: boolean; constructor() { - this._initialized = false; + this.initialized = false; } - async init(directory: string, fsPlugin: Object, gitDirectory: string) { - this._git = git; - git.plugins.set('fs', fsPlugin); - git.plugins.set('http', httpPlugin); - const emitter = new EventEmitter(); - git.plugins.set('emitter', emitter); - - emitter.on('message', message => { - console.log(`[git-event] ${message}`); - }); - + async init({ directory, fs, gitDirectory }: InitOptions) { this._baseOpts = { + ...this._baseOpts, dir: directory, gitdir: gitDirectory, - noGitSuffix: true, + fs, + http: httpClient, }; - if (await this._repoExists()) { + if (await this.repoExists()) { console.log(`[git] Opened repo for ${gitDirectory}`); } else { console.log(`[git] Initialized repo in ${gitDirectory}`); - await git.init({ ...this._baseOpts }); + await git.init(this._baseOpts); } - this._initialized = true; + this.initialized = true; } - async initFromClone( - url: string, - creds: GitCredentials, - directory: string, - fsPlugin: Object, - gitDirectory: string, - ) { - this._git = git; - git.plugins.set('fs', fsPlugin); - + async initFromClone({ url, gitCredentials, directory, fs, gitDirectory }: InitFromCloneOptions) { this._baseOpts = { + ...this._baseOpts, + ...gitCallbacks(gitCredentials), dir: directory, gitdir: gitDirectory, - noGitSuffix: true, + fs, + http: httpClient, }; - await git.clone({ ...this._baseOpts, ...creds, url, singleBranch: true }); + const cloneParams = { + ...this._baseOpts, + url, + singleBranch: true, + }; + await git.clone(cloneParams); console.log(`[git] Clones repo to ${gitDirectory} from ${url}`); - this._initialized = true; + this.initialized = true; } isInitialized(): boolean { - return this._initialized; + return this.initialized; } async listFiles(): Promise> { @@ -141,14 +169,14 @@ export default class GitVCS { branches.push(branch); } - return GitVCS._sortBranches(branches); + return GitVCS.sortBranches(branches); } async listRemoteBranches(): Promise> { const branches = await git.listBranches({ ...this._baseOpts, remote: 'origin' }); // Don't care about returning remote HEAD - return GitVCS._sortBranches(branches.filter(b => b !== 'HEAD')); + return GitVCS.sortBranches(branches.filter(b => b !== 'HEAD')); } async status(filepath: string) { @@ -191,8 +219,8 @@ export default class GitVCS { } async getAuthor(): Promise { - const name = await git.config({ ...this._baseOpts, path: 'user.name' }); - const email = await git.config({ ...this._baseOpts, path: 'user.email' }); + const name = await git.getConfig({ ...this._baseOpts, path: 'user.name' }); + const email = await git.getConfig({ ...this._baseOpts, path: 'user.email' }); return { name: name || '', email: email || '', @@ -200,8 +228,8 @@ export default class GitVCS { } async setAuthor(name: string, email: string): Promise { - await git.config({ ...this._baseOpts, path: 'user.name', value: name }); - await git.config({ ...this._baseOpts, path: 'user.email', value: email }); + await git.setConfig({ ...this._baseOpts, path: 'user.name', value: name }); + await git.setConfig({ ...this._baseOpts, path: 'user.email', value: email }); } async getRemote(name: string): Promise { @@ -220,10 +248,10 @@ export default class GitVCS { * when pushing with isomorphic-git, if the HEAD of local is equal the HEAD * of remote, it will fail with a non-fast-forward message. * - * @param creds + * @param gitCredentials * @returns {Promise} */ - async canPush(creds?: GitCredentials | null) { + async canPush(gitCredentials?: GitCredentials | null) { const branch = await this.getBranch(); const remote = await this.getRemote('origin'); @@ -233,7 +261,7 @@ export default class GitVCS { const remoteInfo = await git.getRemoteInfo({ ...this._baseOpts, - ...creds, + ...gitCallbacks(gitCredentials), forPush: true, url: remote.url, }); @@ -250,15 +278,15 @@ export default class GitVCS { return true; } - async push(creds?: GitCredentials | null, force: boolean = false): Promise { + async push(gitCredentials?: GitCredentials | null, force: boolean = false): Promise { console.log(`[git] Push remote=origin force=${force ? 'true' : 'false'}`); trackEvent('Git', 'Push'); // eslint-disable-next-line no-unreachable const response: PushResponse = await git.push({ ...this._baseOpts, + ...gitCallbacks(gitCredentials), remote: 'origin', - ...creds, force, }); @@ -271,13 +299,13 @@ export default class GitVCS { } } - async pull(creds?: GitCredentials | null): Promise { + async pull(gitCredentials?: GitCredentials | null): Promise { console.log('[git] Pull remote=origin', await this.getBranch()); trackEvent('Git', 'Pull'); return git.pull({ ...this._baseOpts, - ...creds, + ...gitCallbacks(gitCredentials), remote: 'origin', singleBranch: true, fast: true, @@ -294,13 +322,13 @@ export default class GitVCS { async fetch( singleBranch: boolean, depth: number | null, - creds?: GitCredentials | null, + gitCredentials?: GitCredentials | null, ): Promise { console.log('[git] Fetch remote=origin'); return git.fetch({ ...this._baseOpts, - ...creds, + ...gitCallbacks(gitCredentials), singleBranch, remote: 'origin', depth, @@ -310,23 +338,13 @@ export default class GitVCS { } async log(depth?: number): Promise> { - let err = null; - let log = []; - try { - log = await git.log({ ...this._baseOpts, depth: depth }); - } catch (e) { - err = e; - } - - if (err && err.code === 'ResolveRefError') { - return []; - } - - if (err) { - throw err; - } else { - return log; + return await git.log({ ...this._baseOpts, depth }); + } catch (error) { + if (error.code === 'NotFoundError') { + return []; + } + throw error; } } @@ -346,7 +364,7 @@ export default class GitVCS { if (branches.includes(branch)) { trackEvent('Git', 'Checkout Branch'); - await git.fastCheckout({ ...this._baseOpts, ref: branch, remote: 'origin' }); + await git.checkout({ ...this._baseOpts, ref: branch, remote: 'origin' }); } else { await this.branch(branch, true); } @@ -355,7 +373,7 @@ export default class GitVCS { async undoPendingChanges(fileFilter?: Array): Promise { console.log('[git] Undo pending changes'); - await git.fastCheckout({ + await git.checkout({ ...this._baseOpts, ref: await this.getBranch(), remote: 'origin', @@ -379,9 +397,9 @@ export default class GitVCS { } } - async _repoExists() { + async repoExists() { try { - await git.config({ ...this._baseOpts, path: '' }); + await git.getConfig({ ...this._baseOpts, path: '' }); } catch (err) { return false; } @@ -390,10 +408,10 @@ export default class GitVCS { } getFs() { - return git.plugins.get('fs'); + return this._baseOpts.fs; } - static _sortBranches(branches: Array) { + static sortBranches(branches: Array) { const newBranches = [...branches]; newBranches.sort((a: string, b: string) => { if (a === 'master') { diff --git a/packages/insomnia-app/app/sync/git/http-client.js b/packages/insomnia-app/app/sync/git/http-client.js new file mode 100644 index 0000000000..5772ce894d --- /dev/null +++ b/packages/insomnia-app/app/sync/git/http-client.js @@ -0,0 +1,35 @@ +import { axiosRequest } from '../../network/axios-request'; + +/** This is a client for isomorphic-git {@link https://isomorphic-git.org/docs/en/http} */ +export const httpClient = { + request: async config => { + let response; + + let body = null; + if (Array.isArray(config.body)) { + body = Buffer.concat(config.body); + } + + try { + response = await axiosRequest({ + url: config.url, + method: config.method, + headers: config.headers, + data: body, + responseType: 'arraybuffer', + maxRedirects: 10, + }); + } catch (err) { + response = err.response; + } + + return { + url: response.request.res.responseUrl, + method: response.request.method, + headers: response.headers, + body: [response.data], + statusCode: response.status, + statusMessage: response.statusText, + }; + }, +}; diff --git a/packages/insomnia-app/app/sync/git/http.js b/packages/insomnia-app/app/sync/git/http.js deleted file mode 100644 index dc6436a3c6..0000000000 --- a/packages/insomnia-app/app/sync/git/http.js +++ /dev/null @@ -1,36 +0,0 @@ -import { axiosRequest } from '../../network/axios-request'; - -/** - * This is an http plugin for isomorphic-git that uses our axios helper to make - * requests. - */ -export async function httpPlugin(config) { - let response; - - let body = null; - if (Array.isArray(config.body)) { - body = Buffer.concat(config.body); - } - - try { - response = await axiosRequest({ - url: config.url, - method: config.method, - headers: config.headers, - data: body, - responseType: 'arraybuffer', - maxRedirects: 10, - }); - } catch (err) { - response = err.response; - } - - return { - url: response.request.res.responseUrl, - method: response.request.method, - headers: response.headers, - body: [response.data], - statusCode: response.status, - statusMessage: response.statusText, - }; -} diff --git a/packages/insomnia-app/app/sync/git/mem-plugin.js b/packages/insomnia-app/app/sync/git/mem-client.js similarity index 99% rename from packages/insomnia-app/app/sync/git/mem-plugin.js rename to packages/insomnia-app/app/sync/git/mem-client.js index 6812a1b8f5..ea7db03fd1 100644 --- a/packages/insomnia-app/app/sync/git/mem-plugin.js +++ b/packages/insomnia-app/app/sync/git/mem-client.js @@ -31,13 +31,13 @@ type FSDir = {| type FSEntry = FSDir | FSFile | FSLink; -export class MemPlugin { +export class MemClient { __fs: FSEntry; __ino: 0; - static createPlugin() { + static createClient() { return { - promises: new MemPlugin(), + promises: new MemClient(), }; } diff --git a/packages/insomnia-app/app/sync/git/ne-db-plugin.js b/packages/insomnia-app/app/sync/git/ne-db-client.js similarity index 95% rename from packages/insomnia-app/app/sync/git/ne-db-plugin.js rename to packages/insomnia-app/app/sync/git/ne-db-client.js index 7b536bad4c..d7332c6859 100644 --- a/packages/insomnia-app/app/sync/git/ne-db-plugin.js +++ b/packages/insomnia-app/app/sync/git/ne-db-client.js @@ -7,19 +7,19 @@ import Stat from './stat'; import { GIT_INSOMNIA_DIR_NAME } from './git-vcs'; import parseGitPath from './parse-git-path'; -export default class NeDBPlugin { +export class NeDBClient { _workspaceId: string; constructor(workspaceId: string) { if (!workspaceId) { - throw new Error('Cannot use NeDBPlugin without workspace ID'); + throw new Error('Cannot use NeDBClient without workspace ID'); } this._workspaceId = workspaceId; } - static createPlugin(workspaceId: string) { + static createClient(workspaceId: string) { return { - promises: new NeDBPlugin(workspaceId), + promises: new NeDBClient(workspaceId), }; } @@ -141,7 +141,7 @@ export default class NeDBPlugin { } async mkdir(filePath: string, ...x: Array) { - throw new Error('NeDBPlugin is not writable'); + throw new Error('NeDBClient is not writable'); } async stat(filePath: string, ...x: Array): Promise { @@ -201,7 +201,7 @@ export default class NeDBPlugin { } async symlink(targetPath: string, filePath: string, ...x: Array): Promise { - throw new Error('NeDBPlugin symlink not supported'); + throw new Error('NeDBClient symlink not supported'); } _errMissing(filePath: string): Error { diff --git a/packages/insomnia-app/app/sync/git/routable-fs-plugin.js b/packages/insomnia-app/app/sync/git/routable-fs-client.js similarity index 84% rename from packages/insomnia-app/app/sync/git/routable-fs-plugin.js rename to packages/insomnia-app/app/sync/git/routable-fs-client.js index 30c57ad4b1..0e241c21b2 100644 --- a/packages/insomnia-app/app/sync/git/routable-fs-plugin.js +++ b/packages/insomnia-app/app/sync/git/routable-fs-client.js @@ -2,14 +2,13 @@ import path from 'path'; /** - * An isometric-git FS plugin that can route to various plugins depending on - * what the filePath is. + * An isometric-git FS client that can route to various client depending on what the filePath is. * - * @param defaultFS – default plugin - * @param otherFS – map of path prefixes to plugins + * @param defaultFS – default client + * @param otherFS – map of path prefixes to clients * @returns {{promises: *}} */ -export function routableFSPlugin(defaultFS: Object, otherFS: { [string]: Object }) { +export function routableFSClient(defaultFS: Object, otherFS: { [string]: Object }) { const execMethod = async (method: string, filePath: string, ...args: Array) => { filePath = path.normalize(filePath); diff --git a/packages/insomnia-app/app/sync/git/utils.js b/packages/insomnia-app/app/sync/git/utils.js new file mode 100644 index 0000000000..b87b778610 --- /dev/null +++ b/packages/insomnia-app/app/sync/git/utils.js @@ -0,0 +1,46 @@ +// @flow +export const translateSSHtoHTTP = (url: string) => { + // handle "shorter scp-like syntax" + url = url.replace(/^git@([^:]+):/, 'https://$1/'); + // handle proper SSH URLs + url = url.replace(/^ssh:\/\//, 'https://'); + return url; +}; + +export const addDotGit = ({ url }: { url: string }) => { + if (url.endsWith('.git')) { + return url; + } + + return `${url}.git`; +}; + +const onMessage = (message: string) => { + console.log(`[git-event] ${message}`); +}; + +const onAuthFailure = (message: string) => { + console.log(`[git-event] Auth Failure: ${message}`); +}; + +const onAuthSuccess = (message: string) => { + console.log(`[git-event] Auth Success: ${message}`); +}; + +const onAuth = ( + credentials?: { username: string, password?: string, token?: string } = {}, +) => () => ({ + username: credentials.username, + password: credentials.password || credentials.token, +}); + +export const gitCallbacks = (credentials?: { + username: string, + password?: string, + token?: string, +}) => ({ + onMessage, + onAuthFailure, + onAuthSuccess, + onAuth: onAuth(credentials), +}); diff --git a/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.js b/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.js index 8b82644530..3bb3a81ccc 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.js +++ b/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.js @@ -5,8 +5,7 @@ import { AUTOBIND_CFG } from '../../../common/constants'; import classnames from 'classnames'; import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown'; import type { Workspace } from '../../../models/workspace'; -import type { GitLogEntry } from '../../../sync/git/git-vcs'; -import GitVCS from '../../../sync/git/git-vcs'; +import type { GitVCS, GitLogEntry } from '../../../sync/git/git-vcs'; import { showAlert, showError, showModal } from '../modals'; import GitStagingModal from '../modals/git-staging-modal'; import * as db from '../../../common/database'; @@ -20,6 +19,7 @@ import Link from '../base/link'; import { trackEvent } from '../../../common/analytics'; import { docsGitSync } from '../../../common/documentation'; import { isNotNullOrUndefined } from '../../../common/misc'; +import { translateSSHtoHTTP } from '../../../sync/git/utils'; type Props = {| handleInitializeEntities: () => Promise, @@ -91,7 +91,7 @@ class GitSyncDropdown extends React.PureComponent { const log = (await vcs.log()) || []; this.setState({ ...(otherState || {}), log, branch, branches }); - const author = log[0] ? log[0].author : null; + const author = log[0] ? log[0].commit.author : null; const cachedGitRepositoryBranch = branch; const cachedGitLastAuthor = author ? author.name : null; @@ -163,7 +163,7 @@ class GitSyncDropdown extends React.PureComponent { try { await vcs.push(gitRepository.credentials, force); } catch (err) { - if (err.code === 'PushRejectedNonFastForward') { + if (err.code === 'PushRejectedError') { this._dropdown && this._dropdown.hide(); showAlert({ title: 'Push Rejected', @@ -192,6 +192,8 @@ class GitSyncDropdown extends React.PureComponent { const { workspace } = this.props; const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); + patch.uri = translateSSHtoHTTP(patch.uri); + if (gitRepository) { await models.gitRepository.update(gitRepository, patch); } else { diff --git a/packages/insomnia-app/app/ui/components/modals/git-branches-modal.js b/packages/insomnia-app/app/ui/components/modals/git-branches-modal.js index c0a1fc99a0..6987f6362b 100644 --- a/packages/insomnia-app/app/ui/components/modals/git-branches-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/git-branches-modal.js @@ -5,7 +5,7 @@ import { AUTOBIND_CFG } from '../../../common/constants'; import Modal from '../base/modal'; import ModalBody from '../base/modal-body'; import ModalHeader from '../base/modal-header'; -import GitVCS from '../../../sync/git/git-vcs'; +import { GitVCS } from '../../../sync/git/git-vcs'; import classnames from 'classnames'; import PromptButton from '../base/prompt-button'; import * as db from '../../../common/database'; diff --git a/packages/insomnia-app/app/ui/components/modals/git-log-modal.js b/packages/insomnia-app/app/ui/components/modals/git-log-modal.js index c0b7caae5d..e2dcecd190 100644 --- a/packages/insomnia-app/app/ui/components/modals/git-log-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/git-log-modal.js @@ -5,8 +5,7 @@ import { AUTOBIND_CFG } from '../../../common/constants'; import Modal from '../base/modal'; import ModalBody from '../base/modal-body'; import ModalHeader from '../base/modal-header'; -import type { GitLogEntry } from '../../../sync/git/git-vcs'; -import GitVCS from '../../../sync/git/git-vcs'; +import type { GitVCS, GitLogEntry } from '../../../sync/git/git-vcs'; import ModalFooter from '../base/modal-footer'; import Tooltip from '../tooltip'; import TimeFromNow from '../time-from-now'; @@ -16,7 +15,7 @@ type Props = {| |}; type State = {| - log: Array, + logs: Array, branch: string, |}; @@ -28,7 +27,7 @@ class GitLogModal extends React.PureComponent { super(props); this.state = { - log: [], + logs: [], branch: '??', }; } @@ -40,10 +39,10 @@ class GitLogModal extends React.PureComponent { async show() { const { vcs } = this.props; - const log = await vcs.log(); + const logs = await vcs.log(); const branch = await vcs.getBranch(); - this.setState({ log, branch }); + this.setState({ logs, branch }); this.modal && this.modal.show(); } @@ -53,7 +52,10 @@ class GitLogModal extends React.PureComponent { } renderLogEntryRow(entry: GitLogEntry) { - const { author, message, oid } = entry; + const { + commit: { author, message }, + oid, + } = entry; return ( @@ -75,11 +77,11 @@ class GitLogModal extends React.PureComponent { } render() { - const { log, branch } = this.state; + const { logs, branch } = this.state; return ( - Git History ({log.length}) + Git History ({logs.length}) @@ -89,7 +91,7 @@ class GitLogModal extends React.PureComponent { - {log.map(this.renderLogEntryRow)} + {logs.map(this.renderLogEntryRow)}
Author
diff --git a/packages/insomnia-app/app/ui/components/modals/git-repository-settings-modal.js b/packages/insomnia-app/app/ui/components/modals/git-repository-settings-modal.js index 0aae34f2c4..0cb3ba4c3a 100644 --- a/packages/insomnia-app/app/ui/components/modals/git-repository-settings-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/git-repository-settings-modal.js @@ -158,7 +158,6 @@ class GitRepositorySettingsModal extends React.PureComponent { { } if (!this.statusNames[gitPath] && log.length > 0) { - const docYML = await vcs.readObjFromTree(log[0].tree, gitPath); + const docYML = await vcs.readObjFromTree(log[0].commit.tree, gitPath); if (!docYML) { continue; } diff --git a/packages/insomnia-app/app/ui/components/wrapper-home.js b/packages/insomnia-app/app/ui/components/wrapper-home.js index ee8c6cfb9f..a437a8ab0b 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-home.js +++ b/packages/insomnia-app/app/ui/components/wrapper-home.js @@ -28,7 +28,7 @@ import DocumentCardDropdown from './dropdowns/document-card-dropdown'; import KeydownBinder from './keydown-binder'; import { executeHotKey } from '../../common/hotkeys-listener'; import { hotKeyRefs } from '../../common/hotkeys'; -import { showAlert, showError, showModal, showPrompt } from './modals'; +import { showAlert, showModal, showPrompt } from './modals'; import * as models from '../../models'; import { trackEvent, trackSegmentEvent } from '../../common/analytics'; import YAML from 'yaml'; @@ -48,7 +48,7 @@ import GitRepositorySettingsModal from '../components/modals/git-repository-sett import PageLayout from './page-layout'; import { ForceToWorkspaceKeys } from '../redux/modules/helpers'; import coreLogo from '../images/insomnia-core-logo.png'; -import { MemPlugin } from '../../sync/git/mem-plugin'; +import { MemClient } from '../../sync/git/mem-client'; import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, @@ -62,6 +62,8 @@ import AccountDropdown from './dropdowns/account-dropdown'; import { strings } from '../../common/strings'; import { WorkspaceScopeKeys } from '../../models/workspace'; import { descendingNumberSort } from '../../common/sorting'; +import { addDotGit, translateSSHtoHTTP, gitCallbacks } from '../../sync/git/utils'; +import { httpClient } from '../../sync/git/http-client'; type Props = {| wrapperProps: WrapperProps, @@ -159,34 +161,54 @@ class WrapperHome extends React.PureComponent { onSubmitEdits: async repoSettingsPatch => { trackEvent('Git', 'Clone'); - const core = Math.random() + ''; + let fsClient = MemClient.createClient(); - // Create in-memory filesystem to perform clone - const plugins = git.cores.create(core); - const fsPlugin = MemPlugin.createPlugin(); - plugins.set('fs', fsPlugin); + repoSettingsPatch.uri = translateSSHtoHTTP(repoSettingsPatch.uri); // Pull settings returned from dialog and shallow-clone the repo - const { credentials, uri: url } = repoSettingsPatch; + const cloneParams = { + ...gitCallbacks({ + username: repoSettingsPatch.credentials.username, + password: repoSettingsPatch.credentials.token, + }), + fs: fsClient, + http: httpClient, + dir: GIT_CLONE_DIR, + gitdir: GIT_INTERNAL_DIR, + singleBranch: true, + url: repoSettingsPatch.uri, + depth: 1, + }; try { - await git.clone({ - core, - dir: GIT_CLONE_DIR, - gitdir: GIT_INTERNAL_DIR, - singleBranch: true, - url, - ...credentials, - depth: 1, - noGitSuffix: true, - }); - } catch (err) { - showError({ title: 'Error Cloning Repository', message: err.message, error: err }); - return false; + await git.clone(cloneParams); + } catch (originalUrlError) { + if (cloneParams.url.endsWith('.git')) { + showAlert({ title: 'Error Cloning Repository', message: originalUrlError.message }); + return; + } + + const dotGitUrl = addDotGit(cloneParams); + try { + fsClient = MemClient.createClient(); + await git.clone({ + ...cloneParams, + fs: fsClient, + url: dotGitUrl, + }); + + // by this point the clone was successful, so update with this syntax + repoSettingsPatch.uri = dotGitUrl; + } catch (dotGitError) { + showAlert({ + title: 'Error Cloning Repository: failed to clone with and without `.git` suffix', + message: `Failed to clone with original url (${repoSettingsPatch.uri}): ${originalUrlError.message};\n\nAlso failed to clone with \`.git\` suffix added (${dotGitUrl}): ${dotGitError.message}`, + }); + return; + } } - const f = fsPlugin.promises; const ensureDir = async (base: string, name: string): Promise => { - const rootDirs = await f.readdir(base); + const rootDirs = await fsClient.promises.readdir(base); if (rootDirs.includes(name)) { return true; } @@ -216,31 +238,33 @@ class WrapperHome extends React.PureComponent { } const workspaceBase = path.join(GIT_INSOMNIA_DIR, models.workspace.type); - const workspaceDirs = await f.readdir(workspaceBase); + const workspaceDirs = await fsClient.promises.readdir(workspaceBase); if (workspaceDirs.length > 1) { - return showAlert({ + showAlert({ title: 'Clone Problem', message: 'Multiple workspaces found in repository', }); + return; } if (workspaceDirs.length === 0) { - return showAlert({ + showAlert({ title: 'Clone Problem', message: 'No workspaces found in repository', }); + return; } const workspacePath = path.join(workspaceBase, workspaceDirs[0]); - const workspaceJson = await f.readFile(workspacePath); + const workspaceJson = await fsClient.promises.readFile(workspacePath); const workspace = YAML.parse(workspaceJson.toString()); // Check if the workspace already exists const existingWorkspace = await models.workspace.getById(workspace._id); if (existingWorkspace) { - return showAlert({ + showAlert({ title: 'Clone Problem', okLabel: 'Done', message: ( @@ -250,6 +274,7 @@ class WrapperHome extends React.PureComponent { ), }); + return; } // Prompt user to confirm importing the workspace @@ -273,13 +298,13 @@ class WrapperHome extends React.PureComponent { const bufferId = await db.bufferChanges(); // Loop over all model folders in root - for (const modelType of await f.readdir(GIT_INSOMNIA_DIR)) { + for (const modelType of await fsClient.promises.readdir(GIT_INSOMNIA_DIR)) { const modelDir = path.join(GIT_INSOMNIA_DIR, modelType); // Loop over all documents in model folder and save them - for (const docFileName of await f.readdir(modelDir)) { + for (const docFileName of await fsClient.promises.readdir(modelDir)) { const docPath = path.join(modelDir, docFileName); - const docYaml = await f.readFile(docPath); + const docYaml = await fsClient.promises.readFile(docPath); const doc = YAML.parse(docYaml.toString()); await db.upsert(doc); } diff --git a/packages/insomnia-app/app/ui/components/wrapper.js b/packages/insomnia-app/app/ui/components/wrapper.js index 4058d02941..f7f330aa5d 100644 --- a/packages/insomnia-app/app/ui/components/wrapper.js +++ b/packages/insomnia-app/app/ui/components/wrapper.js @@ -75,7 +75,7 @@ import type { StatusCandidate } from '../../sync/types'; import type { RequestMeta } from '../../models/request-meta'; import type { RequestVersion } from '../../models/request-version'; import type { ApiSpec } from '../../models/api-spec'; -import GitVCS from '../../sync/git/git-vcs'; +import { GitVCS } from '../../sync/git/git-vcs'; import { trackPageView } from '../../common/analytics'; import type { GitRepository } from '../../models/git-repository'; import WrapperHome from './wrapper-home'; diff --git a/packages/insomnia-app/app/ui/containers/app.js b/packages/insomnia-app/app/ui/containers/app.js index 94ab90c789..17220090c3 100644 --- a/packages/insomnia-app/app/ui/containers/app.js +++ b/packages/insomnia-app/app/ui/containers/app.js @@ -90,10 +90,10 @@ import ExportRequestsModal from '../components/modals/export-requests-modal'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import VCS from '../../sync/vcs'; import SyncMergeModal from '../components/modals/sync-merge-modal'; -import GitVCS, { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INTERNAL_DIR } from '../../sync/git/git-vcs'; -import NeDBPlugin from '../../sync/git/ne-db-plugin'; -import FSPlugin from '../../sync/git/fs-plugin'; -import { routableFSPlugin } from '../../sync/git/routable-fs-plugin'; +import { GitVCS, GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INTERNAL_DIR } from '../../sync/git/git-vcs'; +import { NeDBClient } from '../../sync/git/ne-db-client'; +import { fsClient } from '../../sync/git/fs-client'; +import { routableFSClient } from '../../sync/git/routable-fs-client'; import { getWorkspaceLabel } from '../../common/get-workspace-label'; import { isCollection, @@ -1017,7 +1017,6 @@ class App extends PureComponent { async _updateGitVCS() { const { activeGitRepository, activeWorkspace } = this.props; - // Get the vcs and set it to null in the state while we update it let gitVCS = this.state.gitVCS; this.setState({ gitVCS: null }); @@ -1027,37 +1026,44 @@ class App extends PureComponent { } if (activeGitRepository) { - // Create FS plugin + // Create FS client const baseDir = path.join( getDataDirectory(), `version-control/git/${activeGitRepository._id}`, ); - const pNeDb = NeDBPlugin.createPlugin(activeWorkspace._id); - const pGitData = FSPlugin.createPlugin(baseDir); - const pOtherData = FSPlugin.createPlugin(path.join(baseDir, 'other')); - const fsPlugin = routableFSPlugin( - // All data outside the directories listed below will be stored in an 'other' - // directory. This is so we can support files that exist outside the ones - // the app is specifically in charge of. - pOtherData, - { - // All app data is stored within the a namespaced directory at the root of the - // repository and is read/written from the local NeDB database - [GIT_INSOMNIA_DIR]: pNeDb, + /** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */ + const neDbClient = NeDBClient.createClient(activeWorkspace._id); - // All git metadata is stored in a git/ directory on the filesystem - [GIT_INTERNAL_DIR]: pGitData, - }, - ); + /** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */ + const gitDataClient = fsClient(baseDir); + + /** All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of. */ + const otherDatClient = fsClient(path.join(baseDir, 'other')); + + /** The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations. */ + const routableFS = routableFSClient(otherDatClient, { + [GIT_INSOMNIA_DIR]: neDbClient, + [GIT_INTERNAL_DIR]: gitDataClient, + }); // Init VCS + const { credentials, uri } = activeGitRepository; if (activeGitRepository.needsFullClone) { await models.gitRepository.update(activeGitRepository, { needsFullClone: false }); - const { credentials, uri } = activeGitRepository; - await gitVCS.initFromClone(uri, credentials, GIT_CLONE_DIR, fsPlugin, GIT_INTERNAL_DIR); + await gitVCS.initFromClone({ + url: uri, + gitCredentials: credentials, + directory: GIT_CLONE_DIR, + fs: routableFS, + gitDirectory: GIT_INTERNAL_DIR, + }); } else { - await gitVCS.init(GIT_CLONE_DIR, fsPlugin, GIT_INTERNAL_DIR); + await gitVCS.init({ + directory: GIT_CLONE_DIR, + fs: routableFS, + gitDirectory: GIT_INTERNAL_DIR, + }); } // Configure basic info diff --git a/packages/insomnia-app/package-lock.json b/packages/insomnia-app/package-lock.json index a2878a2233..b55aca3503 100644 --- a/packages/insomnia-app/package-lock.json +++ b/packages/insomnia-app/package-lock.json @@ -5200,9 +5200,9 @@ "dev": true }, "async-lock": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.2.4.tgz", - "integrity": "sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.2.8.tgz", + "integrity": "sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ==" }, "asynckit": { "version": "0.4.0", @@ -5874,11 +5874,6 @@ } } }, - "base64-js": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", - "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=" - }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6136,15 +6131,6 @@ "hoek": "4.x.x" } }, - "bops": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.7.tgz", - "integrity": "sha1-tKClqDmkBkVK8P4FqLkaenZqVOI=", - "requires": { - "base64-js": "0.0.2", - "to-utf8": "0.0.1" - } - }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -11681,15 +11667,6 @@ "assert-plus": "^1.0.0" } }, - "git-apply-delta": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/git-apply-delta/-/git-apply-delta-0.0.7.tgz", - "integrity": "sha1-+3auFEVA15RAtSsx3gPmPJk8chk=", - "requires": { - "bops": "~0.0.6", - "varint": "0.0.3" - } - }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -11865,11 +11842,6 @@ "define-properties": "^1.1.3" } }, - "globalyzer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.4.tgz", - "integrity": "sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==" - }, "globby": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", @@ -12049,11 +12021,6 @@ } } }, - "globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" - }, "good-listener": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", @@ -13767,19 +13734,15 @@ } }, "isomorphic-git": { - "version": "0.70.9", - "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-0.70.9.tgz", - "integrity": "sha512-Ds7uRPBoUmdRMT8eAd5bVXQkg3wU4N4XhwADcq58aPK46gw1DeAYVMOiu/TZafvVN4sDPhFd3AeBzySOaBuYmQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.8.1.tgz", + "integrity": "sha512-4xs3yzHrEGAWdlWbO/iAc7SbIvWopSydPe1gUfwgpzGeU+ofs1JbvuOzCBIkHec0r3pWZv/u8NL808Rq7xTa7Q==", "requires": { "async-lock": "^1.1.0", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", - "git-apply-delta": "0.0.7", - "globalyzer": "^0.1.4", - "globrex": "^0.1.2", "ignore": "^5.1.4", - "marky": "^1.2.1", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", @@ -16706,7 +16669,8 @@ "marky": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.1.tgz", - "integrity": "sha512-md9k+Gxa3qLH6sUKpeC2CNkJK/Ld+bEz5X96nYwloqphQE0CKCVEKco/6jxEZixinqNdz5RFi/KaCyfbMDMAXQ==" + "integrity": "sha512-md9k+Gxa3qLH6sUKpeC2CNkJK/Ld+bEz5X96nYwloqphQE0CKCVEKco/6jxEZixinqNdz5RFi/KaCyfbMDMAXQ==", + "dev": true }, "matcher": { "version": "3.0.0", @@ -23269,11 +23233,6 @@ "is-number": "^7.0.0" } }, - "to-utf8": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", - "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" - }, "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -24062,11 +24021,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "varint": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/varint/-/varint-0.0.3.tgz", - "integrity": "sha1-uCHemwSzizzSL3LBjZSp+3KrNRg=" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/packages/insomnia-app/package.json b/packages/insomnia-app/package.json index b36ca434b1..b4f8012429 100644 --- a/packages/insomnia-app/package.json +++ b/packages/insomnia-app/package.json @@ -143,7 +143,7 @@ "insomnia-testing": "^2.2.29", "insomnia-url": "^2.2.24", "insomnia-xpath": "^2.2.24", - "isomorphic-git": "^0.70.3", + "isomorphic-git": "^1.8.1", "js-yaml": "^3.14.1", "jshint": "^2.11.1", "json-order": "^1.1.0",