mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-04 11:52:33 -04:00
fix: user can not resolve conflict in app (#9872)
* fix: conflict ux
* fix
* fix
(cherry picked from commit 133f633aeb)
This commit is contained in:
@@ -196,7 +196,7 @@ const git: GitServiceAPI = {
|
||||
pullFromGitRemote: options => invokeWithNormalizedError('git.pullFromGitRemote', options),
|
||||
continueMerge: options => invokeWithNormalizedError('git.continueMerge', options),
|
||||
discardChanges: options => invokeWithNormalizedError('git.discardChanges', options),
|
||||
abortMerge: () => invokeWithNormalizedError('git.abortMerge'),
|
||||
abortMerge: options => invokeWithNormalizedError('git.abortMerge', options),
|
||||
gitStatus: options => invokeWithNormalizedError('git.gitStatus', options),
|
||||
diff: () => invokeWithNormalizedError('git.diff'),
|
||||
multipleCommitToGitRepo: options => invokeWithNormalizedError('git.multipleCommitToGitRepo', options),
|
||||
@@ -265,7 +265,8 @@ const main: Window['main'] = {
|
||||
requestId: string,
|
||||
authentication: AuthTypeOAuth2,
|
||||
forceRefresh?: boolean,
|
||||
): Promise<OAuth2Token | undefined> => invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh),
|
||||
): Promise<OAuth2Token | undefined> =>
|
||||
invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh),
|
||||
insecureReadFile: options => invokeWithNormalizedError('insecureReadFile', options),
|
||||
insecureReadFileWithEncoding: options => invokeWithNormalizedError('insecureReadFileWithEncoding', options),
|
||||
secureReadFile: options => invokeWithNormalizedError('secureReadFile', options),
|
||||
|
||||
@@ -61,7 +61,7 @@ import GitVCS, {
|
||||
import { MemClient } from '../sync/git/mem-client';
|
||||
import { NeDBClient } from '../sync/git/ne-db-client';
|
||||
import { projectRoutableFSClient } from '../sync/git/project-routable-fs-client';
|
||||
import { repoFileWatcherRegistry } from '../sync/git/repo-file-watcher';
|
||||
import { createElectronNotifier, RepoFileWatcherRegistry, type WatcherNotifier } from '../sync/git/repo-file-watcher';
|
||||
import { routableFSClient } from '../sync/git/routable-fs-client';
|
||||
import { shallowClone } from '../sync/git/shallow-clone';
|
||||
import type { AutoResolvedConflict, MergeConflict } from '../sync/types';
|
||||
@@ -72,6 +72,35 @@ import { ipcMainHandle } from './ipc/electron';
|
||||
// Initialize Git Remote Providers on module load
|
||||
initializeGitRemoteProviders();
|
||||
|
||||
/**
|
||||
* Set of repo IDs for which conflict problems should be suppressed in the
|
||||
* file-problems-changed IPC broadcast. Active while the user is resolving
|
||||
* conflicts via SyncMergeModal so the generic "CLI conflict" blocking modal
|
||||
* does not appear on top of the interactive resolver.
|
||||
*/
|
||||
const suppressedConflictRepos = new Set<string>();
|
||||
|
||||
const _electronNotifier = createElectronNotifier();
|
||||
const conflictFilteringNotifier: WatcherNotifier = {
|
||||
onDbSynced: () => _electronNotifier.onDbSynced(),
|
||||
onProblemsChanged: payload => {
|
||||
_electronNotifier.onProblemsChanged({
|
||||
...payload,
|
||||
conflictsSuppressed: suppressedConflictRepos.has(payload.repoId),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const repoFileWatcherRegistry = new RepoFileWatcherRegistry(conflictFilteringNotifier);
|
||||
|
||||
function suppressConflictProblems(repoId: string): void {
|
||||
suppressedConflictRepos.add(repoId);
|
||||
}
|
||||
|
||||
function clearConflictSuppression(repoId: string): void {
|
||||
suppressedConflictRepos.delete(repoId);
|
||||
}
|
||||
|
||||
type PushPull = 'push' | 'pull';
|
||||
type VCSAction =
|
||||
| PushPull
|
||||
@@ -1543,6 +1572,7 @@ export const resetGitRepoAction = async ({ projectId, workspaceId }: { projectId
|
||||
await services.gitRepository.remove(repo);
|
||||
// Stop the file watcher for this repository (project-scoped flow only).
|
||||
repoFileWatcherRegistry.stopWatcher(repo._id);
|
||||
clearConflictSuppression(repo._id);
|
||||
|
||||
await database.flushChanges(flushId);
|
||||
|
||||
@@ -1982,6 +2012,7 @@ export const mergeGitBranch = async ({
|
||||
const bufferId = await database.bufferChanges();
|
||||
|
||||
try {
|
||||
suppressConflictProblems(gitRepository._id);
|
||||
await GitVCS.merge({
|
||||
theirsBranch,
|
||||
allowUncommittedChangesBeforeMerge,
|
||||
@@ -1993,6 +2024,7 @@ export const mergeGitBranch = async ({
|
||||
// Import all YAML files from disk into the DB after merge + checkout
|
||||
const gitRepoId = gitRepository._id;
|
||||
await repoFileWatcherRegistry.importAllFiles(gitRepoId);
|
||||
clearConflictSuppression(gitRepository._id);
|
||||
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'merge_branch'),
|
||||
@@ -2013,8 +2045,10 @@ export const mergeGitBranch = async ({
|
||||
return {};
|
||||
} catch (err) {
|
||||
if (err instanceof MergeConflictError) {
|
||||
// Keep suppression active — user will resolve via SyncMergeModal.
|
||||
return err.data;
|
||||
}
|
||||
clearConflictSuppression(gitRepository._id);
|
||||
let errorMessage = getErrorMessage(err);
|
||||
|
||||
if (err instanceof Errors.HttpError) {
|
||||
@@ -2231,9 +2265,12 @@ export async function fetchGitRemoteBranches({
|
||||
}
|
||||
|
||||
export async function pullFromGitRemote({ projectId, workspaceId }: { projectId: string; workspaceId?: string }) {
|
||||
let repoId: string | null = null;
|
||||
try {
|
||||
await assertBranchOnOrigin('pull');
|
||||
const gitRepository = await getGitRepository({ projectId, workspaceId });
|
||||
repoId = gitRepository._id;
|
||||
suppressConflictProblems(repoId);
|
||||
invariant(gitRepository.credentialsId, 'Git Credentials ID is required');
|
||||
const credentials = await services.gitCredentials.getById(gitRepository.credentialsId);
|
||||
invariant(credentials, 'Git Credentials not found');
|
||||
@@ -2243,6 +2280,7 @@ export async function pullFromGitRemote({ projectId, workspaceId }: { projectId:
|
||||
|
||||
// Import all YAML files from disk into the DB after pull
|
||||
await repoFileWatcherRegistry.importAllFiles(gitRepository._id);
|
||||
clearConflictSuppression(repoId);
|
||||
|
||||
trackSegmentEvent(SegmentEvent.vcsAction, {
|
||||
...vcsSegmentEventProperties('git', 'pull'),
|
||||
@@ -2266,9 +2304,13 @@ export async function pullFromGitRemote({ projectId, workspaceId }: { projectId:
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof MergeConflictError) {
|
||||
// Keep suppression active — user will resolve via SyncMergeModal.
|
||||
// clearConflictSuppression is called by continueMerge or abortMergeAction.
|
||||
return err.data;
|
||||
}
|
||||
|
||||
if (repoId) clearConflictSuppression(repoId);
|
||||
|
||||
if (
|
||||
err instanceof Errors.UserCanceledError ||
|
||||
(err instanceof Errors.HttpError && (err.data.statusCode === 401 || err.data.statusCode === 403))
|
||||
@@ -2334,6 +2376,9 @@ export const continueMerge = async ({
|
||||
|
||||
// Import all YAML files from disk into the DB after merge resolution
|
||||
await repoFileWatcherRegistry.importAllFiles(gitRepository._id);
|
||||
// Files are clean now — lift the conflict suppression so any remaining
|
||||
// issues (parse errors etc.) are reported normally.
|
||||
clearConflictSuppression(gitRepository._id);
|
||||
|
||||
const log = (await GitVCS.log({ depth: 1 })) || [];
|
||||
|
||||
@@ -2414,7 +2459,9 @@ export const discardChangesAction = async ({
|
||||
}
|
||||
};
|
||||
|
||||
export const abortMergeAction = async () => {
|
||||
export const abortMergeAction = async ({ projectId, workspaceId }: { projectId: string; workspaceId?: string }) => {
|
||||
const gitRepository = await getGitRepository({ projectId, workspaceId });
|
||||
clearConflictSuppression(gitRepository._id);
|
||||
return GitVCS.abortMerge();
|
||||
};
|
||||
|
||||
@@ -2869,7 +2916,9 @@ export async function runAllGitRepoMigrations(): Promise<MigrationSummary> {
|
||||
|
||||
const ts = () => new Date().toISOString();
|
||||
const projectList = gitProjects.map(p => `"${p.name}"`).join(', ');
|
||||
logs.push(`${ts()} [INFO] Starting migration v${CURRENT_MIGRATION_VERSION} for ${gitProjects.length} repo(s): ${projectList}`);
|
||||
logs.push(
|
||||
`${ts()} [INFO] Starting migration v${CURRENT_MIGRATION_VERSION} for ${gitProjects.length} repo(s): ${projectList}`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
gitProjects.map(async project => {
|
||||
@@ -3036,7 +3085,7 @@ export const registerGitServiceAPI = () => {
|
||||
ipcMainHandle('git.discardChanges', (_, options: Parameters<typeof discardChangesAction>[0]) =>
|
||||
discardChangesAction(options),
|
||||
);
|
||||
ipcMainHandle('git.abortMerge', _ => abortMergeAction());
|
||||
ipcMainHandle('git.abortMerge', (_, options: Parameters<typeof abortMergeAction>[0]) => abortMergeAction(options));
|
||||
ipcMainHandle('git.gitStatus', (_, options: Parameters<typeof gitStatusAction>[0]) => gitStatusAction(options));
|
||||
ipcMainHandle('git.diff', () => diff());
|
||||
ipcMainHandle('git.stageChanges', (_, options: Parameters<typeof stageChangesAction>[0]) =>
|
||||
|
||||
@@ -401,7 +401,7 @@ const Component = () => {
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
const { issuesByWorkspaceId } = useGitFileIssues();
|
||||
const { issuesByWorkspaceId, conflictsSuppressed } = useGitFileIssues();
|
||||
const currentIssue = issuesByWorkspaceId[workspaceId];
|
||||
|
||||
const handleBackToList = () => {
|
||||
@@ -414,7 +414,9 @@ const Component = () => {
|
||||
};
|
||||
|
||||
const modalText = currentIssue ? workspaceFileIssueModalText[currentIssue.kind] : null;
|
||||
const isIssueModalOpen = Boolean(currentIssue && modalText);
|
||||
const isIssueModalOpen = Boolean(
|
||||
currentIssue && modalText && !(currentIssue.kind === 'conflict' && conflictsSuppressed),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden" data-testid="workspace-page">
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface FileProblemsChangedPayload {
|
||||
repoId: string;
|
||||
problems: FileIssue[];
|
||||
workspaceIssues: WorkspaceFileIssue[];
|
||||
/** True when the main process is suppressing conflict display (e.g. SyncMergeModal is open). */
|
||||
conflictsSuppressed: boolean;
|
||||
}
|
||||
|
||||
/** Compute a SHA-256 hex digest of a string. */
|
||||
@@ -792,6 +794,7 @@ class RepoFileWatcher {
|
||||
repoId: this.repoId,
|
||||
problems: this.getProblems(),
|
||||
workspaceIssues: this.getWorkspaceIssues(),
|
||||
conflictsSuppressed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -900,7 +903,7 @@ export class RepoFileWatcherRegistry {
|
||||
}
|
||||
|
||||
/** Default notifier that broadcasts to all Electron BrowserWindows. */
|
||||
function createElectronNotifier(): WatcherNotifier {
|
||||
export function createElectronNotifier(): WatcherNotifier {
|
||||
return {
|
||||
onDbSynced: () => {
|
||||
for (const w of BrowserWindow.getAllWindows()) {
|
||||
@@ -914,5 +917,3 @@ function createElectronNotifier(): WatcherNotifier {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const repoFileWatcherRegistry = new RepoFileWatcherRegistry(createElectronNotifier());
|
||||
|
||||
@@ -459,6 +459,7 @@ export const GitProjectSyncDropdown: FC<Props> = ({ gitRepository, activeProject
|
||||
});
|
||||
},
|
||||
onCancelUnresolved: () => {
|
||||
window.main.git.abortMerge({ projectId });
|
||||
closeGitProjectStagingModalRef.current?.();
|
||||
setIsPulling(false);
|
||||
showToast({
|
||||
|
||||
@@ -22,20 +22,24 @@ export const WorkspaceSyncDropdown: FC = () => {
|
||||
}
|
||||
|
||||
const isLocalProject =
|
||||
!models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId && !models.project.isGitProject(activeProject);
|
||||
!models.project.isRemoteProject(activeProject) &&
|
||||
!activeWorkspaceMeta?.gitRepositoryId &&
|
||||
!models.project.isGitProject(activeProject);
|
||||
|
||||
if (isLocalProject) {
|
||||
return <LocalProjectBar />;
|
||||
}
|
||||
|
||||
const shouldShowCloudSyncDropdown = models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId;
|
||||
const shouldShowCloudSyncDropdown =
|
||||
models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId;
|
||||
|
||||
if (shouldShowCloudSyncDropdown) {
|
||||
return <SyncDropdown key={activeWorkspace?._id} workspace={activeWorkspace} project={activeProject} />;
|
||||
}
|
||||
|
||||
const shouldShowGitSyncDropdown =
|
||||
features.gitSync.enabled && (activeWorkspaceMeta?.gitRepositoryId || !models.project.isRemoteProject(activeProject));
|
||||
features.gitSync.enabled &&
|
||||
(activeWorkspaceMeta?.gitRepositoryId || !models.project.isRemoteProject(activeProject));
|
||||
if (shouldShowGitSyncDropdown) {
|
||||
if (models.project.isGitProject(activeProject)) {
|
||||
return (
|
||||
|
||||
@@ -166,7 +166,7 @@ const LocalBranchItem = ({
|
||||
},
|
||||
onCancelUnresolved: () => {
|
||||
// user aborted merge
|
||||
window.main.git.abortMerge();
|
||||
window.main.git.abortMerge({ projectId });
|
||||
// TODO: the abortMerge method provided by isomorphic-git is unreliable
|
||||
// clean up any partial merges here
|
||||
reject(
|
||||
|
||||
@@ -19,6 +19,7 @@ const mapIssuesByWorkspaceId = (issues: WorkspaceFileIssue[]) => {
|
||||
|
||||
export interface GitFileIssuesValue {
|
||||
issuesByWorkspaceId: Record<string, WorkspaceFileIssue>;
|
||||
conflictsSuppressed: boolean;
|
||||
}
|
||||
|
||||
const GitFileIssuesContext = createContext<GitFileIssuesValue | undefined>(undefined);
|
||||
@@ -43,6 +44,7 @@ export const useProjectGitFileIssues = ({
|
||||
gitRepositoryId?: string | null;
|
||||
}): GitFileIssuesValue => {
|
||||
const [issuesByWorkspaceId, setIssuesByWorkspaceId] = useState<Record<string, WorkspaceFileIssue>>({});
|
||||
const [conflictsSuppressed, setConflictsSuppressed] = useState(false);
|
||||
|
||||
const loadIssues = useCallback(async () => {
|
||||
if (!projectId || !gitRepositoryId) {
|
||||
@@ -76,6 +78,7 @@ export const useProjectGitFileIssues = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setConflictsSuppressed(payload.conflictsSuppressed);
|
||||
setIssuesByWorkspaceId(mapIssuesByWorkspaceId(payload.workspaceIssues));
|
||||
});
|
||||
}, [gitRepositoryId]);
|
||||
@@ -83,7 +86,8 @@ export const useProjectGitFileIssues = ({
|
||||
return useMemo<GitFileIssuesValue>(
|
||||
() => ({
|
||||
issuesByWorkspaceId,
|
||||
conflictsSuppressed,
|
||||
}),
|
||||
[issuesByWorkspaceId],
|
||||
[issuesByWorkspaceId, conflictsSuppressed],
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user