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:
James Gatz 2023-03-02 14:33:34 +01:00 committed by GitHub
parent 3738fb56f4
commit 6463d7095f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 811 additions and 402 deletions

View File

@ -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,
});

View File

@ -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);
});
});
});

View File

@ -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();

View File

@ -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>
);
};

View File

@ -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">&nbsp;</th>
</tr>
</thead>

View File

@ -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">

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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: [

View File

@ -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 {