mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
Feat/git improvements (#5779)
* fix git repo tab styles * remove extra fetches * add loading indicator for git changes * remote branches and git fetch improvements * fixes * fix tests * fetch before getting the git log * only reinit vcs for different repos * fix make commit button disabled if commiting * show close button in settings modal * fix tests, only fetch from server if there is a remote uri
This commit is contained in:
parent
3738fb56f4
commit
6463d7095f
@ -3,7 +3,7 @@ import path from 'path';
|
||||
|
||||
import type { FileWithStatus } from '../git-rollback';
|
||||
import { gitRollback } from '../git-rollback';
|
||||
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GitVCS } from '../git-vcs';
|
||||
import GitVCS, { GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs';
|
||||
import { MemClient } from '../mem-client';
|
||||
import { setupDateMocks } from './util';
|
||||
|
||||
@ -13,7 +13,7 @@ describe('git rollback', () => {
|
||||
const unlinkMock = jest.fn().mockResolvedValue(undefined);
|
||||
const undoPendingChangesMock = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
let vcs: Partial<GitVCS> = {};
|
||||
let vcs: Partial<typeof GitVCS> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
@ -126,8 +126,10 @@ describe('git rollback', () => {
|
||||
await fsClient.promises.writeFile(fooTxt, 'foo');
|
||||
await fsClient.promises.writeFile(barTxt, 'bar');
|
||||
await fsClient.promises.writeFile(bazTxt, originalContent);
|
||||
const vcs = new GitVCS();
|
||||
const vcs = GitVCS;
|
||||
await vcs.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@je
|
||||
import * as git from 'isomorphic-git';
|
||||
import path from 'path';
|
||||
|
||||
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GitVCS } from '../git-vcs';
|
||||
import GitVCS, { GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs';
|
||||
import { MemClient } from '../mem-client';
|
||||
import { setupDateMocks } from './util';
|
||||
|
||||
@ -22,18 +22,20 @@ describe('Git-VCS', () => {
|
||||
describe('common operations', () => {
|
||||
it('listFiles()', async () => {
|
||||
const fsClient = MemClient.createClient();
|
||||
const vcs = new GitVCS();
|
||||
await vcs.init({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
// No files exist yet
|
||||
const files1 = await vcs.listFiles();
|
||||
const files1 = await GitVCS.listFiles();
|
||||
expect(files1).toEqual([]);
|
||||
// File does not exist in git index
|
||||
await fsClient.promises.writeFile('foo.txt', 'bar');
|
||||
const files2 = await vcs.listFiles();
|
||||
const files2 = await GitVCS.listFiles();
|
||||
expect(files2).toEqual([]);
|
||||
});
|
||||
|
||||
@ -44,31 +46,35 @@ describe('Git-VCS', () => {
|
||||
await fsClient.promises.writeFile(barTxt, 'bar');
|
||||
// Files outside namespace should be ignored
|
||||
await fsClient.promises.writeFile('/other.txt', 'other');
|
||||
const vcs = new GitVCS();
|
||||
await vcs.init({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('*added');
|
||||
await vcs.add(fooTxt);
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('added');
|
||||
await vcs.remove(fooTxt);
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('*added');
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('*added');
|
||||
await GitVCS.add(fooTxt);
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('added');
|
||||
await GitVCS.remove(fooTxt);
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('*added');
|
||||
});
|
||||
|
||||
it('Returns empty log without first commit', async () => {
|
||||
const fsClient = MemClient.createClient();
|
||||
const vcs = new GitVCS();
|
||||
await vcs.init({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
expect(await vcs.log()).toEqual([]);
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
expect(await GitVCS.log()).toEqual([]);
|
||||
});
|
||||
|
||||
it('commit file', async () => {
|
||||
@ -77,17 +83,19 @@ describe('Git-VCS', () => {
|
||||
await fsClient.promises.writeFile(fooTxt, 'foo');
|
||||
await fsClient.promises.writeFile(barTxt, 'bar');
|
||||
await fsClient.promises.writeFile('other.txt', 'should be ignored');
|
||||
const vcs = new GitVCS();
|
||||
await vcs.init({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await vcs.add(fooTxt);
|
||||
await vcs.commit('First commit!');
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('unmodified');
|
||||
expect(await vcs.log()).toEqual([
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await GitVCS.add(fooTxt);
|
||||
await GitVCS.commit('First commit!');
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('unmodified');
|
||||
expect(await GitVCS.log()).toEqual([
|
||||
{
|
||||
commit: {
|
||||
author: {
|
||||
@ -116,14 +124,14 @@ First commit!
|
||||
},
|
||||
]);
|
||||
await fsClient.promises.unlink(fooTxt);
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('*deleted');
|
||||
await vcs.remove(fooTxt);
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('deleted');
|
||||
await vcs.remove(fooTxt);
|
||||
expect(await vcs.status(barTxt)).toBe('*added');
|
||||
expect(await vcs.status(fooTxt)).toBe('deleted');
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('*deleted');
|
||||
await GitVCS.remove(fooTxt);
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('deleted');
|
||||
await GitVCS.remove(fooTxt);
|
||||
expect(await GitVCS.status(barTxt)).toBe('*added');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('deleted');
|
||||
});
|
||||
|
||||
it('create branch', async () => {
|
||||
@ -131,22 +139,24 @@ First commit!
|
||||
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({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await vcs.add(fooTxt);
|
||||
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(barTxt);
|
||||
await vcs.commit('Second commit!');
|
||||
expect((await vcs.log()).length).toBe(2);
|
||||
await vcs.checkout('master');
|
||||
expect((await vcs.log()).length).toBe(1);
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await GitVCS.add(fooTxt);
|
||||
await GitVCS.commit('First commit!');
|
||||
expect((await GitVCS.log()).length).toBe(1);
|
||||
await GitVCS.checkout('new-branch');
|
||||
expect((await GitVCS.log()).length).toBe(1);
|
||||
await GitVCS.add(barTxt);
|
||||
await GitVCS.commit('Second commit!');
|
||||
expect((await GitVCS.log()).length).toBe(2);
|
||||
await GitVCS.checkout('main');
|
||||
expect((await GitVCS.log()).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -156,8 +166,8 @@ First commit!
|
||||
ok: ['unpack'],
|
||||
errors: ['refs/heads/master pre-receive hook declined'],
|
||||
});
|
||||
const vcs = new GitVCS();
|
||||
await expect(vcs.push()).rejects.toThrowError(
|
||||
|
||||
await expect(GitVCS.push()).rejects.toThrowError(
|
||||
'Push rejected with errors: ["refs/heads/master pre-receive hook declined"].\n\nGo to View > Toggle DevTools > Console for more information.',
|
||||
);
|
||||
});
|
||||
@ -173,26 +183,28 @@ First commit!
|
||||
await fsClient.promises.writeFile(fooTxt, originalContent);
|
||||
await fsClient.promises.mkdir(folder);
|
||||
await fsClient.promises.writeFile(folderBarTxt, originalContent);
|
||||
const vcs = new GitVCS();
|
||||
await vcs.init({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
// Commit
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await vcs.add(fooTxt);
|
||||
await vcs.add(folderBarTxt);
|
||||
await vcs.commit('First commit!');
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await GitVCS.add(fooTxt);
|
||||
await GitVCS.add(folderBarTxt);
|
||||
await GitVCS.commit('First commit!');
|
||||
// Change the file
|
||||
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');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('*modified');
|
||||
expect(await GitVCS.status(folderBarTxt)).toBe('*modified');
|
||||
// Undo
|
||||
await vcs.undoPendingChanges();
|
||||
await GitVCS.undoPendingChanges();
|
||||
// Ensure git doesn't recognize a change anymore
|
||||
expect(await vcs.status(fooTxt)).toBe('unmodified');
|
||||
expect(await vcs.status(folderBarTxt)).toBe('unmodified');
|
||||
expect(await GitVCS.status(fooTxt)).toBe('unmodified');
|
||||
expect(await GitVCS.status(folderBarTxt)).toBe('unmodified');
|
||||
// Expect original doc to have reverted
|
||||
expect((await fsClient.promises.readFile(fooTxt)).toString()).toBe(originalContent);
|
||||
expect((await fsClient.promises.readFile(folderBarTxt)).toString()).toBe(originalContent);
|
||||
@ -207,29 +219,31 @@ First commit!
|
||||
const changedContent = 'changedContent';
|
||||
const fsClient = MemClient.createClient();
|
||||
await fsClient.promises.mkdir(GIT_INSOMNIA_DIR);
|
||||
const vcs = new GitVCS();
|
||||
await vcs.init({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
// Write to all files
|
||||
await Promise.all(files.map(f => fsClient.promises.writeFile(f, originalContent)));
|
||||
// Commit all files
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await Promise.all(files.map(f => vcs.add(f, originalContent)));
|
||||
await vcs.commit('First commit!');
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await Promise.all(files.map(f => GitVCS.add(f, originalContent)));
|
||||
await GitVCS.commit('First commit!');
|
||||
// Change all files
|
||||
await Promise.all(files.map(f => fsClient.promises.writeFile(f, changedContent)));
|
||||
await Promise.all(files.map(() => expect(vcs.status(foo1Txt)).resolves.toBe('*modified')));
|
||||
await Promise.all(files.map(() => expect(GitVCS.status(foo1Txt)).resolves.toBe('*modified')));
|
||||
// Undo foo1 and foo2, but not foo3
|
||||
await vcs.undoPendingChanges([foo1Txt, foo2Txt]);
|
||||
expect(await vcs.status(foo1Txt)).toBe('unmodified');
|
||||
expect(await vcs.status(foo2Txt)).toBe('unmodified');
|
||||
await GitVCS.undoPendingChanges([foo1Txt, foo2Txt]);
|
||||
expect(await GitVCS.status(foo1Txt)).toBe('unmodified');
|
||||
expect(await GitVCS.status(foo2Txt)).toBe('unmodified');
|
||||
// Expect original doc to have reverted for foo1 and foo2
|
||||
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 GitVCS.status(foo3Txt)).toBe('*modified');
|
||||
expect((await fsClient.promises.readFile(foo3Txt)).toString()).toBe(changedContent);
|
||||
});
|
||||
});
|
||||
@ -242,23 +256,25 @@ First commit!
|
||||
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({
|
||||
|
||||
await GitVCS.init({
|
||||
uri: '',
|
||||
repoId: '',
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: fsClient,
|
||||
});
|
||||
await vcs.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await vcs.add(dirFooTxt);
|
||||
await vcs.commit('First');
|
||||
await GitVCS.setAuthor('Karen Brown', 'karen@example.com');
|
||||
await GitVCS.add(dirFooTxt);
|
||||
await GitVCS.commit('First');
|
||||
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].commit.tree, dirFooTxt)).toBe('foo bar');
|
||||
expect(await vcs.readObjFromTree(log[1].commit.tree, dirFooTxt)).toBe('foo');
|
||||
await GitVCS.add(dirFooTxt);
|
||||
await GitVCS.commit('Second');
|
||||
const log = await GitVCS.log();
|
||||
expect(await GitVCS.readObjFromTree(log[0].commit.tree, dirFooTxt)).toBe('foo bar');
|
||||
expect(await GitVCS.readObjFromTree(log[1].commit.tree, dirFooTxt)).toBe('foo');
|
||||
// Some extra checks
|
||||
expect(await vcs.readObjFromTree(log[1].commit.tree, 'missing')).toBe(null);
|
||||
expect(await vcs.readObjFromTree('missing', 'missing')).toBe(null);
|
||||
expect(await GitVCS.readObjFromTree(log[1].commit.tree, 'missing')).toBe(null);
|
||||
expect(await GitVCS.readObjFromTree('missing', 'missing')).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -25,7 +25,7 @@ interface GitCredentialsOAuth {
|
||||
* Supported OAuth formats.
|
||||
* This is needed by isomorphic-git to be able to push/pull using an oauth2 token.
|
||||
* https://isomorphic-git.org/docs/en/authentication.html
|
||||
*/
|
||||
*/
|
||||
oauth2format?: 'github' | 'gitlab';
|
||||
username: string;
|
||||
token: string;
|
||||
@ -33,7 +33,9 @@ interface GitCredentialsOAuth {
|
||||
|
||||
export type GitCredentials = GitCredentialsBase | GitCredentialsOAuth;
|
||||
|
||||
export const isGitCredentialsOAuth = (credentials: GitCredentials): credentials is GitCredentialsOAuth => {
|
||||
export const isGitCredentialsOAuth = (
|
||||
credentials: GitCredentials
|
||||
): credentials is GitCredentialsOAuth => {
|
||||
return 'oauth2format' in credentials;
|
||||
};
|
||||
|
||||
@ -62,6 +64,9 @@ interface InitOptions {
|
||||
directory: string;
|
||||
fs: git.FsClient;
|
||||
gitDirectory?: string;
|
||||
gitCredentials?: GitCredentials | null;
|
||||
uri?: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
interface InitFromCloneOptions {
|
||||
@ -70,6 +75,7 @@ interface InitFromCloneOptions {
|
||||
directory: string;
|
||||
fs: git.FsClient;
|
||||
gitDirectory: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,38 +100,77 @@ interface BaseOpts {
|
||||
onAuthFailure: git.AuthFailureCallback;
|
||||
onAuthSuccess: git.AuthSuccessCallback;
|
||||
onAuth: git.AuthCallback;
|
||||
uri: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
export class GitVCS {
|
||||
// @ts-expect-error -- TSCONVERSION not initialized with required properties
|
||||
_baseOpts: BaseOpts = gitCallbacks();
|
||||
|
||||
initialized: boolean;
|
||||
initializedRepoId = '';
|
||||
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async init({ directory, fs, gitDirectory }: InitOptions) {
|
||||
async init({ directory, fs, gitDirectory, gitCredentials, uri = '', repoId }: InitOptions) {
|
||||
this._baseOpts = {
|
||||
...this._baseOpts,
|
||||
dir: directory,
|
||||
...gitCallbacks(gitCredentials),
|
||||
gitdir: gitDirectory,
|
||||
fs,
|
||||
http: httpClient,
|
||||
uri,
|
||||
repoId,
|
||||
};
|
||||
|
||||
if (await this.repoExists()) {
|
||||
console.log(`[git] Opened repo for ${gitDirectory}`);
|
||||
} else {
|
||||
console.log(`[git] Initialized repo in ${gitDirectory}`);
|
||||
await git.init(this._baseOpts);
|
||||
}
|
||||
let defaultBranch = 'main';
|
||||
|
||||
this.initialized = true;
|
||||
try {
|
||||
const url = await this.getRemoteOriginURI();
|
||||
if (!url) {
|
||||
throw new Error('No remote origin URL');
|
||||
}
|
||||
const [mainRef] = await git.listServerRefs({
|
||||
...this._baseOpts,
|
||||
url,
|
||||
prefix: 'HEAD',
|
||||
symrefs: true,
|
||||
});
|
||||
|
||||
defaultBranch = mainRef?.target?.replace('refs/heads/', '') || 'main';
|
||||
} catch (err) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
await git.init({ ...this._baseOpts, defaultBranch });
|
||||
}
|
||||
}
|
||||
|
||||
async initFromClone({ url, gitCredentials, directory, fs, gitDirectory }: InitFromCloneOptions) {
|
||||
async getRemoteOriginURI() {
|
||||
try {
|
||||
const remoteOriginURI = await git.getConfig({
|
||||
...this._baseOpts,
|
||||
path: 'remote.origin.url',
|
||||
});
|
||||
|
||||
return remoteOriginURI;
|
||||
} catch (err) {
|
||||
// Ignore error
|
||||
return this._baseOpts.uri || '';
|
||||
}
|
||||
}
|
||||
|
||||
async initFromClone({
|
||||
repoId,
|
||||
url,
|
||||
gitCredentials,
|
||||
directory,
|
||||
fs,
|
||||
gitDirectory,
|
||||
}: InitFromCloneOptions) {
|
||||
this._baseOpts = {
|
||||
...this._baseOpts,
|
||||
...gitCallbacks(gitCredentials),
|
||||
@ -133,21 +178,26 @@ export class GitVCS {
|
||||
gitdir: gitDirectory,
|
||||
fs,
|
||||
http: httpClient,
|
||||
repoId,
|
||||
};
|
||||
const cloneParams = { ...this._baseOpts, url, singleBranch: true };
|
||||
await git.clone(cloneParams);
|
||||
await git.clone({
|
||||
...this._baseOpts,
|
||||
url,
|
||||
singleBranch: true,
|
||||
});
|
||||
console.log(`[git] Clones repo to ${gitDirectory} from ${url}`);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
isInitialized() {
|
||||
return this.initialized;
|
||||
isInitializedForRepo(id: string) {
|
||||
return this._baseOpts.repoId === id;
|
||||
}
|
||||
|
||||
async listFiles() {
|
||||
console.log('[git] List files');
|
||||
const repositoryFiles = await git.listFiles({ ...this._baseOpts });
|
||||
const insomniaFiles = repositoryFiles.filter(file => file.startsWith(GIT_INSOMNIA_DIR_NAME)).map(convertToOsSep);
|
||||
const insomniaFiles = repositoryFiles
|
||||
.filter(file => file.startsWith(GIT_INSOMNIA_DIR_NAME))
|
||||
.map(convertToOsSep);
|
||||
return insomniaFiles;
|
||||
}
|
||||
|
||||
@ -170,17 +220,48 @@ export class GitVCS {
|
||||
branches.push(branch);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[git] Local branches: ${branches.join(', ')} (current: ${branch})`
|
||||
);
|
||||
|
||||
return GitVCS.sortBranches(branches);
|
||||
}
|
||||
|
||||
async listRemoteBranches() {
|
||||
const branches = await git.listBranches({ ...this._baseOpts, remote: 'origin' });
|
||||
const branches = await git.listBranches({
|
||||
...this._baseOpts,
|
||||
remote: 'origin',
|
||||
});
|
||||
// Don't care about returning remote HEAD
|
||||
return GitVCS.sortBranches(branches.filter(b => b !== 'HEAD'));
|
||||
}
|
||||
|
||||
async fetchRemoteBranches() {
|
||||
const uri = await this.getRemoteOriginURI();
|
||||
try {
|
||||
const branches = await git.listServerRefs({
|
||||
...this._baseOpts,
|
||||
prefix: 'refs/heads/',
|
||||
url: uri,
|
||||
});
|
||||
console.log({ branches });
|
||||
// Don't care about returning remote HEAD
|
||||
return GitVCS.sortBranches(
|
||||
branches
|
||||
.filter(b => b.ref !== 'HEAD')
|
||||
.map(b => b.ref.replace('refs/heads/', ''))
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(`[git] Failed to list remote branches for ${uri}`, e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async status(filepath: string) {
|
||||
return git.status({ ...this._baseOpts, filepath: convertToPosixSep(filepath) });
|
||||
return git.status({
|
||||
...this._baseOpts,
|
||||
filepath: convertToPosixSep(filepath),
|
||||
});
|
||||
}
|
||||
|
||||
async add(relPath: string) {
|
||||
@ -197,7 +278,12 @@ export class GitVCS {
|
||||
|
||||
async addRemote(url: string) {
|
||||
console.log(`[git] Add Remote url=${url}`);
|
||||
await git.addRemote({ ...this._baseOpts, remote: 'origin', url, force: true });
|
||||
await git.addRemote({
|
||||
...this._baseOpts,
|
||||
remote: 'origin',
|
||||
url,
|
||||
force: true,
|
||||
});
|
||||
const config = await this.getRemote('origin');
|
||||
|
||||
if (config === null) {
|
||||
@ -213,7 +299,10 @@ export class GitVCS {
|
||||
|
||||
async getAuthor() {
|
||||
const name = await git.getConfig({ ...this._baseOpts, path: 'user.name' });
|
||||
const email = await git.getConfig({ ...this._baseOpts, path: 'user.email' });
|
||||
const email = await git.getConfig({
|
||||
...this._baseOpts,
|
||||
path: 'user.email',
|
||||
});
|
||||
return {
|
||||
name: name || '',
|
||||
email: email || '',
|
||||
@ -222,7 +311,11 @@ export class GitVCS {
|
||||
|
||||
async setAuthor(name: string, email: string) {
|
||||
await git.setConfig({ ...this._baseOpts, path: 'user.name', value: name });
|
||||
await git.setConfig({ ...this._baseOpts, path: 'user.email', value: email });
|
||||
await git.setConfig({
|
||||
...this._baseOpts,
|
||||
path: 'user.email',
|
||||
value: email,
|
||||
});
|
||||
}
|
||||
|
||||
async getRemote(name: string): Promise<GitRemoteConfig | null> {
|
||||
@ -257,7 +350,7 @@ export class GitVCS {
|
||||
forPush: true,
|
||||
url: remote.url,
|
||||
});
|
||||
const logs = (await this.log(1)) || [];
|
||||
const logs = (await this.log({ depth: 1 })) || [];
|
||||
const localHead = logs[0].oid;
|
||||
const remoteRefs = remoteInfo.refs || {};
|
||||
const remoteHeads = remoteRefs.heads || {};
|
||||
@ -286,7 +379,7 @@ export class GitVCS {
|
||||
// @ts-expect-error -- TSCONVERSION git errors are not handled correctly
|
||||
const errorsString = JSON.stringify(response.errors);
|
||||
throw new Error(
|
||||
`Push rejected with errors: ${errorsString}.\n\nGo to View > Toggle DevTools > Console for more information.`,
|
||||
`Push rejected with errors: ${errorsString}.\n\nGo to View > Toggle DevTools > Console for more information.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -311,28 +404,48 @@ export class GitVCS {
|
||||
});
|
||||
}
|
||||
|
||||
async fetch(
|
||||
singleBranch: boolean,
|
||||
depth?: number,
|
||||
gitCredentials?: GitCredentials | null,
|
||||
) {
|
||||
async fetch({
|
||||
singleBranch,
|
||||
depth,
|
||||
credentials,
|
||||
relative = false,
|
||||
}: {
|
||||
singleBranch: boolean;
|
||||
depth?: number;
|
||||
credentials?: GitCredentials | null;
|
||||
relative?: boolean;
|
||||
}) {
|
||||
console.log('[git] Fetch remote=origin');
|
||||
return git.fetch({
|
||||
...this._baseOpts,
|
||||
...gitCallbacks(gitCredentials),
|
||||
...gitCallbacks(credentials),
|
||||
singleBranch,
|
||||
remote: 'origin',
|
||||
relative,
|
||||
depth,
|
||||
prune: true,
|
||||
pruneTags: true,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async log(depth?: number) {
|
||||
async log(input: {depth?: number} = {}) {
|
||||
const { depth = 35 } = input;
|
||||
try {
|
||||
const remoteOriginURI = await this.getRemoteOriginURI();
|
||||
if (remoteOriginURI) {
|
||||
await git.fetch({
|
||||
...this._baseOpts,
|
||||
remote: 'origin',
|
||||
depth,
|
||||
singleBranch: true,
|
||||
tags: false,
|
||||
});
|
||||
}
|
||||
|
||||
return await git.log({ ...this._baseOpts, depth });
|
||||
} catch (error) {
|
||||
if (error.code === 'NotFoundError') {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof git.Errors.NotFoundError) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -341,8 +454,18 @@ export class GitVCS {
|
||||
}
|
||||
|
||||
async branch(branch: string, checkout = false) {
|
||||
// @ts-expect-error -- TSCONVERSION remote doesn't exist as an option
|
||||
await git.branch({ ...this._baseOpts, ref: branch, checkout, remote: 'origin' });
|
||||
console.log('[git] Branch', {
|
||||
branch,
|
||||
checkout,
|
||||
});
|
||||
|
||||
await git.branch({
|
||||
...this._baseOpts,
|
||||
ref: branch,
|
||||
checkout,
|
||||
// @ts-expect-error -- TSCONVERSION remote doesn't exist as an option
|
||||
remote: 'origin',
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBranch(branch: string) {
|
||||
@ -354,19 +477,39 @@ export class GitVCS {
|
||||
branch,
|
||||
});
|
||||
const localBranches = await this.listBranches();
|
||||
const remoteBranches = await this.listRemoteBranches();
|
||||
const branches = [...localBranches, ...remoteBranches];
|
||||
const syncedBranches = await this.listRemoteBranches();
|
||||
const remoteBranches = await this.fetchRemoteBranches();
|
||||
const branches = [...localBranches, ...syncedBranches, ...remoteBranches];
|
||||
console.log('[git] Checkout branches', { branches, branch });
|
||||
|
||||
if (branches.includes(branch)) {
|
||||
try {
|
||||
if (!syncedBranches.includes(branch)) {
|
||||
console.log('[git] Fetching branch', branch);
|
||||
// Try to fetch the branch from the remote if it doesn't exist locally;
|
||||
await git.fetch({
|
||||
...this._baseOpts,
|
||||
remote: 'origin',
|
||||
depth: 1,
|
||||
ref: branch,
|
||||
singleBranch: true,
|
||||
tags: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[git] Fetch failed', e);
|
||||
}
|
||||
|
||||
await git.checkout({
|
||||
...this._baseOpts,
|
||||
ref: branch,
|
||||
remote: 'origin',
|
||||
});
|
||||
const branches = await this.listBranches();
|
||||
console.log('[git] Checkout branches', { branches });
|
||||
} else {
|
||||
await this.branch(branch, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async undoPendingChanges(fileFilter?: string[]) {
|
||||
@ -423,9 +566,4 @@ export class GitVCS {
|
||||
}
|
||||
}
|
||||
|
||||
let gitVCSInstance = new GitVCS();
|
||||
|
||||
export const getGitVCS = () => gitVCSInstance;
|
||||
export const setGitVCS = (gitVCS: GitVCS) => {
|
||||
gitVCSInstance = gitVCS;
|
||||
};
|
||||
export default new GitVCS();
|
||||
|
@ -5,8 +5,20 @@ import { useFetcher, useParams } from 'react-router-dom';
|
||||
import { docsGitSync } from '../../../common/documentation';
|
||||
import { GitRepository } from '../../../models/git-repository';
|
||||
import { getOauth2FormatName } from '../../../sync/git/utils';
|
||||
import { GitRepoLoaderData, PullFromGitRemoteResult, PushToGitRemoteResult } from '../../routes/git-actions';
|
||||
import { type DropdownHandle, Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import {
|
||||
GitFetchLoaderData,
|
||||
GitRepoLoaderData,
|
||||
PullFromGitRemoteResult,
|
||||
PushToGitRemoteResult,
|
||||
} from '../../routes/git-actions';
|
||||
import {
|
||||
type DropdownHandle,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
DropdownItem,
|
||||
DropdownSection,
|
||||
ItemContent,
|
||||
} from '../base/dropdown';
|
||||
import { Link } from '../base/link';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { showAlert } from '../modals';
|
||||
@ -22,10 +34,15 @@ interface Props {
|
||||
}
|
||||
|
||||
export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string };
|
||||
const { organizationId, projectId, workspaceId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
|
||||
const [isGitRepoSettingsModalOpen, setIsGitRepoSettingsModalOpen] = useState(false);
|
||||
const [isGitRepoSettingsModalOpen, setIsGitRepoSettingsModalOpen] =
|
||||
useState(false);
|
||||
const [isGitBranchesModalOpen, setIsGitBranchesModalOpen] = useState(false);
|
||||
const [isGitLogModalOpen, setIsGitLogModalOpen] = useState(false);
|
||||
const [isGitStagingModalOpen, setIsGitStagingModalOpen] = useState(false);
|
||||
@ -34,20 +51,34 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
const gitPullFetcher = useFetcher<PullFromGitRemoteResult>();
|
||||
const gitCheckoutFetcher = useFetcher();
|
||||
const gitRepoDataFetcher = useFetcher<GitRepoLoaderData>();
|
||||
const gitFetchFetcher = useFetcher<GitFetchLoaderData>();
|
||||
|
||||
const loadingPush = gitPushFetcher.state === 'loading';
|
||||
const loadingPull = gitPullFetcher.state === 'loading';
|
||||
const loadingFetch = gitFetchFetcher.state === 'loading';
|
||||
|
||||
useEffect(() => {
|
||||
if (gitRepository?.uri && gitRepository?._id && gitRepoDataFetcher.state === 'idle' && !gitRepoDataFetcher.data) {
|
||||
gitRepoDataFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/repo`);
|
||||
if (
|
||||
gitRepository?.uri &&
|
||||
gitRepository?._id &&
|
||||
gitRepoDataFetcher.state === 'idle' &&
|
||||
!gitRepoDataFetcher.data
|
||||
) {
|
||||
gitRepoDataFetcher.load(
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/repo`
|
||||
);
|
||||
}
|
||||
}, [gitRepoDataFetcher, gitRepository?.uri, gitRepository?._id, organizationId, projectId, workspaceId]);
|
||||
}, [
|
||||
gitRepoDataFetcher,
|
||||
gitRepository?.uri,
|
||||
gitRepository?._id,
|
||||
organizationId,
|
||||
projectId,
|
||||
workspaceId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [
|
||||
...gitPushFetcher.data?.errors ?? [],
|
||||
];
|
||||
const errors = [...(gitPushFetcher.data?.errors ?? [])];
|
||||
if (errors.length > 0) {
|
||||
showAlert({
|
||||
title: 'Push Failed',
|
||||
@ -57,9 +88,21 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
}, [gitPushFetcher.data?.errors]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [
|
||||
...gitPullFetcher.data?.errors ?? [],
|
||||
];
|
||||
const gitRepoDataErrors =
|
||||
gitRepoDataFetcher.data && 'errors' in gitRepoDataFetcher.data
|
||||
? gitRepoDataFetcher.data.errors
|
||||
: [];
|
||||
const errors = [...gitRepoDataErrors];
|
||||
if (errors.length > 0) {
|
||||
showAlert({
|
||||
title: 'Loading of Git Repository Failed',
|
||||
message: errors.join('\n'),
|
||||
});
|
||||
}
|
||||
}, [gitRepoDataFetcher.data]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [...(gitPullFetcher.data?.errors ?? [])];
|
||||
if (errors.length > 0) {
|
||||
showAlert({
|
||||
title: 'Pull Failed',
|
||||
@ -69,9 +112,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
}, [gitPullFetcher.data?.errors]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [
|
||||
...gitCheckoutFetcher.data?.errors ?? [],
|
||||
];
|
||||
const errors = [...(gitCheckoutFetcher.data?.errors ?? [])];
|
||||
if (errors.length > 0) {
|
||||
showAlert({
|
||||
title: 'Checkout Failed',
|
||||
@ -81,12 +122,15 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
}, [gitCheckoutFetcher.data?.errors]);
|
||||
|
||||
async function handlePush({ force }: { force: boolean }) {
|
||||
gitPushFetcher.submit({
|
||||
force: `${force}`,
|
||||
}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/push`,
|
||||
method: 'post',
|
||||
});
|
||||
gitPushFetcher.submit(
|
||||
{
|
||||
force: `${force}`,
|
||||
},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/push`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let iconClassName = '';
|
||||
@ -98,10 +142,21 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
iconClassName = 'fa fa-gitlab';
|
||||
}
|
||||
|
||||
const isLoading = gitRepoDataFetcher.state === 'loading';
|
||||
const isButton = !gitRepository || (isLoading && !gitRepoDataFetcher.data) || (gitRepoDataFetcher.data && 'errors' in gitRepoDataFetcher.data);
|
||||
const isLoading =
|
||||
gitRepoDataFetcher.state === 'loading' ||
|
||||
gitFetchFetcher.state === 'loading' ||
|
||||
gitCheckoutFetcher.state === 'loading' ||
|
||||
gitPushFetcher.state === 'loading' ||
|
||||
gitPullFetcher.state === 'loading';
|
||||
const isButton =
|
||||
!gitRepository ||
|
||||
(isLoading && !gitRepoDataFetcher.data) ||
|
||||
(gitRepoDataFetcher.data && 'errors' in gitRepoDataFetcher.data);
|
||||
|
||||
const { log, branches, branch: currentBranch, remoteBranches } = (gitRepoDataFetcher.data && 'log' in gitRepoDataFetcher.data) ? gitRepoDataFetcher.data : { log: [], branches: [], branch: '', remoteBranches: [] };
|
||||
const { branches, branch: currentBranch } =
|
||||
gitRepoDataFetcher.data && 'branches' in gitRepoDataFetcher.data
|
||||
? gitRepoDataFetcher.data
|
||||
: { branches: [], branch: '' };
|
||||
|
||||
let dropdown: React.ReactNode = null;
|
||||
|
||||
@ -118,30 +173,44 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
icon: loadingPull ? 'refresh fa-spin' : 'cloud-download',
|
||||
label: 'Pull',
|
||||
onClick: async () => {
|
||||
gitPullFetcher.submit({}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/pull`,
|
||||
method: 'post',
|
||||
});
|
||||
gitPullFetcher.submit(
|
||||
{},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/pull`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
isDisabled: log.length === 0,
|
||||
icon: 'clock-o',
|
||||
label: <span>History ({log.length})</span>,
|
||||
onClick: () => setIsGitLogModalOpen(true),
|
||||
},
|
||||
];
|
||||
|
||||
if (log.length > 0) {
|
||||
currentBranchActions.splice(1, 0, {
|
||||
id: 4,
|
||||
stayOpenAfterClick: true,
|
||||
icon: loadingPush ? 'refresh fa-spin' : 'cloud-upload',
|
||||
label: 'Push',
|
||||
onClick: () => handlePush({ force: false }),
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: 'clock-o',
|
||||
label: <span>History</span>,
|
||||
onClick: () => setIsGitLogModalOpen(true),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
stayOpenAfterClick: true,
|
||||
icon: loadingFetch ? 'refresh fa-spin' : 'refresh',
|
||||
label: 'Fetch',
|
||||
onClick: () => {
|
||||
gitFetchFetcher.submit(
|
||||
{},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/fetch`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (isButton) {
|
||||
dropdown = (
|
||||
@ -151,7 +220,11 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
className="btn--clicky-small btn-sync"
|
||||
onClick={() => setIsGitRepoSettingsModalOpen(true)}
|
||||
>
|
||||
<i className={`fa fa-code-fork space-right ${isLoading ? 'fa-fade' : ''}`} />
|
||||
<i
|
||||
className={`fa fa-code-fork space-right ${
|
||||
isLoading ? 'fa-fade' : ''
|
||||
}`}
|
||||
/>
|
||||
{isLoading ? 'Loading...' : 'Setup Git Sync'}
|
||||
</Button>
|
||||
);
|
||||
@ -161,13 +234,26 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
<Dropdown
|
||||
className="wide tall"
|
||||
ref={dropdownRef}
|
||||
onOpen={() => {
|
||||
gitFetchFetcher.submit(
|
||||
{},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/fetch`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
}}
|
||||
triggerButton={
|
||||
<DropdownButton className="btn--clicky-small btn-sync">
|
||||
{iconClassName && (
|
||||
<i className={classnames('space-right', iconClassName)} />
|
||||
)}
|
||||
<div className="ellipsis">{currentBranch}</div>
|
||||
<i className={`fa fa-code-fork space-left ${isLoading ? 'fa-fade' : ''}`} />
|
||||
<i
|
||||
className={`fa fa-code-fork space-left ${
|
||||
isLoading ? 'fa-fade' : ''
|
||||
}`}
|
||||
/>
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
@ -184,11 +270,10 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DropdownItem>
|
||||
<DropdownItem textValue="Settings">
|
||||
<ItemContent
|
||||
icon="wrench"
|
||||
label="Repository Settings"
|
||||
@ -198,7 +283,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
/>
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem>
|
||||
<DropdownItem textValue="Branches">
|
||||
{currentBranch && (
|
||||
<ItemContent
|
||||
icon="code-fork"
|
||||
@ -219,21 +304,22 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
const isCurrentBranch = branch === currentBranch;
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key={branch}
|
||||
>
|
||||
<DropdownItem key={branch} textValue={branch}>
|
||||
<ItemContent
|
||||
className={classnames({ bold: isCurrentBranch })}
|
||||
icon={branch === currentBranch ? 'tag' : 'empty'}
|
||||
label={branch}
|
||||
isDisabled={isCurrentBranch}
|
||||
onClick={async () => {
|
||||
gitCheckoutFetcher.submit({
|
||||
branch,
|
||||
}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branch/checkout`,
|
||||
method: 'post',
|
||||
});
|
||||
gitCheckoutFetcher.submit(
|
||||
{
|
||||
branch,
|
||||
},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branch/checkout`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
@ -245,11 +331,16 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
title={currentBranch}
|
||||
items={currentBranch ? currentBranchActions : []}
|
||||
>
|
||||
{({ id, ...action }) =>
|
||||
<DropdownItem key={id}>
|
||||
{({ id, ...action }) => (
|
||||
<DropdownItem
|
||||
key={id}
|
||||
textValue={
|
||||
typeof action.label === 'string' ? action.label : `${id}`
|
||||
}
|
||||
>
|
||||
<ItemContent {...action} />
|
||||
</DropdownItem>
|
||||
}
|
||||
)}
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@ -259,22 +350,29 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{dropdown}
|
||||
{isGitRepoSettingsModalOpen && <GitRepositorySettingsModal gitRepository={gitRepository ?? undefined} onHide={() => setIsGitRepoSettingsModalOpen(false)} />}
|
||||
{isGitBranchesModalOpen &&
|
||||
{isGitRepoSettingsModalOpen && (
|
||||
<GitRepositorySettingsModal
|
||||
gitRepository={gitRepository ?? undefined}
|
||||
onHide={() => setIsGitRepoSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isGitBranchesModalOpen && (
|
||||
<GitBranchesModal
|
||||
gitRepository={gitRepository}
|
||||
branches={branches}
|
||||
remoteBranches={remoteBranches}
|
||||
gitRepository={gitRepository}
|
||||
activeBranch={currentBranch}
|
||||
onHide={() => setIsGitBranchesModalOpen(false)}
|
||||
/>
|
||||
}
|
||||
{isGitLogModalOpen && <GitLogModal branch={currentBranch} logs={log} onHide={() => setIsGitLogModalOpen(false)} />}
|
||||
{isGitStagingModalOpen &&
|
||||
<GitStagingModal
|
||||
onHide={() => setIsGitStagingModalOpen(false)}
|
||||
)}
|
||||
{isGitLogModalOpen && (
|
||||
<GitLogModal
|
||||
branch={currentBranch}
|
||||
onHide={() => setIsGitLogModalOpen(false)}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
{isGitStagingModalOpen && (
|
||||
<GitStagingModal onHide={() => setIsGitStagingModalOpen(false)} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import { OverlayContainer } from 'react-aria';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
|
||||
import { GitRepository } from '../../../models/git-repository';
|
||||
import { CreateNewGitBranchResult } from '../../routes/git-actions';
|
||||
import { CreateNewGitBranchResult, GitBranchesLoaderData } from '../../routes/git-actions';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
@ -13,10 +13,9 @@ import { PromptButton } from '../base/prompt-button';
|
||||
import { showAlert } from '.';
|
||||
|
||||
type Props = ModalProps & {
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
activeBranch: string;
|
||||
gitRepository: GitRepository | null;
|
||||
branches: string[];
|
||||
};
|
||||
|
||||
export interface GitBranchesModalOptions {
|
||||
@ -24,10 +23,9 @@ export interface GitBranchesModalOptions {
|
||||
}
|
||||
|
||||
export const GitBranchesModal: FC<Props> = (({
|
||||
onHide,
|
||||
branches,
|
||||
onHide,
|
||||
activeBranch,
|
||||
remoteBranches,
|
||||
}) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string};
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
@ -36,12 +34,24 @@ export const GitBranchesModal: FC<Props> = (({
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const branchesFetcher = useFetcher<GitBranchesLoaderData>();
|
||||
const checkoutBranchFetcher = useFetcher();
|
||||
const mergeBranchFetcher = useFetcher();
|
||||
const newBranchFetcher = useFetcher<CreateNewGitBranchResult>();
|
||||
const deleteBranchFetcher = useFetcher();
|
||||
|
||||
const remoteOnlyBranches = remoteBranches.filter(b => !branches.includes(b));
|
||||
// const errors = branchesFetcher.data && 'errors' in branchesFetcher.data ? branchesFetcher.data.errors : [];
|
||||
const { remoteBranches, branches: localBranches } = branchesFetcher.data && 'branches' in branchesFetcher.data ? branchesFetcher.data : { branches: [], remoteBranches: [] };
|
||||
|
||||
const fetchedBranches = localBranches.length > 0 ? localBranches : branches;
|
||||
const remoteOnlyBranches = remoteBranches.filter(b => !fetchedBranches.includes(b));
|
||||
const isFetchingRemoteBranches = branchesFetcher.state !== 'idle';
|
||||
|
||||
useEffect(() => {
|
||||
if (branchesFetcher.state === 'idle' && !branchesFetcher.data) {
|
||||
branchesFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branches`);
|
||||
}
|
||||
}, [branchesFetcher, organizationId, projectId, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newBranchFetcher.data?.errors?.length) {
|
||||
@ -55,7 +65,7 @@ export const GitBranchesModal: FC<Props> = (({
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} onHide={onHide}>
|
||||
<ModalHeader>Branches</ModalHeader>
|
||||
<ModalHeader><i className={`fa fa-code-fork space-left ${isFetchingRemoteBranches ? 'fa-fade' : ''}`} /> Branches</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
<newBranchFetcher.Form
|
||||
method="post"
|
||||
@ -91,7 +101,7 @@ export const GitBranchesModal: FC<Props> = (({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{branches.map(branch => (
|
||||
{fetchedBranches.map(branch => (
|
||||
<tr key={branch} className="table--no-outline-row">
|
||||
<td>
|
||||
<span
|
||||
@ -157,12 +167,20 @@ export const GitBranchesModal: FC<Props> = (({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{Boolean(isFetchingRemoteBranches && !remoteOnlyBranches.length) && (
|
||||
<div className="pad-top">
|
||||
<div className="txt-sm faint italic">
|
||||
<i className="fa fa-spinner fa-spin space-right" />
|
||||
Fetching remote branches...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{remoteOnlyBranches.length > 0 && (
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Remote Branches</th>
|
||||
<th className="text-left">Remote Branches {isFetchingRemoteBranches && <i className="fa fa-spinner fa-spin space-right" />}</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
|
||||
import type { GitLogEntry } from '../../../sync/git/git-vcs';
|
||||
import { GitLogLoaderData } from '../../routes/git-actions';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
@ -10,23 +11,40 @@ import { TimeFromNow } from '../time-from-now';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
type Props = ModalProps & {
|
||||
logs: GitLogEntry[];
|
||||
branch: string;
|
||||
};
|
||||
|
||||
export const GitLogModal: FC<Props> = ({ branch, logs, onHide }) => {
|
||||
export const GitLogModal: FC<Props> = ({ branch, onHide }) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const gitLogFetcher = useFetcher<GitLogLoaderData>();
|
||||
|
||||
const isLoading = gitLogFetcher.state !== 'idle';
|
||||
useEffect(() => {
|
||||
if (gitLogFetcher.state === 'idle' && !gitLogFetcher.data) {
|
||||
gitLogFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/log`);
|
||||
}
|
||||
}, [organizationId, projectId, workspaceId, gitLogFetcher]);
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const { log } = gitLogFetcher.data && 'log' in gitLogFetcher.data ? gitLogFetcher.data : { log: [] };
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} onHide={onHide}>
|
||||
<ModalHeader>Git History ({logs.length})</ModalHeader>
|
||||
<ModalHeader>Git History</ModalHeader>
|
||||
<ModalBody className="pad">
|
||||
<table className="table--fancy table--striped">
|
||||
{isLoading && <div className="txt-sm faint italic">
|
||||
<i className="fa fa-spinner fa-spin space-right" />
|
||||
Loading git log...
|
||||
</div>}
|
||||
{!isLoading && <table className="table--fancy table--striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Message</th>
|
||||
@ -34,7 +52,7 @@ export const GitLogModal: FC<Props> = ({ branch, logs, onHide }) => {
|
||||
<th className="text-left">Author</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{logs.map(entry => {
|
||||
<tbody>{log.map(entry => {
|
||||
const {
|
||||
commit: { author, message },
|
||||
oid,
|
||||
@ -57,7 +75,7 @@ export const GitLogModal: FC<Props> = ({ branch, logs, onHide }) => {
|
||||
</tr>
|
||||
);
|
||||
})}</tbody>
|
||||
</table>
|
||||
</table>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Key, useEffect, useRef, useState } from 'react';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { docsGitSync } from '../../../../common/documentation';
|
||||
import type { GitRepository, OauthProviderName } from '../../../../models/git-repository';
|
||||
@ -16,6 +17,12 @@ import { CustomRepositorySettingsFormGroup } from './custom-repository-settings-
|
||||
import { GitHubRepositorySetupFormGroup } from './github-repository-settings-form-group';
|
||||
import { GitLabRepositorySetupFormGroup } from './gitlab-repository-settings-form-group';
|
||||
|
||||
const TabPill = styled.div({
|
||||
display: 'flex',
|
||||
gap: 'var(--padding-xs)',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const GitRepositoryCloneModal = (props: ModalProps) => {
|
||||
const { organizationId, projectId } = useParams() as { organizationId: string; projectId: string };
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
@ -82,21 +89,21 @@ export const GitRepositoryCloneModal = (props: ModalProps) => {
|
||||
selectedKey={selectedTab}
|
||||
onSelectionChange={(key: Key) => setTab(key as OauthProviderName)}
|
||||
>
|
||||
<TabItem key='github' title={<><i className="fa fa-github" /> GitHub</>}>
|
||||
<TabItem key='github' title={<TabPill><i className="fa fa-github" /> GitHub</TabPill>}>
|
||||
<PanelContainer className="pad pad-top-sm">
|
||||
<GitHubRepositorySetupFormGroup
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<TabItem key='gitlab' title={<><i className="fa fa-gitlab" /> GitLab</>}>
|
||||
<TabItem key='gitlab' title={<TabPill><i className="fa fa-gitlab" /> GitLab</TabPill>}>
|
||||
<PanelContainer className="pad pad-top-sm">
|
||||
<GitLabRepositorySetupFormGroup
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<TabItem key='custom' title={<><i className="fa fa-code-fork" /> Git</>}>
|
||||
<TabItem key='custom' title={<TabPill><i className="fa fa-code-fork" /> Git</TabPill>}>
|
||||
<PanelContainer className="pad pad-top-sm">
|
||||
<CustomRepositorySettingsFormGroup
|
||||
onSubmit={onSubmit}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { Key, useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { docsGitSync } from '../../../../common/documentation';
|
||||
import type { GitRepository, OauthProviderName } from '../../../../models/git-repository';
|
||||
@ -17,6 +18,12 @@ import { CustomRepositorySettingsFormGroup } from './custom-repository-settings-
|
||||
import { GitHubRepositorySetupFormGroup } from './github-repository-settings-form-group';
|
||||
import { GitLabRepositorySetupFormGroup } from './gitlab-repository-settings-form-group';
|
||||
|
||||
const TabPill = styled.div({
|
||||
display: 'flex',
|
||||
gap: 'var(--padding-xs)',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const GitRepositorySettingsModal = (props: ModalProps & {
|
||||
gitRepository?: GitRepository;
|
||||
}) => {
|
||||
@ -58,9 +65,9 @@ export const GitRepositorySettingsModal = (props: ModalProps & {
|
||||
);
|
||||
};
|
||||
|
||||
const isSubmitting = updateGitRepositoryFetcher.state === 'submitting';
|
||||
const isLoading = updateGitRepositoryFetcher.state !== 'idle';
|
||||
const hasGitRepository = Boolean(gitRepository);
|
||||
const errors = updateGitRepositoryFetcher.data?.errors as (Error | string)[];
|
||||
const isDisabled = isSubmitting || Boolean(gitRepository);
|
||||
|
||||
useEffect(() => {
|
||||
if (errors && errors.length) {
|
||||
@ -85,12 +92,12 @@ export const GitRepositorySettingsModal = (props: ModalProps & {
|
||||
<ModalBody>
|
||||
<ErrorBoundary>
|
||||
<Tabs
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={isLoading || hasGitRepository}
|
||||
aria-label="Git repository settings tabs"
|
||||
selectedKey={selectedTab}
|
||||
onSelectionChange={(key: Key) => setTab(key as OauthProviderName)}
|
||||
>
|
||||
<TabItem key='github' title={<><i className="fa fa-github" /> GitHub</>}>
|
||||
<TabItem key='github' title={<TabPill><i className="fa fa-github" /> GitHub</TabPill>}>
|
||||
<PanelContainer className="pad pad-top-sm">
|
||||
<GitHubRepositorySetupFormGroup
|
||||
uri={gitRepository?.uri}
|
||||
@ -98,7 +105,7 @@ export const GitRepositorySettingsModal = (props: ModalProps & {
|
||||
/>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<TabItem key='gitlab' title={<><i className="fa fa-gitlab" /> GitLab</>}>
|
||||
<TabItem key='gitlab' title={<TabPill><i className="fa fa-gitlab" /> GitLab</TabPill>}>
|
||||
<PanelContainer className="pad pad-top-sm">
|
||||
<GitLabRepositorySetupFormGroup
|
||||
uri={gitRepository?.uri}
|
||||
@ -106,7 +113,7 @@ export const GitRepositorySettingsModal = (props: ModalProps & {
|
||||
/>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<TabItem key='custom' title={<><i className="fa fa-code-fork" /> Git</>}>
|
||||
<TabItem key='custom' title={<TabPill><i className="fa fa-code-fork" /> Git</TabPill>}>
|
||||
<PanelContainer className="pad pad-top-sm">
|
||||
<CustomRepositorySettingsFormGroup
|
||||
gitRepository={gitRepository}
|
||||
@ -136,9 +143,26 @@ export const GitRepositorySettingsModal = (props: ModalProps & {
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button type="submit" disabled={isDisabled} form={selectedTab} className="btn" data-testid="git-repository-settings-modal__sync-btn">
|
||||
Sync
|
||||
</button>
|
||||
{hasGitRepository ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => modalRef.current?.hide()}
|
||||
className="btn"
|
||||
data-testid="git-repository-settings-modal__sync-btn-close"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
form={selectedTab}
|
||||
className="btn"
|
||||
data-testid="git-repository-settings-modal__sync-btn"
|
||||
>
|
||||
Sync
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -40,6 +40,8 @@ export const GitStagingModal: FC<ModalProps> = ({
|
||||
const gitCommitFetcher = useFetcher<CommitToGitRepoResult>();
|
||||
const rollbackFetcher = useFetcher<GitRollbackChangesResult>();
|
||||
|
||||
const isLoadingGitChanges = gitChangesFetcher.state !== 'idle';
|
||||
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
@ -313,7 +315,11 @@ export const GitStagingModal: FC<ModalProps> = ({
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>No changes to commit.</>
|
||||
<div className="txt-sm faint italic">
|
||||
{isLoadingGitChanges ? <>
|
||||
<i className="fa fa-spinner fa-spin space-right" />
|
||||
Loading changes...</> : 'No changes to commit.'}
|
||||
</div>
|
||||
)}
|
||||
</gitCommitFetcher.Form>
|
||||
</ModalBody>
|
||||
@ -329,8 +335,9 @@ export const GitStagingModal: FC<ModalProps> = ({
|
||||
type="submit"
|
||||
form="git-staging-form"
|
||||
className="btn"
|
||||
disabled={gitCommitFetcher.state === 'submitting' || !hasChanges}
|
||||
disabled={gitCommitFetcher.state !== 'idle' || !hasChanges}
|
||||
>
|
||||
<i className={`fa ${gitCommitFetcher.state === 'idle' ? 'fa-check' : 'fa-spinner fa-spin'} space-right`} />
|
||||
Commit
|
||||
</button>
|
||||
</div>
|
||||
|
@ -259,6 +259,18 @@ const router = createMemoryRouter(
|
||||
path: 'commit',
|
||||
action: async (...args) => (await import('./routes/git-actions')).commitToGitRepoAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'branches',
|
||||
loader: async (...args) => (await import('./routes/git-actions')).gitBranchesLoader(...args),
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
loader: async (...args) => (await import('./routes/git-actions')).gitLogLoader(...args),
|
||||
},
|
||||
{
|
||||
path: 'fetch',
|
||||
action: async (...args) => (await import('./routes/git-actions')).gitFetchAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'branch',
|
||||
children: [
|
||||
|
@ -17,15 +17,12 @@ import {
|
||||
} from '../../models/workspace';
|
||||
import { fsClient } from '../../sync/git/fs-client';
|
||||
import { gitRollback } from '../../sync/git/git-rollback';
|
||||
import {
|
||||
getGitVCS,
|
||||
import GitVCS, {
|
||||
GIT_CLONE_DIR,
|
||||
GIT_INSOMNIA_DIR,
|
||||
GIT_INSOMNIA_DIR_NAME,
|
||||
GIT_INTERNAL_DIR,
|
||||
GitLogEntry,
|
||||
GitVCS,
|
||||
setGitVCS,
|
||||
} from '../../sync/git/git-vcs';
|
||||
import { MemClient } from '../../sync/git/mem-client';
|
||||
import { NeDBClient } from '../../sync/git/ne-db-client';
|
||||
@ -42,9 +39,7 @@ import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../a
|
||||
// Loaders
|
||||
export type GitRepoLoaderData = {
|
||||
branch: string;
|
||||
log: GitLogEntry[];
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
gitRepository: GitRepository | null;
|
||||
} | {
|
||||
errors: string[];
|
||||
@ -68,69 +63,62 @@ export const gitRepoLoader: LoaderFunction = async ({ params }): Promise<GitRepo
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
// Create FS client
|
||||
const baseDir = path.join(
|
||||
process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
|
||||
`version-control/git/${gitRepository._id}`,
|
||||
);
|
||||
if (!GitVCS.isInitializedForRepo(gitRepository._id)) {
|
||||
const baseDir = path.join(
|
||||
process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
|
||||
`version-control/git/${gitRepository._id}`,
|
||||
);
|
||||
|
||||
// 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(workspaceId, projectId);
|
||||
// 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(workspaceId, projectId);
|
||||
|
||||
// All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem
|
||||
const gitDataClient = fsClient(baseDir);
|
||||
// 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'));
|
||||
// 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,
|
||||
});
|
||||
|
||||
const vcs = new GitVCS();
|
||||
|
||||
// Init VCS
|
||||
const { credentials, uri } = gitRepository;
|
||||
if (gitRepository.needsFullClone) {
|
||||
await vcs.initFromClone({
|
||||
url: uri,
|
||||
gitCredentials: credentials,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
// 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,
|
||||
});
|
||||
|
||||
await models.gitRepository.update(gitRepository, {
|
||||
needsFullClone: false,
|
||||
});
|
||||
} else {
|
||||
await vcs.init({
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
});
|
||||
// Init VCS
|
||||
const { credentials, uri } = gitRepository;
|
||||
if (gitRepository.needsFullClone) {
|
||||
await GitVCS.initFromClone({
|
||||
repoId: gitRepository._id,
|
||||
url: uri,
|
||||
gitCredentials: credentials,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
});
|
||||
|
||||
await models.gitRepository.update(gitRepository, {
|
||||
needsFullClone: false,
|
||||
});
|
||||
} else {
|
||||
await GitVCS.init({
|
||||
repoId: gitRepository._id,
|
||||
uri,
|
||||
directory: GIT_CLONE_DIR,
|
||||
fs: routableFS,
|
||||
gitDirectory: GIT_INTERNAL_DIR,
|
||||
gitCredentials: credentials,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure basic info
|
||||
const { author, uri: gitUri } = gitRepository;
|
||||
await GitVCS.setAuthor(author.name, author.email);
|
||||
await GitVCS.addRemote(gitUri);
|
||||
}
|
||||
|
||||
// Configure basic info
|
||||
const { author, uri: gitUri } = gitRepository;
|
||||
await vcs.setAuthor(author.name, author.email);
|
||||
await vcs.addRemote(gitUri);
|
||||
|
||||
try {
|
||||
await vcs.fetch(false, 1, gitRepository?.credentials);
|
||||
} catch (e) {
|
||||
console.warn('Error fetching from remote');
|
||||
}
|
||||
|
||||
setGitVCS(vcs);
|
||||
|
||||
return {
|
||||
branch: await vcs.getBranch(),
|
||||
log: await vcs.log() || [],
|
||||
branches: await vcs.listBranches(),
|
||||
remoteBranches: await vcs.listRemoteBranches(),
|
||||
branch: await GitVCS.getBranch(),
|
||||
branches: await GitVCS.listBranches(),
|
||||
gitRepository: gitRepository,
|
||||
};
|
||||
} catch (e) {
|
||||
@ -141,6 +129,96 @@ export const gitRepoLoader: LoaderFunction = async ({ params }): Promise<GitRepo
|
||||
}
|
||||
};
|
||||
|
||||
export type GitBranchesLoaderData = {
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
} | {
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export const gitBranchesLoader: LoaderFunction = async ({ params }): Promise<GitBranchesLoaderData> => {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
invariant(typeof projectId === 'string', 'Project Id is required');
|
||||
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const branches = await GitVCS.listBranches();
|
||||
|
||||
const remoteBranches = await GitVCS.fetchRemoteBranches();
|
||||
|
||||
return {
|
||||
branches,
|
||||
remoteBranches,
|
||||
};
|
||||
};
|
||||
|
||||
export type GitFetchLoaderData = {
|
||||
errors: string[];
|
||||
} | {};
|
||||
|
||||
export const gitFetchAction: ActionFunction = async ({ params }): Promise<GitFetchLoaderData> => {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
invariant(typeof projectId === 'string', 'Project Id is required');
|
||||
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
await GitVCS.fetch({ singleBranch: true, depth: 1, credentials: gitRepository?.credentials });
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export type GitLogLoaderData = {
|
||||
log: GitLogEntry[];
|
||||
} | {
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export const gitLogLoader: LoaderFunction = async ({ params }): Promise<GitLogLoaderData> => {
|
||||
const { workspaceId, projectId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
|
||||
invariant(typeof projectId === 'string', 'Project Id is required');
|
||||
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
|
||||
if (!workspaceMeta?.gitRepositoryId) {
|
||||
return {
|
||||
errors: ['Workspace is not linked to a git repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const gitRepository = await models.gitRepository.getById(workspaceMeta?.gitRepositoryId);
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const log = await GitVCS.log({ depth: 35 });
|
||||
|
||||
return {
|
||||
log,
|
||||
};
|
||||
};
|
||||
|
||||
export interface GitChangesLoaderData {
|
||||
changes: GitChange[];
|
||||
branch: string;
|
||||
@ -164,11 +242,9 @@ export const gitChangesLoader: LoaderFunction = async ({ params }): Promise<GitC
|
||||
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
const branch = await GitVCS.getBranch();
|
||||
|
||||
const branch = await vcs.getBranch();
|
||||
|
||||
const { changes, statusNames } = await getGitChanges(vcs, workspace);
|
||||
const { changes, statusNames } = await getGitChanges(GitVCS, workspace);
|
||||
|
||||
return {
|
||||
branch,
|
||||
@ -302,82 +378,95 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
);
|
||||
return rootDirs.includes(models.workspace.type);
|
||||
};
|
||||
// If no workspace exists, user should be prompted to create a document
|
||||
|
||||
// Stop the DB from pushing updates to the UI temporarily
|
||||
const bufferId = await database.bufferChanges();
|
||||
let workspaceId = '';
|
||||
// If no workspace exists we create a new one
|
||||
if (!(await containsInsomniaWorkspaceDir(fsClient))) {
|
||||
// Create a new workspace
|
||||
const workspace = await models.workspace.create({
|
||||
name: repoSettingsPatch.uri.split('/').pop(),
|
||||
scope: WorkspaceScopeKeys.design,
|
||||
parentId: project._id,
|
||||
description: `Insomnia Workspace for ${repoSettingsPatch.uri}}`,
|
||||
});
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'no directory found'),
|
||||
providerName,
|
||||
});
|
||||
return {
|
||||
errors: ['No insomnia directory found in repository'],
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceBase = path.join(GIT_INSOMNIA_DIR, models.workspace.type);
|
||||
const workspaces = await fsClient.promises.readdir(workspaceBase);
|
||||
workspaceId = workspace._id;
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'no workspaces found'),
|
||||
providerName,
|
||||
});
|
||||
// Store GitRepository settings and set it as active
|
||||
await createGitRepository(workspace._id, repoSettingsPatch);
|
||||
} else {
|
||||
// Clone all entities from the repository
|
||||
const workspaceBase = path.join(GIT_INSOMNIA_DIR, models.workspace.type);
|
||||
const workspaces = await fsClient.promises.readdir(workspaceBase);
|
||||
|
||||
return {
|
||||
errors: ['No workspaces found in repository'],
|
||||
};
|
||||
}
|
||||
if (workspaces.length === 0) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'no workspaces found'),
|
||||
providerName,
|
||||
});
|
||||
|
||||
if (workspaces.length > 1) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found'),
|
||||
providerName,
|
||||
});
|
||||
|
||||
return {
|
||||
errors: ['Multiple workspaces found in repository. Expected one.'],
|
||||
};
|
||||
}
|
||||
|
||||
// Only one workspace
|
||||
const workspacePath = path.join(workspaceBase, workspaces[0]);
|
||||
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) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'workspace already exists'),
|
||||
providerName,
|
||||
});
|
||||
return {
|
||||
errors: [`Workspace ${existingWorkspace.name} already exists. Please delete it before cloning.`],
|
||||
};
|
||||
}
|
||||
|
||||
// Stop the DB from pushing updates to the UI temporarily
|
||||
const bufferId = await database.bufferChanges();
|
||||
|
||||
// Loop over all model folders in root
|
||||
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 fsClient.promises.readdir(modelDir)) {
|
||||
const docPath = path.join(modelDir, docFileName);
|
||||
const docYaml = await fsClient.promises.readFile(docPath);
|
||||
const doc: models.BaseModel = YAML.parse(docYaml.toString());
|
||||
if (isWorkspace(doc)) {
|
||||
// @ts-expect-error parentId can be string or null for a workspace
|
||||
doc.parentId = project?._id || null;
|
||||
doc.scope = WorkspaceScopeKeys.design;
|
||||
}
|
||||
await database.upsert(doc);
|
||||
return {
|
||||
errors: ['No workspaces found in repository'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Store GitRepository settings and set it as active
|
||||
await createGitRepository(workspace._id, repoSettingsPatch);
|
||||
if (workspaces.length > 1) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found'),
|
||||
providerName,
|
||||
});
|
||||
|
||||
return {
|
||||
errors: ['Multiple workspaces found in repository. Expected one.'],
|
||||
};
|
||||
}
|
||||
|
||||
// Only one workspace
|
||||
const workspacePath = path.join(workspaceBase, workspaces[0]);
|
||||
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) {
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncComplete, {
|
||||
...vcsSegmentEventProperties('git', 'clone', 'workspace already exists'),
|
||||
providerName,
|
||||
});
|
||||
return {
|
||||
errors: [`Workspace ${existingWorkspace.name} already exists. Please delete it before cloning.`],
|
||||
};
|
||||
}
|
||||
|
||||
// Loop over all model folders in root
|
||||
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 fsClient.promises.readdir(modelDir)) {
|
||||
const docPath = path.join(modelDir, docFileName);
|
||||
const docYaml = await fsClient.promises.readFile(docPath);
|
||||
const doc: models.BaseModel = YAML.parse(docYaml.toString());
|
||||
if (isWorkspace(doc)) {
|
||||
doc.parentId = project._id;
|
||||
doc.scope = WorkspaceScopeKeys.design;
|
||||
const workspace = await database.upsert(doc);
|
||||
workspaceId = workspace._id;
|
||||
} else {
|
||||
await database.upsert(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store GitRepository settings and set it as active
|
||||
await createGitRepository(workspace._id, repoSettingsPatch);
|
||||
}
|
||||
|
||||
// Flush DB changes
|
||||
await database.flushChanges(bufferId);
|
||||
@ -386,7 +475,9 @@ export const cloneGitRepoAction: ActionFunction = async ({
|
||||
providerName,
|
||||
});
|
||||
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/${ACTIVITY_SPEC}`);
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}`);
|
||||
};
|
||||
|
||||
export const updateGitRepoAction: ActionFunction = async ({
|
||||
@ -525,10 +616,8 @@ export const commitToGitRepoAction: ActionFunction = async ({
|
||||
const allModified = Boolean(formData.get('allModified'));
|
||||
const allUnversioned = Boolean(formData.get('allUnversioned'));
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
try {
|
||||
const { changes } = await getGitChanges(vcs, workspace);
|
||||
const { changes } = await getGitChanges(GitVCS, workspace);
|
||||
|
||||
const changesToCommit = changes.filter(change => {
|
||||
if (allModified && !change.added) {
|
||||
@ -540,10 +629,10 @@ export const commitToGitRepoAction: ActionFunction = async ({
|
||||
});
|
||||
|
||||
for (const item of changesToCommit) {
|
||||
item.status.includes('deleted') ? await vcs.remove(item.path) : await vcs.add(item.path);
|
||||
item.status.includes('deleted') ? await GitVCS.remove(item.path) : await GitVCS.add(item.path);
|
||||
}
|
||||
|
||||
await vcs.commit(message);
|
||||
await GitVCS.commit(message);
|
||||
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'commit'), providerName });
|
||||
@ -579,11 +668,9 @@ export const createNewGitBranchAction: ActionFunction = async ({ request, params
|
||||
const branch = formData.get('branch');
|
||||
invariant(typeof branch === 'string', 'Branch name is required');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
try {
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
await vcs.checkout(branch);
|
||||
await GitVCS.checkout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'create_branch'), providerName });
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Something went wrong while creating a new branch';
|
||||
@ -623,18 +710,9 @@ export const checkoutGitBranchAction: ActionFunction = async ({
|
||||
const branch = formData.get('branch');
|
||||
invariant(typeof branch === 'string', 'Branch name is required');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
try {
|
||||
await vcs.fetch(false, 1, repo?.credentials);
|
||||
} catch (e) {
|
||||
console.warn('Error fetching from remote');
|
||||
}
|
||||
const bufferId = await database.bufferChanges();
|
||||
try {
|
||||
await vcs.checkout(branch);
|
||||
// Fetch the last 20 commits for the branch
|
||||
await vcs.fetch(true, 20, repo?.credentials);
|
||||
await GitVCS.checkout(branch);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : err;
|
||||
return {
|
||||
@ -643,7 +721,7 @@ export const checkoutGitBranchAction: ActionFunction = async ({
|
||||
}
|
||||
|
||||
if (workspaceMeta) {
|
||||
const log = (await vcs.log()) || [];
|
||||
const log = (await GitVCS.log({ depth: 1 })) || [];
|
||||
|
||||
const author = log[0] ? log[0].commit.author : null;
|
||||
const cachedGitLastCommitTime = author ? author.timestamp * 1000 : null;
|
||||
@ -681,19 +759,17 @@ export const mergeGitBranchAction: ActionFunction = async ({ request, params }):
|
||||
const repo = await models.gitRepository.getById(repoId);
|
||||
invariant(repo, 'Git Repository not found');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
const formData = await request.formData();
|
||||
const branch = formData.get('branch');
|
||||
invariant(typeof branch === 'string', 'Branch name is required');
|
||||
|
||||
try {
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
await vcs.merge(branch);
|
||||
await GitVCS.merge(branch);
|
||||
// Apparently merge doesn't update the working dir so need to checkout too
|
||||
const bufferId = await database.bufferChanges();
|
||||
|
||||
await vcs.checkout(branch);
|
||||
await GitVCS.checkout(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'checkout_branch'), providerName });
|
||||
await database.flushChanges(bufferId, true);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'merge_branch'), providerName });
|
||||
@ -726,8 +802,6 @@ export const deleteGitBranchAction: ActionFunction = async ({ request, params })
|
||||
const repo = await models.gitRepository.getById(repoId);
|
||||
invariant(repo, 'Git Repository not found');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const branch = formData.get('branch');
|
||||
@ -735,7 +809,7 @@ export const deleteGitBranchAction: ActionFunction = async ({ request, params })
|
||||
|
||||
try {
|
||||
const providerName = getOauth2FormatName(repo?.credentials);
|
||||
await vcs.deleteBranch(branch);
|
||||
await GitVCS.deleteBranch(branch);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'delete_branch'), providerName });
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
@ -772,12 +846,10 @@ export const pushToGitRemoteAction: ActionFunction = async ({
|
||||
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
// Check if there is anything to push
|
||||
let canPush = false;
|
||||
try {
|
||||
canPush = await vcs.canPush(gitRepository.credentials);
|
||||
canPush = await GitVCS.canPush(gitRepository.credentials);
|
||||
} catch (err) {
|
||||
return { errors: ['Error Pushing Repository'] };
|
||||
}
|
||||
@ -791,7 +863,7 @@ export const pushToGitRemoteAction: ActionFunction = async ({
|
||||
const bufferId = await database.bufferChanges();
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
try {
|
||||
await vcs.push(gitRepository.credentials);
|
||||
await GitVCS.push(gitRepository.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'), providerName });
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown Error';
|
||||
@ -836,20 +908,18 @@ export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
const bufferId = await database.bufferChanges();
|
||||
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
|
||||
try {
|
||||
await vcs.fetch(false, 1, gitRepository?.credentials);
|
||||
await GitVCS.fetch({ singleBranch: true, depth: 1, credentials: gitRepository?.credentials });
|
||||
} catch (e) {
|
||||
console.warn('Error fetching from remote');
|
||||
}
|
||||
|
||||
try {
|
||||
await vcs.pull(gitRepository.credentials);
|
||||
await GitVCS.pull(gitRepository.credentials);
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'pull'), providerName });
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown Error';
|
||||
@ -874,7 +944,7 @@ export interface GitChange {
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
async function getGitVCSPaths(vcs: GitVCS) {
|
||||
async function getGitVCSPaths(vcs: typeof GitVCS) {
|
||||
const gitFS = vcs.getFs();
|
||||
|
||||
const fs = 'promises' in gitFS ? gitFS.promises : gitFS;
|
||||
@ -897,7 +967,7 @@ async function getGitVCSPaths(vcs: GitVCS) {
|
||||
return Array.from(uniquePaths).sort();
|
||||
}
|
||||
|
||||
async function getGitChanges(vcs: GitVCS, workspace: Workspace) {
|
||||
async function getGitChanges(vcs: typeof GitVCS, workspace: Workspace) {
|
||||
// Cache status names
|
||||
const docs = await database.withDescendants(workspace);
|
||||
const allPaths = await getGitVCSPaths(vcs);
|
||||
@ -909,7 +979,8 @@ async function getGitChanges(vcs: GitVCS, workspace: Workspace) {
|
||||
}
|
||||
// Create status items
|
||||
const items: Record<string, GitChange> = {};
|
||||
const log = (await vcs.log(1)) || [];
|
||||
const log = (await vcs.log({ depth: 1 })) || [];
|
||||
|
||||
for (const gitPath of allPaths) {
|
||||
const status = await vcs.status(gitPath);
|
||||
if (status === 'unmodified') {
|
||||
@ -977,14 +1048,12 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request
|
||||
|
||||
invariant(gitRepository, 'Git Repository not found');
|
||||
|
||||
const vcs = getGitVCS();
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const paths = [...formData.getAll('paths')] as string[];
|
||||
const changeType = formData.get('changeType') as string;
|
||||
try {
|
||||
const { changes } = await getGitChanges(vcs, workspace);
|
||||
const { changes } = await getGitChanges(GitVCS, workspace);
|
||||
|
||||
const files = changes
|
||||
.filter(i => changeType === 'modified' ? !i.status.includes('added') : i.status.includes('added'))
|
||||
@ -997,7 +1066,7 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request
|
||||
status: i.status,
|
||||
}));
|
||||
|
||||
await gitRollback(vcs, files);
|
||||
await gitRollback(GitVCS, files);
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Error while rolling back changes';
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user