mirror of
https://github.com/Kong/insomnia
synced 2024-11-06 22:03:16 +00:00
Support for resolving conflicts when pulling from remote git branch. [INS-4550] (#8118)
Support for resolving conflicts when pulling from remote git branch.
This commit is contained in:
parent
67fa4a1b96
commit
c38597187e
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -21,6 +21,8 @@
|
||||
"inso",
|
||||
"libcurl",
|
||||
"svgr",
|
||||
"unstage",
|
||||
"Unstaged",
|
||||
"xmark"
|
||||
],
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import * as git from 'isomorphic-git';
|
||||
import path from 'path';
|
||||
import { parse } from 'yaml';
|
||||
import { parse, stringify } from 'yaml';
|
||||
|
||||
import type { MergeConflict } from '../types';
|
||||
import { httpClient } from './http-client';
|
||||
import { convertToPosixSep } from './path-sep';
|
||||
import { gitCallbacks } from './utils';
|
||||
@ -624,12 +625,186 @@ export class GitVCS {
|
||||
}
|
||||
|
||||
async pull(gitCredentials?: GitCredentials | null) {
|
||||
const changes = await this.status();
|
||||
const hasUncommittedChanges = changes.staged.length > 0 || changes.unstaged.length > 0;
|
||||
if (hasUncommittedChanges) {
|
||||
throw new Error('Cannot pull with uncommitted changes, please commit local changes first.');
|
||||
}
|
||||
console.log('[git] Pull remote=origin', await this.getCurrentBranch());
|
||||
return git.pull({
|
||||
...this._baseOpts,
|
||||
...gitCallbacks(gitCredentials),
|
||||
remote: 'origin',
|
||||
singleBranch: true,
|
||||
}).catch(
|
||||
async err => {
|
||||
if (err instanceof git.Errors.MergeConflictError) {
|
||||
const oursBranch = await this.getCurrentBranch();
|
||||
const theirsBranch = `origin/${oursBranch}`;
|
||||
|
||||
return await this._collectMergeConflicts(
|
||||
err,
|
||||
oursBranch,
|
||||
theirsBranch,
|
||||
gitCredentials
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async _collectMergeConflicts(
|
||||
mergeConflictError: InstanceType<typeof git.Errors.MergeConflictError>,
|
||||
oursBranch: string,
|
||||
theirsBranch: string,
|
||||
gitCredentials?: GitCredentials | null,
|
||||
) {
|
||||
const {
|
||||
filepaths, bothModified, deleteByUs, deleteByTheirs,
|
||||
} = mergeConflictError.data;
|
||||
if (filepaths.length) {
|
||||
const mergeConflicts: MergeConflict[] = [];
|
||||
const conflictPathsObj = {
|
||||
bothModified,
|
||||
deleteByUs,
|
||||
deleteByTheirs,
|
||||
};
|
||||
const conflictTypeList: (keyof typeof conflictPathsObj)[] = [
|
||||
'bothModified',
|
||||
'deleteByUs',
|
||||
'deleteByTheirs',
|
||||
];
|
||||
|
||||
const oursHeadCommitOid = await git.resolveRef({
|
||||
...this._baseOpts,
|
||||
...gitCallbacks(gitCredentials),
|
||||
ref: oursBranch,
|
||||
});
|
||||
|
||||
const theirsHeadCommitOid = await git.resolveRef({
|
||||
...this._baseOpts,
|
||||
...gitCallbacks(gitCredentials),
|
||||
ref: theirsBranch,
|
||||
});
|
||||
|
||||
const _baseOpts = this._baseOpts;
|
||||
|
||||
function readBlob(filepath: string, oid: string) {
|
||||
return git.readBlob({
|
||||
..._baseOpts,
|
||||
...gitCallbacks(gitCredentials),
|
||||
oid,
|
||||
filepath,
|
||||
}).then(
|
||||
({ blob, oid: blobId }) => ({
|
||||
blobContent: parse(Buffer.from(blob).toString('utf8')),
|
||||
blobId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function readOursBlob(filepath: string) {
|
||||
return readBlob(filepath, oursHeadCommitOid);
|
||||
}
|
||||
|
||||
function readTheirsBlob(filepath: string) {
|
||||
return readBlob(filepath, theirsHeadCommitOid);
|
||||
}
|
||||
|
||||
for (const conflictType of conflictTypeList) {
|
||||
const conflictPaths = conflictPathsObj[conflictType];
|
||||
const message = {
|
||||
'bothModified': 'both modified',
|
||||
'deleteByUs': 'you deleted and they modified',
|
||||
'deleteByTheirs': 'they deleted and you modified',
|
||||
}[conflictType];
|
||||
for (const conflictPath of conflictPaths) {
|
||||
let mineBlobContent = null;
|
||||
let mineBlobId = null;
|
||||
|
||||
let theirsBlobContent = null;
|
||||
let theirsBlobId = null;
|
||||
|
||||
if (conflictType !== 'deleteByUs') {
|
||||
const {
|
||||
blobContent,
|
||||
blobId,
|
||||
} = await readOursBlob(conflictPath);
|
||||
mineBlobContent = blobContent;
|
||||
mineBlobId = blobId;
|
||||
}
|
||||
|
||||
if (conflictType !== 'deleteByTheirs') {
|
||||
const {
|
||||
blobContent,
|
||||
blobId,
|
||||
} = await readTheirsBlob(conflictPath);
|
||||
theirsBlobContent = blobContent;
|
||||
theirsBlobId = blobId;
|
||||
}
|
||||
const name = mineBlobContent?.name || theirsBlobContent?.name || '';
|
||||
|
||||
mergeConflicts.push({
|
||||
key: conflictPath,
|
||||
name,
|
||||
message,
|
||||
mineBlob: mineBlobId,
|
||||
theirsBlob: theirsBlobId,
|
||||
choose: mineBlobId || theirsBlobId,
|
||||
mineBlobContent,
|
||||
theirsBlobContent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new MergeConflictError('Need to solve merge conflicts first', {
|
||||
conflicts: mergeConflicts,
|
||||
labels: {
|
||||
ours: `${oursBranch} ${oursHeadCommitOid}`,
|
||||
theirs: `${theirsBranch} ${theirsHeadCommitOid}`,
|
||||
},
|
||||
commitMessage: `Merge branch '${theirsBranch}' into ${oursBranch}`,
|
||||
commitParent: [oursHeadCommitOid, theirsHeadCommitOid],
|
||||
});
|
||||
|
||||
} else {
|
||||
throw new Error('Merge conflict filepaths is of length 0');
|
||||
}
|
||||
}
|
||||
|
||||
// create a commit after resolving merge conflicts
|
||||
async continueMerge({
|
||||
handledMergeConflicts,
|
||||
commitMessage,
|
||||
commitParent,
|
||||
}: {
|
||||
gitCredentials?: GitCredentials | null;
|
||||
handledMergeConflicts: MergeConflict[];
|
||||
commitMessage: string;
|
||||
commitParent: string[];
|
||||
}) {
|
||||
console.log('[git] continue to merge after resolving merge conflicts', await this.getCurrentBranch());
|
||||
for (const conflict of handledMergeConflicts) {
|
||||
assertIsPromiseFsClient(this._baseOpts.fs);
|
||||
if (conflict.theirsBlobContent) {
|
||||
await this._baseOpts.fs.promises.writeFile(
|
||||
conflict.key,
|
||||
stringify(conflict.theirsBlobContent),
|
||||
);
|
||||
await git.add({ ...this._baseOpts, filepath: conflict.key });
|
||||
} else {
|
||||
await this._baseOpts.fs.promises.unlink(
|
||||
conflict.key,
|
||||
);
|
||||
await git.remove({ ...this._baseOpts, filepath: conflict.key });
|
||||
}
|
||||
}
|
||||
await git.commit({
|
||||
...this._baseOpts,
|
||||
message: commitMessage,
|
||||
parent: commitParent,
|
||||
});
|
||||
}
|
||||
|
||||
@ -811,5 +986,27 @@ export class GitVCS {
|
||||
return newBranches;
|
||||
}
|
||||
}
|
||||
export class MergeConflictError extends Error {
|
||||
constructor(msg: string, data: {
|
||||
conflicts: MergeConflict[];
|
||||
labels: {
|
||||
ours: string;
|
||||
theirs: string;
|
||||
};
|
||||
commitMessage: string;
|
||||
commitParent: string[];
|
||||
}) {
|
||||
super(msg);
|
||||
this.data = data;
|
||||
}
|
||||
data;
|
||||
name = 'MergeConflictError';
|
||||
}
|
||||
|
||||
function assertIsPromiseFsClient(fs: git.FsClient): asserts fs is git.PromiseFsClient {
|
||||
if (!('promises' in fs)) {
|
||||
throw new Error('Expected fs to be of PromiseFsClient');
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitVCS();
|
||||
|
@ -33,6 +33,9 @@ interface FSDir {
|
||||
|
||||
type FSEntry = FSDir | FSFile | FSLink;
|
||||
|
||||
/**
|
||||
* In-memory file system client
|
||||
*/
|
||||
export class MemClient {
|
||||
__fs: FSEntry;
|
||||
__ino: 0;
|
||||
|
@ -12,6 +12,11 @@ import parseGitPath from './parse-git-path';
|
||||
import Stat from './stat';
|
||||
import { SystemError } from './system-error';
|
||||
|
||||
/**
|
||||
* A fs client to access workspace data stored in NeDB as files.
|
||||
* Used by isomorphic-git
|
||||
* https://isomorphic-git.org/docs/en/fs#implementing-your-own-fs
|
||||
*/
|
||||
export class NeDBClient {
|
||||
_workspaceId: string;
|
||||
_projectId: string;
|
||||
@ -138,6 +143,7 @@ export class NeDBClient {
|
||||
if (root === null && id === null && type === null) {
|
||||
otherFolders = [GIT_INSOMNIA_DIR_NAME];
|
||||
} else if (id === null && type === null) {
|
||||
// TODO: It doesn't scale if we add another model which can be sync in the future
|
||||
otherFolders = [
|
||||
models.workspace.type,
|
||||
models.environment.type,
|
||||
|
@ -6,22 +6,26 @@ import { useInterval } from 'react-use';
|
||||
|
||||
import type { GitRepository } from '../../../models/git-repository';
|
||||
import { deleteGitRepository } from '../../../models/helpers/git-repository-operations';
|
||||
import { MergeConflictError } from '../../../sync/git/git-vcs';
|
||||
import { getOauth2FormatName } from '../../../sync/git/utils';
|
||||
import type { MergeConflict } from '../../../sync/types';
|
||||
import {
|
||||
checkGitCanPush,
|
||||
checkGitChanges,
|
||||
continueMerge,
|
||||
type GitFetchLoaderData,
|
||||
type GitRepoLoaderData,
|
||||
type GitStatusResult,
|
||||
type PullFromGitRemoteResult,
|
||||
pullFromGitRemote,
|
||||
type PushToGitRemoteResult,
|
||||
} from '../../routes/git-actions';
|
||||
import { Icon } from '../icon';
|
||||
import { showAlert } from '../modals';
|
||||
import { showAlert, showModal } from '../modals';
|
||||
import { GitBranchesModal } from '../modals/git-branches-modal';
|
||||
import { GitLogModal } from '../modals/git-log-modal';
|
||||
import { GitRepositorySettingsModal } from '../modals/git-repository-settings-modal';
|
||||
import { GitStagingModal } from '../modals/git-staging-modal';
|
||||
import { SyncMergeModal } from '../modals/sync-merge-modal';
|
||||
|
||||
interface Props {
|
||||
gitRepository: GitRepository | null;
|
||||
@ -42,17 +46,17 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
const [isGitStagingModalOpen, setIsGitStagingModalOpen] = useState(false);
|
||||
|
||||
const gitPushFetcher = useFetcher<PushToGitRemoteResult>();
|
||||
const gitPullFetcher = useFetcher<PullFromGitRemoteResult>();
|
||||
const gitCheckoutFetcher = useFetcher();
|
||||
const gitRepoDataFetcher = useFetcher<GitRepoLoaderData>();
|
||||
const gitFetchFetcher = useFetcher<GitFetchLoaderData>();
|
||||
const gitStatusFetcher = useFetcher<GitStatusResult>();
|
||||
|
||||
const loadingPush = gitPushFetcher.state === 'loading';
|
||||
const loadingPull = gitPullFetcher.state === 'loading';
|
||||
const loadingFetch = gitFetchFetcher.state === 'loading';
|
||||
const loadingStatus = gitStatusFetcher.state === 'loading';
|
||||
|
||||
const [isPulling, setIsPulling] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
gitRepository?.uri &&
|
||||
@ -60,6 +64,7 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
gitRepoDataFetcher.state === 'idle' &&
|
||||
!gitRepoDataFetcher.data
|
||||
) {
|
||||
// file://./../../routes/git-actions.tsx#gitRepoLoader
|
||||
gitRepoDataFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/repo`);
|
||||
}
|
||||
}, [
|
||||
@ -76,6 +81,7 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFetchGitRepoStatus) {
|
||||
// file://./../../routes/git-actions.tsx#gitStatusAction
|
||||
gitStatusFetcher.submit({}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/status`,
|
||||
method: 'post',
|
||||
@ -122,16 +128,6 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
}
|
||||
}, [gitRepoDataFetcher.data]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [...(gitPullFetcher.data?.errors ?? [])];
|
||||
if (errors.length > 0) {
|
||||
showAlert({
|
||||
title: 'Pull Failed',
|
||||
message: errors.join('\n'),
|
||||
});
|
||||
}
|
||||
}, [gitPullFetcher.data?.errors]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors = [...(gitCheckoutFetcher.data?.errors ?? [])];
|
||||
if (errors.length > 0) {
|
||||
@ -168,7 +164,7 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
gitFetchFetcher.state === 'loading' ||
|
||||
gitCheckoutFetcher.state === 'loading' ||
|
||||
gitPushFetcher.state === 'loading' ||
|
||||
gitPullFetcher.state === 'loading';
|
||||
isPulling;
|
||||
|
||||
const isSynced = Boolean(gitRepository?.uri && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data));
|
||||
|
||||
@ -195,17 +191,46 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
},
|
||||
{
|
||||
id: 'pull',
|
||||
icon: loadingPull ? 'refresh' : 'cloud-download',
|
||||
icon: isPulling ? 'refresh' : 'cloud-download',
|
||||
label: 'Pull',
|
||||
isDisabled: false,
|
||||
action: async () => {
|
||||
gitPullFetcher.submit(
|
||||
{},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/pull`,
|
||||
method: 'post',
|
||||
try {
|
||||
setIsPulling(true);
|
||||
await pullFromGitRemote(workspaceId).finally(() => {
|
||||
setIsPulling(false);
|
||||
revalidate();
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof MergeConflictError) {
|
||||
const data = err.data;
|
||||
showModal(SyncMergeModal, {
|
||||
conflicts: data.conflicts,
|
||||
labels: data.labels,
|
||||
handleDone: (conflicts?: MergeConflict[]) => {
|
||||
if (Array.isArray(conflicts) && conflicts.length > 0) {
|
||||
setIsPulling(true);
|
||||
continueMerge({
|
||||
handledMergeConflicts: conflicts,
|
||||
commitMessage: data.commitMessage,
|
||||
commitParent: data.commitParent,
|
||||
}).finally(() => {
|
||||
setIsPulling(false);
|
||||
revalidate();
|
||||
});
|
||||
} else {
|
||||
// user aborted merge, do nothing
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
showAlert({
|
||||
title: 'Pull Failed',
|
||||
message: err.message,
|
||||
bodyClassName: 'whitespace-break-spaces',
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -313,6 +338,7 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
|
||||
isActive: branch === currentBranch,
|
||||
icon: 'code-branch',
|
||||
action: async () => {
|
||||
// file://./../../routes/git-actions.tsx#gitCheckoutAction
|
||||
gitCheckoutFetcher.submit(
|
||||
{
|
||||
branch,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { forwardRef, type ReactNode, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { Modal, type ModalHandle, type ModalProps } from '../base/modal';
|
||||
@ -11,6 +12,7 @@ export interface AlertModalOptions {
|
||||
addCancel?: boolean;
|
||||
okLabel?: React.ReactNode;
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
export interface AlertModalHandle {
|
||||
show: (options: AlertModalOptions) => void;
|
||||
@ -23,29 +25,39 @@ export const AlertModal = forwardRef<AlertModalHandle, ModalProps>((_, ref) => {
|
||||
message: '',
|
||||
addCancel: false,
|
||||
okLabel: '',
|
||||
bodyClassName: '',
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: ({ title, message, addCancel, onConfirm, okLabel }) => {
|
||||
show: ({ title, message, addCancel, onConfirm, okLabel, bodyClassName = '' }) => {
|
||||
setState({
|
||||
title,
|
||||
message,
|
||||
addCancel,
|
||||
okLabel,
|
||||
onConfirm,
|
||||
bodyClassName,
|
||||
});
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), []);
|
||||
|
||||
const { message, title, addCancel, okLabel } = state;
|
||||
const { message, title, addCancel, okLabel, bodyClassName } = state;
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
|
||||
<ModalBody className="wide pad">{message}</ModalBody>
|
||||
<ModalBody
|
||||
className={classnames([
|
||||
'wide',
|
||||
'pad',
|
||||
bodyClassName,
|
||||
])}
|
||||
>
|
||||
{message}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div>
|
||||
{addCancel ? (
|
||||
|
@ -81,12 +81,15 @@ const LocalBranchItem = ({
|
||||
<Button
|
||||
className="px-4 py-1 font-semibold border border-solid border-[--hl-md] flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
isDisabled={isCurrent}
|
||||
onPress={() => checkoutBranchFetcher.submit({
|
||||
branch,
|
||||
}, {
|
||||
method: 'POST',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branch/checkout`,
|
||||
})}
|
||||
onPress={() => {
|
||||
// file://./../../routes/git-actions.tsx#checkoutGitBranchAction
|
||||
checkoutBranchFetcher.submit({
|
||||
branch,
|
||||
}, {
|
||||
method: 'POST',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branch/checkout`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon={checkoutBranchFetcher.state !== 'idle' ? 'spinner' : 'turn-up'} className={`w-5 ${checkoutBranchFetcher.state !== 'idle' ? 'animate-spin' : 'rotate-90'}`} />
|
||||
Checkout
|
||||
|
@ -23,6 +23,7 @@ export const GitLogModal: FC<Props> = ({ onClose }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (gitLogFetcher.state === 'idle' && !gitLogFetcher.data) {
|
||||
// file://./../../routes/git-actions.tsx#gitLogLoader
|
||||
gitLogFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/log`);
|
||||
}
|
||||
}, [organizationId, projectId, workspaceId, gitLogFetcher]);
|
||||
|
@ -47,6 +47,7 @@ export const GitRepositoryCloneModal = (props: ModalProps) => {
|
||||
...credentials,
|
||||
},
|
||||
{
|
||||
// file://./../../../routes/git-actions.tsx#cloneGitRepoAction
|
||||
action: `/organization/${organizationId}/project/${projectId}/git/clone`,
|
||||
method: 'post',
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ export const GitStagingModal: FC<{ onClose: () => void }> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (gitChangesFetcher.state === 'idle' && !gitChangesFetcher.data) {
|
||||
// file://./../../routes/git-actions.tsx#gitChangesLoader
|
||||
gitChangesFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/changes`);
|
||||
}
|
||||
}, [organizationId, projectId, workspaceId, gitChangesFetcher]);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Differ, Viewer } from 'json-diff-kit';
|
||||
import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
||||
import { Button, Dialog, Form, GridList, GridListItem, Heading, Modal, ModalOverlay, Radio, RadioGroup } from 'react-aria-components';
|
||||
|
||||
import type { MergeConflict } from '../../../sync/types';
|
||||
@ -33,12 +33,17 @@ export const SyncMergeModal = forwardRef<SyncMergeModalHandle>((_, ref) => {
|
||||
labels: { ours: '', theirs: '' },
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => setState({
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
conflicts: [],
|
||||
isOpen: false,
|
||||
labels: { ours: '', theirs: '' },
|
||||
}),
|
||||
});
|
||||
setSelectedConflict(null);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: reset,
|
||||
show: ({ conflicts, labels, handleDone }) => {
|
||||
setState({
|
||||
conflicts,
|
||||
@ -46,12 +51,14 @@ export const SyncMergeModal = forwardRef<SyncMergeModalHandle>((_, ref) => {
|
||||
isOpen: true,
|
||||
labels,
|
||||
});
|
||||
// select the first conflict by default
|
||||
setSelectedConflict(conflicts?.[0] || null);
|
||||
|
||||
window.main.trackSegmentEvent({
|
||||
event: SegmentEvent.syncConflictResolutionStart,
|
||||
});
|
||||
},
|
||||
}), []);
|
||||
}), [reset]);
|
||||
|
||||
const { conflicts, handleDone } = state;
|
||||
|
||||
@ -61,11 +68,7 @@ export const SyncMergeModal = forwardRef<SyncMergeModalHandle>((_, ref) => {
|
||||
<ModalOverlay
|
||||
isOpen={state.isOpen}
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && setState({
|
||||
conflicts: [],
|
||||
isOpen: false,
|
||||
labels: { ours: '', theirs: '' },
|
||||
});
|
||||
!isOpen && reset();
|
||||
|
||||
!isOpen && handleDone?.();
|
||||
}}
|
||||
@ -74,11 +77,7 @@ export const SyncMergeModal = forwardRef<SyncMergeModalHandle>((_, ref) => {
|
||||
>
|
||||
<Modal
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && setState({
|
||||
conflicts: [],
|
||||
isOpen: false,
|
||||
labels: { ours: '', theirs: '' },
|
||||
});
|
||||
!isOpen && reset();
|
||||
|
||||
!isOpen && handleDone?.();
|
||||
}}
|
||||
@ -116,11 +115,7 @@ export const SyncMergeModal = forwardRef<SyncMergeModalHandle>((_, ref) => {
|
||||
});
|
||||
}
|
||||
|
||||
setState({
|
||||
conflicts: [],
|
||||
isOpen: false,
|
||||
labels: { ours: '', theirs: '' },
|
||||
});
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<div className='grid [grid-template-columns:300px_1fr] h-full overflow-hidden divide-x divide-solid divide-[--hl-md] gap-2'>
|
||||
|
@ -877,13 +877,6 @@ async function renderApp() {
|
||||
path: 'changes',
|
||||
loader: async (...args) =>
|
||||
(await import('./routes/git-actions')).gitChangesLoader(...args),
|
||||
shouldRevalidate: ({ formAction }) => {
|
||||
if (formAction?.includes('git')) {
|
||||
return true;
|
||||
}
|
||||
// disable revalidation for this loader, we will fetch this loader periodically through fetcher.load in component
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
@ -925,11 +918,6 @@ async function renderApp() {
|
||||
action: async (...args) =>
|
||||
(await import('./routes/git-actions')).resetGitRepoAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'pull',
|
||||
action: async (...args) =>
|
||||
(await import('./routes/git-actions')).pullFromGitRemoteAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'push',
|
||||
action: async (...args) =>
|
||||
|
@ -27,6 +27,7 @@ import { shallowClone } from '../../sync/git/shallow-clone';
|
||||
import {
|
||||
getOauth2FormatName,
|
||||
} from '../../sync/git/utils';
|
||||
import type { MergeConflict } from '../../sync/types';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import {
|
||||
SegmentEvent,
|
||||
@ -1266,14 +1267,7 @@ export const pushToGitRemoteAction: ActionFunction = async ({
|
||||
return {};
|
||||
};
|
||||
|
||||
export interface PullFromGitRemoteResult {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
params,
|
||||
}): Promise<PullFromGitRemoteResult> => {
|
||||
const { workspaceId } = params;
|
||||
export const pullFromGitRemote = async (workspaceId: string) => {
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
@ -1292,16 +1286,6 @@ export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
|
||||
const providerName = getOauth2FormatName(gitRepository.credentials);
|
||||
|
||||
try {
|
||||
await GitVCS.fetch({
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
credentials: gitRepository?.credentials,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error fetching from remote', e);
|
||||
}
|
||||
|
||||
try {
|
||||
await GitVCS.pull(gitRepository.credentials);
|
||||
window.main.trackSegmentEvent({
|
||||
@ -1312,7 +1296,7 @@ export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Errors.HttpError) {
|
||||
return { errors: [`${err.message}, ${err.data.response}`] };
|
||||
err = new Error(`${err.message}, ${err.data.response}`);
|
||||
}
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown Error';
|
||||
window.main.trackSegmentEvent({
|
||||
@ -1321,14 +1305,35 @@ export const pullFromGitRemoteAction: ActionFunction = async ({
|
||||
vcsSegmentEventProperties('git', 'pull', errorMessage),
|
||||
});
|
||||
|
||||
return {
|
||||
errors: [`${errorMessage}`],
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
|
||||
await database.flushChanges(bufferId);
|
||||
};
|
||||
|
||||
return {};
|
||||
export const continueMerge = async (
|
||||
{
|
||||
handledMergeConflicts,
|
||||
commitMessage,
|
||||
commitParent,
|
||||
}: {
|
||||
handledMergeConflicts: MergeConflict[];
|
||||
commitMessage: string;
|
||||
commitParent: string[];
|
||||
}
|
||||
) => {
|
||||
// filter in conflicts that user has chosen to keep 'theirs'
|
||||
handledMergeConflicts = handledMergeConflicts.filter(conflict => conflict.choose !== conflict.mineBlob);
|
||||
|
||||
const bufferId = await database.bufferChanges();
|
||||
|
||||
await GitVCS.continueMerge({
|
||||
handledMergeConflicts,
|
||||
commitMessage,
|
||||
commitParent,
|
||||
});
|
||||
|
||||
await database.flushChanges(bufferId);
|
||||
};
|
||||
|
||||
export interface GitChange {
|
||||
|
Loading…
Reference in New Issue
Block a user