From 5cf2f59e4641bc01bfbf63a64c063a9639d986cb Mon Sep 17 00:00:00 2001 From: Opender Singh Date: Fri, 29 May 2020 06:13:13 +1200 Subject: [PATCH] Git clone fails on Windows. This fixes it. (#2194) Co-authored-by: Gregory Schier --- .../app/sync/git/__mocks__/path.js | 12 ++ .../app/sync/git/__tests__/git-vcs.test.js | 100 +++++++------- .../app/sync/git/__tests__/mem-plugin.test.js | 31 +++-- .../sync/git/__tests__/ne-db-plugin.test.js | 122 ++++++++++++++++-- .../git/__tests__/routable-fs-plugin.test.js | 9 +- packages/insomnia-app/app/sync/git/git-vcs.js | 12 +- .../insomnia-app/app/sync/git/mem-plugin.js | 4 +- .../insomnia-app/app/sync/git/ne-db-plugin.js | 22 +++- .../modals/git-repository-settings-modal.js | 1 + .../ui/components/modals/git-staging-modal.js | 13 +- .../app/ui/components/wrapper-home.js | 25 ++-- .../insomnia-app/app/ui/containers/app.js | 11 +- 12 files changed, 258 insertions(+), 104 deletions(-) create mode 100644 packages/insomnia-app/app/sync/git/__mocks__/path.js diff --git a/packages/insomnia-app/app/sync/git/__mocks__/path.js b/packages/insomnia-app/app/sync/git/__mocks__/path.js new file mode 100644 index 0000000000..ab0248c410 --- /dev/null +++ b/packages/insomnia-app/app/sync/git/__mocks__/path.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line filenames/match-exported +const path = jest.requireActual('path'); + +const exportObj = { __mockPath, ...path }; + +function __mockPath(type) { + const mock = type === 'win32' ? path.win32 : path.posix; + + Object.keys(mock).forEach(k => (exportObj[k] = mock[k])); +} + +module.exports = exportObj; 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 cc9b7004d0..d565cc10fa 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,8 +1,12 @@ -import GitVCS, { GIT_NAMESPACE_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 path from 'path'; +jest.mock('path'); -describe('Git-VCS', () => { +describe.each(['win32', 'posix'])('Git-VCS using path.%s', type => { + beforeAll(() => path.__mockPath(type)); + afterAll(() => jest.restoreAllMocks()); beforeEach(setupDateMocks); describe('common operations', () => { @@ -10,7 +14,7 @@ describe('Git-VCS', () => { const fs = MemPlugin.createPlugin(); const vcs = new GitVCS(); - await vcs.init('/', fs); + await vcs.init(GIT_CLONE_DIR, fs); await vcs.setAuthor('Karen Brown', 'karen@example.com'); // No files exist yet @@ -24,33 +28,33 @@ describe('Git-VCS', () => { it('stage and unstage file', async () => { const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(`/${GIT_NAMESPACE_DIR}`); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/foo.txt`, 'foo'); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/bar.txt`, 'bar'); + await fs.promises.mkdir(GIT_INSOMNIA_DIR); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/foo.txt`, 'foo'); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/bar.txt`, 'bar'); // Files outside namespace should be ignored await fs.promises.writeFile('/other.txt', 'other'); const vcs = new GitVCS(); - await vcs.init('/', fs); + await vcs.init(GIT_CLONE_DIR, fs); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('*added'); - await vcs.add(`${GIT_NAMESPACE_DIR}/foo.txt`); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('added'); + await vcs.add(`${GIT_INSOMNIA_DIR}/foo.txt`); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('added'); - await vcs.remove(`${GIT_NAMESPACE_DIR}/foo.txt`); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('*added'); + await vcs.remove(`${GIT_INSOMNIA_DIR}/foo.txt`); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('*added'); }); it('Returns empty log without first commit', async () => { const fs = MemPlugin.createPlugin(); const vcs = new GitVCS(); - await vcs.init('/', fs); + await vcs.init(GIT_CLONE_DIR, fs); await vcs.setAuthor('Karen Brown', 'karen@example.com'); expect(await vcs.log()).toEqual([]); @@ -58,20 +62,20 @@ describe('Git-VCS', () => { it('commit file', async () => { const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(`/${GIT_NAMESPACE_DIR}`); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/foo.txt`, 'foo'); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/bar.txt`, 'bar'); + await fs.promises.mkdir(`${GIT_INSOMNIA_DIR}`); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/foo.txt`, 'foo'); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/bar.txt`, 'bar'); await fs.promises.writeFile('/other.txt', 'should be ignored'); const vcs = new GitVCS(); - await vcs.init('/', fs); + await vcs.init(GIT_CLONE_DIR, fs); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - await vcs.add(`${GIT_NAMESPACE_DIR}/foo.txt`); + await vcs.add(`${GIT_INSOMNIA_DIR}/foo.txt`); await vcs.commit('First commit!'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('unmodified'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('unmodified'); expect(await vcs.log()).toEqual([ { @@ -94,36 +98,36 @@ describe('Git-VCS', () => { }, ]); - await fs.promises.unlink(`/${GIT_NAMESPACE_DIR}/foo.txt`); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('*deleted'); + await fs.promises.unlink(`${GIT_INSOMNIA_DIR}/foo.txt`); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('*deleted'); - await vcs.remove(`${GIT_NAMESPACE_DIR}/foo.txt`); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('deleted'); + await vcs.remove(`${GIT_INSOMNIA_DIR}/foo.txt`); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('deleted'); - await vcs.remove(`${GIT_NAMESPACE_DIR}/foo.txt`); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/bar.txt`)).toBe('*added'); - expect(await vcs.status(`${GIT_NAMESPACE_DIR}/foo.txt`)).toBe('deleted'); + await vcs.remove(`${GIT_INSOMNIA_DIR}/foo.txt`); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/bar.txt`)).toBe('*added'); + expect(await vcs.status(`${GIT_INSOMNIA_DIR}/foo.txt`)).toBe('deleted'); }); it('create branch', async () => { const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(`/${GIT_NAMESPACE_DIR}`); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/foo.txt`, 'foo'); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/bar.txt`, 'bar'); + await fs.promises.mkdir(`${GIT_INSOMNIA_DIR}`); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/foo.txt`, 'foo'); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/bar.txt`, 'bar'); const vcs = new GitVCS(); - await vcs.init('/', fs); + await vcs.init(GIT_CLONE_DIR, fs); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - await vcs.add(`${GIT_NAMESPACE_DIR}/foo.txt`); + await vcs.add(`${GIT_INSOMNIA_DIR}/foo.txt`); await vcs.commit('First commit!'); expect((await vcs.log()).length).toBe(1); await vcs.checkout('new-branch'); expect((await vcs.log()).length).toBe(1); - await vcs.add(`${GIT_NAMESPACE_DIR}/bar.txt`); + await vcs.add(`${GIT_INSOMNIA_DIR}/bar.txt`); await vcs.commit('Second commit!'); expect((await vcs.log()).length).toBe(2); @@ -135,28 +139,26 @@ describe('Git-VCS', () => { describe('readObjectFromTree()', () => { it('reads an object from tree', async () => { const fs = MemPlugin.createPlugin(); - await fs.promises.mkdir(`/${GIT_NAMESPACE_DIR}`); - await fs.promises.mkdir(`/${GIT_NAMESPACE_DIR}/dir`); - await fs.promises.writeFile(`/${GIT_NAMESPACE_DIR}/dir/foo.txt`, 'foo'); + await fs.promises.mkdir(`${GIT_INSOMNIA_DIR}`); + await fs.promises.mkdir(`${GIT_INSOMNIA_DIR}/dir`); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/dir/foo.txt`, 'foo'); const vcs = new GitVCS(); - await vcs.init('/', fs); + await vcs.init(GIT_CLONE_DIR, fs); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - await vcs.add(`${GIT_NAMESPACE_DIR}/dir/foo.txt`); + await vcs.add(`${GIT_INSOMNIA_DIR}/dir/foo.txt`); await vcs.commit('First'); - await fs.promises.writeFile(`${GIT_NAMESPACE_DIR}//dir/foo.txt`, 'foo bar'); - await vcs.add(`${GIT_NAMESPACE_DIR}/dir/foo.txt`); + await fs.promises.writeFile(`${GIT_INSOMNIA_DIR}/dir/foo.txt`, 'foo bar'); + await vcs.add(`${GIT_INSOMNIA_DIR}/dir/foo.txt`); await vcs.commit('Second'); const log = await vcs.log(); - expect(await vcs.readObjFromTree(log[0].tree, `${GIT_NAMESPACE_DIR}/dir/foo.txt`)).toBe( + expect(await vcs.readObjFromTree(log[0].tree, `${GIT_INSOMNIA_DIR}/dir/foo.txt`)).toBe( 'foo bar', ); - expect(await vcs.readObjFromTree(log[1].tree, `${GIT_NAMESPACE_DIR}/dir/foo.txt`)).toBe( - 'foo', - ); + expect(await vcs.readObjFromTree(log[1].tree, `${GIT_INSOMNIA_DIR}/dir/foo.txt`)).toBe('foo'); // Some extra checks expect(await vcs.readObjFromTree(log[1].tree, 'missing')).toBe(null); 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 index 49b8a3f6c3..6a7f495f17 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/mem-plugin.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/mem-plugin.test.js @@ -1,7 +1,12 @@ import { assertAsyncError, setupDateMocks } from './util'; import { MemPlugin } from '../mem-plugin'; +import path from 'path'; +import { GIT_CLONE_DIR } from '../git-vcs'; +jest.mock('path'); -describe('MemPlugin', () => { +describe.each(['win32', 'posix'])('Memlugin using path.%s', type => { + beforeAll(() => path.__mockPath(type)); + afterAll(() => jest.restoreAllMocks()); beforeEach(setupDateMocks); describe('readfile()', () => { @@ -90,12 +95,12 @@ describe('MemPlugin', () => { const p = new MemPlugin(); // Root dir should always exist - expect(await p.readdir('/')).toEqual([]); + expect(await p.readdir(GIT_CLONE_DIR)).toEqual([]); // Write a file and list it again await p.writeFile('/foo.txt', 'Hello World!'); await p.writeFile('/bar.txt', 'Bar!'); - expect(await p.readdir('/')).toEqual(['bar.txt', 'foo.txt']); + expect(await p.readdir(GIT_CLONE_DIR)).toEqual(['bar.txt', 'foo.txt']); }); it('errors on file', async () => { @@ -117,21 +122,29 @@ describe('MemPlugin', () => { await p.mkdir('/foo'); await p.mkdir('/foo/bar'); - expect(await p.readdir('/')).toEqual(['foo']); - expect(await p.readdir('/foo')).toEqual(['bar']); + expect(await p.readdir(GIT_CLONE_DIR)).toEqual(['foo']); + expect(await p.readdir(`${GIT_CLONE_DIR}/foo`)).toEqual(['bar']); + }); + + it('creates directory non-recursively', async () => { + const p = new MemPlugin(); + + await p.mkdir(`${GIT_CLONE_DIR}/foo`, { recursive: true }); + await p.mkdir(`${GIT_CLONE_DIR}/foo/bar`); + expect(await p.readdir(`${GIT_CLONE_DIR}/foo/bar`)).toEqual([]); }); it('creates directory recursively', async () => { const p = new MemPlugin(); - await p.mkdir('/foo/bar/baz', { recursive: true }); - expect(await p.readdir('/foo/bar/baz')).toEqual([]); + await p.mkdir(`${GIT_CLONE_DIR}/foo/bar/baz`, { recursive: true }); + expect(await p.readdir(`${GIT_CLONE_DIR}/foo/bar/baz`)).toEqual([]); }); it('fails to create if no parent', async () => { const p = new MemPlugin(); - await assertAsyncError(p.mkdir('/foo/bar/baz'), 'ENOENT'); + await assertAsyncError(p.mkdir(`${GIT_CLONE_DIR}/foo/bar/baz`), 'ENOENT'); }); }); @@ -167,7 +180,7 @@ describe('MemPlugin', () => { it('stats root dir', async () => { const p = new MemPlugin(); - const stat = await p.stat('/'); + const stat = await p.stat(GIT_CLONE_DIR); expect(stat).toEqual({ ctimeMs: 1000000000000, 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-plugin.test.js index 21c8d4fdb6..d3c4e3d205 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/ne-db-plugin.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/ne-db-plugin.test.js @@ -1,11 +1,18 @@ +// @flow import YAML from 'yaml'; 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 { GIT_NAMESPACE_DIR } from '../git-vcs'; +import path from 'path'; +import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../git-vcs'; +jest.mock('path'); + +describe.each(['win32', 'posix'])('NeDBPlugin using path.%s', type => { + beforeAll(() => path.__mockPath(type)); + afterAll(() => jest.restoreAllMocks()); -describe('NeDBPlugin', () => { beforeEach(async () => { await globalBeforeEach(); @@ -24,19 +31,19 @@ describe('NeDBPlugin', () => { it('reads model IDs from model type folders', async () => { const pNeDB = new NeDBPlugin('wrk_1'); - expect(await pNeDB.readdir('/')).toEqual([GIT_NAMESPACE_DIR]); - expect(await pNeDB.readdir(`/${GIT_NAMESPACE_DIR}`)).toEqual([ + expect(await pNeDB.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]); + expect(await pNeDB.readdir(GIT_INSOMNIA_DIR)).toEqual([ 'ApiSpec', 'Environment', 'Request', 'RequestGroup', 'Workspace', ]); - expect(await pNeDB.readdir(`/${GIT_NAMESPACE_DIR}/Request`)).toEqual([ + expect(await pNeDB.readdir(`${GIT_INSOMNIA_DIR}/Request`)).toEqual([ 'req_1.yml', 'req_2.yml', ]); - expect(await pNeDB.readdir(`/${GIT_NAMESPACE_DIR}/Workspace`)).toEqual(['wrk_1.yml']); + expect(await pNeDB.readdir(`${GIT_INSOMNIA_DIR}/Workspace`)).toEqual(['wrk_1.yml']); }); }); @@ -45,14 +52,14 @@ describe('NeDBPlugin', () => { const pNeDB = new NeDBPlugin('wrk_1'); expect( - YAML.parse(await pNeDB.readFile(`/${GIT_NAMESPACE_DIR}/Workspace/wrk_1.yml`, 'utf8')), + YAML.parse(await pNeDB.readFile(`${GIT_INSOMNIA_DIR}/Workspace/wrk_1.yml`, 'utf8')), ).toEqual(expect.objectContaining({ _id: 'wrk_1', parentId: null })); expect( - YAML.parse(await pNeDB.readFile(`/${GIT_NAMESPACE_DIR}/Request/req_1.yml`, 'utf8')), + YAML.parse(await pNeDB.readFile(`${GIT_INSOMNIA_DIR}/Request/req_1.yml`, 'utf8')), ).toEqual(expect.objectContaining({ _id: 'req_1', parentId: 'wrk_1' })); - await assertAsyncError(pNeDB.readFile(`/${GIT_NAMESPACE_DIR}/Request/req_x.yml`)); + await assertAsyncError(pNeDB.readFile(`${GIT_INSOMNIA_DIR}/Request/req_x.yml`)); }); }); @@ -60,19 +67,106 @@ describe('NeDBPlugin', () => { it('stats a dir', async () => { const pNeDB = new NeDBPlugin('wrk_1'); - expect(await pNeDB.stat('/')).toEqual(expect.objectContaining({ type: 'dir' })); - expect(await pNeDB.stat(`/${GIT_NAMESPACE_DIR}`)).toEqual( + expect(await pNeDB.stat(GIT_CLONE_DIR)).toEqual(expect.objectContaining({ type: 'dir' })); + expect(await pNeDB.stat(`${GIT_INSOMNIA_DIR}`)).toEqual( expect.objectContaining({ type: 'dir' }), ); - expect(await pNeDB.stat(`/${GIT_NAMESPACE_DIR}/Workspace/wrk_1.yml`)).toEqual( + expect(await pNeDB.stat(`${GIT_INSOMNIA_DIR}/Workspace/wrk_1.yml`)).toEqual( expect.objectContaining({ type: 'file' }), ); - expect(await pNeDB.stat(`/${GIT_NAMESPACE_DIR}/Request`)).toEqual( + expect(await pNeDB.stat(`${GIT_INSOMNIA_DIR}/Request`)).toEqual( expect.objectContaining({ type: 'dir' }), ); - expect(await pNeDB.stat(`/${GIT_NAMESPACE_DIR}/Request/req_2.yml`)).toEqual( + expect(await pNeDB.stat(`${GIT_INSOMNIA_DIR}/Request/req_2.yml`)).toEqual( expect.objectContaining({ type: 'file' }), ); }); }); + + describe('writeFile()', () => { + it('should ignore files not in GIT_INSOMNIA_DIR directory', async () => { + // Assemble + const upsertSpy = jest.spyOn(db, 'upsert'); + const workspaceId = 'wrk_1'; + const pNeDB = new NeDBPlugin(workspaceId); + + const env = { _id: 'env_1', type: 'Environment', parentId: workspaceId }; + const filePath = `anotherDir/${env.type}/${env._id}.yml`; + + // Act + await pNeDB.writeFile(filePath, YAML.stringify(env)); + + // Assert + expect(upsertSpy).not.toBeCalled(); + + // Cleanup + upsertSpy.mockRestore(); + }); + + it('should write files in GIT_INSOMNIA_DIR directory to db', async () => { + // Assemble + const workspaceId = 'wrk_1'; + const pNeDB = new NeDBPlugin(workspaceId); + const upsertSpy = jest.spyOn(db, 'upsert'); + + const env = { _id: 'env_1', type: 'Environment', parentId: workspaceId }; + const filePath = `${GIT_INSOMNIA_DIR}/${env.type}/${env._id}.yml`; + + // Act + await pNeDB.writeFile(filePath, YAML.stringify(env)); + + // Assert + expect(upsertSpy).toHaveBeenCalledTimes(1); + expect(upsertSpy).toHaveBeenCalledWith(env, true); + + // Cleanup + upsertSpy.mockRestore(); + }); + + it('should throw error if id does not match', async () => { + // Assemble + const workspaceId = 'wrk_1'; + const pNeDB = new NeDBPlugin(workspaceId); + + const env = { _id: 'env_1', type: 'Environment', parentId: workspaceId }; + const filePath = `${GIT_INSOMNIA_DIR}/${env.type}/env_2.yml`; + + // Act + const promiseResult = pNeDB.writeFile(filePath, YAML.stringify(env)); + + // Assert + await expect(promiseResult).rejects.toThrowError( + `Doc _id does not match file path [env_1 != env_2]`, + ); + }); + + it('should throw error if type does not match', async () => { + // Assemble + const workspaceId = 'wrk_1'; + const pNeDB = new NeDBPlugin(workspaceId); + + const env = { _id: 'env_1', type: 'Environment', parentId: workspaceId }; + const filePath = `${GIT_INSOMNIA_DIR}/Request/${env._id}.yml`; + + // Act + const promiseResult = pNeDB.writeFile(filePath, YAML.stringify(env)); + + // Assert + await expect(promiseResult).rejects.toThrowError( + `Doc type does not match file path [Environment != Request]`, + ); + }); + }); + + describe('mkdir()', () => { + it('should throw error', async () => { + const workspaceId = 'wrk_1'; + const pNeDB = new NeDBPlugin(workspaceId); + + const promiseResult = pNeDB.mkdir('', ''); + + // Assert + await expect(promiseResult).rejects.toThrowError('NeDBPlugin is not writable'); + }); + }); }); 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 index f12d95265a..808d6b59f7 100644 --- 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 @@ -1,7 +1,12 @@ import { MemPlugin } from '../mem-plugin'; import { routableFSPlugin } from '../routable-fs-plugin'; +import { GIT_CLONE_DIR } from '../git-vcs'; +import path from 'path'; +jest.mock('path'); -describe('routableFSPlugin', () => { +describe.each(['win32', 'posix'])('routableFSPlugin using path.%s', type => { + beforeAll(() => path.__mockPath(type)); + afterAll(() => jest.restoreAllMocks()); it('routes .git and other files to separate places', async () => { const pGit = MemPlugin.createPlugin(); const pDir = MemPlugin.createPlugin(); @@ -18,7 +23,7 @@ describe('routableFSPlugin', () => { 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('/')).toEqual(['other']); + 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/git-vcs.js b/packages/insomnia-app/app/sync/git/git-vcs.js index 454acbd3f9..dd28252a52 100644 --- a/packages/insomnia-app/app/sync/git/git-vcs.js +++ b/packages/insomnia-app/app/sync/git/git-vcs.js @@ -2,6 +2,7 @@ import * as git from 'isomorphic-git'; import { trackEvent } from '../../common/analytics'; import { httpPlugin } from './http'; +import path from 'path'; export type GitAuthor = {| name: string, @@ -34,7 +35,16 @@ export type GitLogEntry = {| }, |}; -export const GIT_NAMESPACE_DIR = '.insomnia'; +// 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. +export const GIT_CLONE_DIR = '.'; +const _gitInternalDirName = 'git'; +export const GIT_INSOMNIA_DIR_NAME = '.insomnia'; + +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; diff --git a/packages/insomnia-app/app/sync/git/mem-plugin.js b/packages/insomnia-app/app/sync/git/mem-plugin.js index 6708bba54a..6812a1b8f5 100644 --- a/packages/insomnia-app/app/sync/git/mem-plugin.js +++ b/packages/insomnia-app/app/sync/git/mem-plugin.js @@ -278,7 +278,9 @@ export class MemPlugin { filePath = path.normalize(filePath); let current = this.__fs; - const pathSegments = filePath.split(path.sep).filter(s => s !== ''); + + // Ignore empty and current directory '.' segments + const pathSegments = filePath.split(path.sep).filter(s => s !== '' && s !== '.'); for (const expectedName of pathSegments) { const e = (current.children || []).find(c => c.name === expectedName); diff --git a/packages/insomnia-app/app/sync/git/ne-db-plugin.js b/packages/insomnia-app/app/sync/git/ne-db-plugin.js index 13760d5e23..c6b5e80643 100644 --- a/packages/insomnia-app/app/sync/git/ne-db-plugin.js +++ b/packages/insomnia-app/app/sync/git/ne-db-plugin.js @@ -4,16 +4,21 @@ import * as db from '../../common/database'; import * as models from '../../models'; import YAML from 'yaml'; import Stat from './stat'; -import { GIT_NAMESPACE_DIR } from './git-vcs'; +import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR_NAME } from './git-vcs'; export default class NeDBPlugin { _workspaceId: string; + _cloneDirRegExp: RegExp; constructor(workspaceId: string) { if (!workspaceId) { throw new Error('Cannot use NeDBPlugin without workspace ID'); } this._workspaceId = workspaceId; + + // The win32 separator is a single backslash (\), but we have to escape both the JS string and RegExp. + const pathSep = path.sep === path.win32.sep ? '\\\\' : '/'; + this._cloneDirRegExp = new RegExp(`^${GIT_CLONE_DIR}${pathSep}`); } static createPlugin(workspaceId: string) { @@ -69,7 +74,7 @@ export default class NeDBPlugin { filePath = path.normalize(filePath); const { root, id, type } = this._parsePath(filePath); - if (root !== GIT_NAMESPACE_DIR) { + if (root !== GIT_INSOMNIA_DIR_NAME) { console.log(`[git] Ignoring external file ${filePath}`); return; } @@ -77,11 +82,11 @@ export default class NeDBPlugin { const doc = YAML.parse(data.toString()); if (id !== doc._id) { - throw new Error(`Doc _id does not match file path ${doc._id} != ${id || 'null'}`); + throw new Error(`Doc _id does not match file path [${doc._id} != ${id || 'null'}]`); } if (type !== doc.type) { - throw new Error(`Doc type does not match file path ${doc.type} != ${type || 'null'}`); + throw new Error(`Doc type does not match file path [${doc.type} != ${type || 'null'}]`); } await db.upsert(doc, true); @@ -112,7 +117,7 @@ export default class NeDBPlugin { let docs = []; let otherFolders = []; if (root === null && id === null && type === null) { - otherFolders = [GIT_NAMESPACE_DIR]; + otherFolders = [GIT_INSOMNIA_DIR_NAME]; } else if (id === null && type === null) { otherFolders = [ models.workspace.type, @@ -201,7 +206,12 @@ export default class NeDBPlugin { _parsePath(filePath: string): { root: string | null, type: string | null, id: string | null } { filePath = path.normalize(filePath); - const [root, type, idRaw] = filePath.split(path.sep).filter(s => s !== ''); + // FilePath will start with the clone directory. We want to remove the clone dir, so that the + // segments can be extracted correctly. + filePath = filePath.replace(this._cloneDirRegExp, ''); + + // Ignore empty and current directory '.' segments + const [root, type, idRaw] = filePath.split(path.sep).filter(s => s !== '' && s !== '.'); const id = typeof idRaw === 'string' ? idRaw.replace(/\.(json|yml)$/, '') : idRaw; 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 a186e647c6..60131de5b3 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 @@ -114,6 +114,7 @@ class GitRepositorySettingsModal extends React.PureComponent { inputs.token = typeof creds.token === 'string' ? creds.token : ''; inputs.authorEmail = gitRepository.author.email; inputs.authorName = gitRepository.author.name; + inputs.username = gitRepository.credentials?.username || ''; inputs.uri = gitRepository.uri; } diff --git a/packages/insomnia-app/app/ui/components/modals/git-staging-modal.js b/packages/insomnia-app/app/ui/components/modals/git-staging-modal.js index dcfc1740e2..35c44a7123 100644 --- a/packages/insomnia-app/app/ui/components/modals/git-staging-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/git-staging-modal.js @@ -8,7 +8,7 @@ import Modal from '../base/modal'; import ModalBody from '../base/modal-body'; import ModalHeader from '../base/modal-header'; import type { Workspace } from '../../../models/workspace'; -import GitVCS, { GIT_NAMESPACE_DIR } from '../../../sync/git/git-vcs'; +import GitVCS, { GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../../../sync/git/git-vcs'; import { withDescendants } from '../../../common/database'; import IndeterminateCheckbox from '../base/indeterminate-checkbox'; import ModalFooter from '../base/modal-footer'; @@ -129,15 +129,14 @@ class GitStagingModal extends React.PureComponent { const { vcs } = this.props; const f = vcs.getFs().promises; - const rootDir = path.join('/', GIT_NAMESPACE_DIR); const fsPaths = []; - for (const type of await f.readdir(rootDir)) { - const typeDir = path.join(rootDir, type); + for (const type of await f.readdir(GIT_INSOMNIA_DIR)) { + const typeDir = path.join(GIT_INSOMNIA_DIR, type); for (const name of await f.readdir(typeDir)) { // NOTE: git paths don't start with '/' so we're omitting // it here too. - const gitPath = path.join(`${GIT_NAMESPACE_DIR}/`, type, name); + const gitPath = path.join(GIT_INSOMNIA_DIR_NAME, type, name); fsPaths.push(path.join(gitPath)); } } @@ -167,9 +166,9 @@ class GitStagingModal extends React.PureComponent { this.statusNames = {}; for (const doc of docs) { - this.statusNames[path.join(GIT_NAMESPACE_DIR, doc.type, `${doc._id}.json`)] = + this.statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] = (doc: any).name || ''; - this.statusNames[path.join(GIT_NAMESPACE_DIR, doc.type, `${doc._id}.yml`)] = + this.statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] = (doc: any).name || ''; } diff --git a/packages/insomnia-app/app/ui/components/wrapper-home.js b/packages/insomnia-app/app/ui/components/wrapper-home.js index e58b040463..39f4d73064 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-home.js +++ b/packages/insomnia-app/app/ui/components/wrapper-home.js @@ -38,7 +38,12 @@ import type { ForceToWorkspace } from '../redux/modules/helpers'; import { ForceToWorkspaceKeys } from '../redux/modules/helpers'; import designerLogo from '../images/insomnia-designer-logo.svg'; import { MemPlugin } from '../../sync/git/mem-plugin'; -import GitVCS, { GIT_NAMESPACE_DIR } from '../../sync/git/git-vcs'; +import GitVCS, { + GIT_CLONE_DIR, + GIT_INSOMNIA_DIR, + GIT_INSOMNIA_DIR_NAME, + GIT_INTERNAL_DIR, +} from '../../sync/git/git-vcs'; import { parseApiSpec } from '../../common/api-specs'; type Props = {| @@ -115,7 +120,6 @@ class WrapperHome extends React.PureComponent { trackEvent('Git', 'Clone'); const core = Math.random() + ''; - const rootDir = path.join('/', GIT_NAMESPACE_DIR); // Create in-memory filesystem to perform clone const plugins = git.cores.create(core); @@ -127,7 +131,8 @@ class WrapperHome extends React.PureComponent { try { await git.clone({ core, - dir: '/', + dir: GIT_CLONE_DIR, + gitdir: GIT_INTERNAL_DIR, singleBranch: true, url, ...credentials, @@ -161,15 +166,15 @@ class WrapperHome extends React.PureComponent { return false; }; - if (!(await ensureDir('/', GIT_NAMESPACE_DIR))) { + if (!(await ensureDir(GIT_CLONE_DIR, GIT_INSOMNIA_DIR_NAME))) { return; } - if (!(await ensureDir(rootDir, models.workspace.type))) { + if (!(await ensureDir(GIT_INSOMNIA_DIR, models.workspace.type))) { return; } - const workspaceBase = path.join(rootDir, models.workspace.type); + const workspaceBase = path.join(GIT_INSOMNIA_DIR, models.workspace.type); const workspaceDirs = await f.readdir(workspaceBase); if (workspaceDirs.length > 1) { @@ -219,14 +224,16 @@ class WrapperHome extends React.PureComponent { // Import all docs to the DB onConfirm: async () => { - const { handleSetActiveWorkspace } = this.props; + const { + wrapperProps: { handleSetActiveWorkspace }, + } = this.props; // Stop the DB from pushing updates to the UI temporarily const bufferId = await db.bufferChanges(); // Loop over all model folders in root - for (const modelType of await f.readdir(rootDir)) { - const modelDir = path.join(rootDir, modelType); + for (const modelType of await f.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)) { diff --git a/packages/insomnia-app/app/ui/containers/app.js b/packages/insomnia-app/app/ui/containers/app.js index 224d7bdb63..52f7f69ab1 100644 --- a/packages/insomnia-app/app/ui/containers/app.js +++ b/packages/insomnia-app/app/ui/containers/app.js @@ -78,7 +78,7 @@ 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_NAMESPACE_DIR } from '../../sync/git/git-vcs'; +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'; @@ -998,7 +998,6 @@ class App extends PureComponent { const pNeDb = NeDBPlugin.createPlugin(activeWorkspace._id); const pGitData = FSPlugin.createPlugin(baseDir); const pOtherData = FSPlugin.createPlugin(path.join(baseDir, 'other')); - const gitSubDir = '/git'; const fsPlugin = routableFSPlugin( // All data outside the directories listed below will be stored in an 'other' @@ -1008,10 +1007,10 @@ class App extends PureComponent { { // 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_NAMESPACE_DIR}`]: pNeDb, + [GIT_INSOMNIA_DIR]: pNeDb, // All git metadata is stored in a git/ directory on the filesystem - [gitSubDir]: pGitData, + [GIT_INTERNAL_DIR]: pGitData, }, ); @@ -1019,9 +1018,9 @@ class App extends PureComponent { if (activeGitRepository.needsFullClone) { await models.gitRepository.update(activeGitRepository, { needsFullClone: false }); const { credentials, uri } = activeGitRepository; - await gitVCS.initFromClone(uri, credentials, '/', fsPlugin, gitSubDir); + await gitVCS.initFromClone(uri, credentials, GIT_CLONE_DIR, fsPlugin, GIT_INTERNAL_DIR); } else { - await gitVCS.init('/', fsPlugin, gitSubDir); + await gitVCS.init(GIT_CLONE_DIR, fsPlugin, GIT_INTERNAL_DIR); } // Configure basic info