diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index af3298e0a2..a4319e6c75 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -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 => invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh), + ): Promise => + invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh), insecureReadFile: options => invokeWithNormalizedError('insecureReadFile', options), insecureReadFileWithEncoding: options => invokeWithNormalizedError('insecureReadFileWithEncoding', options), secureReadFile: options => invokeWithNormalizedError('secureReadFile', options), diff --git a/packages/insomnia/src/main/git-service.ts b/packages/insomnia/src/main/git-service.ts index 00934e9089..a2b84db99c 100644 --- a/packages/insomnia/src/main/git-service.ts +++ b/packages/insomnia/src/main/git-service.ts @@ -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(); + +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 { 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[0]) => discardChangesAction(options), ); - ipcMainHandle('git.abortMerge', _ => abortMergeAction()); + ipcMainHandle('git.abortMerge', (_, options: Parameters[0]) => abortMergeAction(options)); ipcMainHandle('git.gitStatus', (_, options: Parameters[0]) => gitStatusAction(options)); ipcMainHandle('git.diff', () => diff()); ipcMainHandle('git.stageChanges', (_, options: Parameters[0]) => diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx index e178001938..95879eda45 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx @@ -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 (
diff --git a/packages/insomnia/src/sync/git/repo-file-watcher.ts b/packages/insomnia/src/sync/git/repo-file-watcher.ts index 13b90cc9be..4bba7f51fc 100644 --- a/packages/insomnia/src/sync/git/repo-file-watcher.ts +++ b/packages/insomnia/src/sync/git/repo-file-watcher.ts @@ -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()); diff --git a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx index f365f628bf..2bce4ad79c 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx @@ -459,6 +459,7 @@ export const GitProjectSyncDropdown: FC = ({ gitRepository, activeProject }); }, onCancelUnresolved: () => { + window.main.git.abortMerge({ projectId }); closeGitProjectStagingModalRef.current?.(); setIsPulling(false); showToast({ diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx index 07cff3f43f..a325f37670 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx @@ -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 ; } - const shouldShowCloudSyncDropdown = models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId; + const shouldShowCloudSyncDropdown = + models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId; if (shouldShowCloudSyncDropdown) { return ; } 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 ( diff --git a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx index 987df57667..50a1ae5b3d 100644 --- a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx @@ -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( diff --git a/packages/insomnia/src/ui/hooks/use-git-file-issues.ts b/packages/insomnia/src/ui/hooks/use-git-file-issues.ts index 3fdb898dc3..ab7842125d 100644 --- a/packages/insomnia/src/ui/hooks/use-git-file-issues.ts +++ b/packages/insomnia/src/ui/hooks/use-git-file-issues.ts @@ -19,6 +19,7 @@ const mapIssuesByWorkspaceId = (issues: WorkspaceFileIssue[]) => { export interface GitFileIssuesValue { issuesByWorkspaceId: Record; + conflictsSuppressed: boolean; } const GitFileIssuesContext = createContext(undefined); @@ -43,6 +44,7 @@ export const useProjectGitFileIssues = ({ gitRepositoryId?: string | null; }): GitFileIssuesValue => { const [issuesByWorkspaceId, setIssuesByWorkspaceId] = useState>({}); + 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( () => ({ issuesByWorkspaceId, + conflictsSuppressed, }), - [issuesByWorkspaceId], + [issuesByWorkspaceId, conflictsSuppressed], ); };