Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b70e6a
initial support for canonical repo output (#9739)
gatzjames Apr 8, 2026
cf8f0a0
feat: enhance git repository migration with concurrency guard and sym…
pavkout Apr 13, 2026
c1ae98b
feat: enhance git repository migration with config sanitization and f…
pavkout Apr 13, 2026
8792aff
feat: implement repo migration version tracking and improve migration…
pavkout Apr 15, 2026
578a36a
feat: add runAllGitRepoMigrations function and migration view for Git…
pavkout Apr 23, 2026
b440ff0
fix: reset initial migration status to 'default' in MigrationView com…
pavkout Apr 23, 2026
23cc425
refactor: simplify MigrationView component and update navigation logic
pavkout Apr 23, 2026
2ad12da
refactor: remove legacy directory structure migration from loadGitRep…
pavkout Apr 23, 2026
c0dd4c2
feat: enhance runAllGitRepoMigrations to return logs and improve erro…
pavkout Apr 23, 2026
09e055b
feat: update runAllGitRepoMigrations to return detailed logs and fail…
pavkout Apr 23, 2026
bd4ada6
feat: optimize runAllGitRepoMigrations by batch-fetching git reposito…
pavkout Apr 23, 2026
eb90b95
feat: introduce CURRENT_MIGRATION_VERSION constant for migration trac…
pavkout Apr 23, 2026
62736e8
feat: handle failed projects in runAllGitRepoMigrations by converting…
pavkout Apr 23, 2026
d734986
feat: integrate CURRENT_MIGRATION_VERSION for migration tracking and …
pavkout Apr 23, 2026
6e7fecb
feat: reorder import statements in ProjectSettingsForm for consistency
pavkout Apr 23, 2026
d3961af
feat: update MigrationStatus type and related logic for better error …
pavkout Apr 23, 2026
e731116
feat: enhance migration logging with detailed error stack and include…
pavkout Apr 23, 2026
66bb5a4
feat: simplify migration logging messages for clarity and consistency
pavkout Apr 23, 2026
eb43b45
feat: improve migration check logic to prioritize version stamp over …
pavkout Apr 23, 2026
21eadd7
feat: add tests for migrateRepoStructureIfNeeded function to ensure m…
pavkout Apr 23, 2026
733091b
feat: update migration logic to re-run when old git/ directory exists…
pavkout Apr 23, 2026
8fb989d
test: update migration tests to ensure directory existence checks are…
pavkout Apr 23, 2026
1f8c1e8
refactor: remove redundant useEffect for localStorage in Component
pavkout Apr 23, 2026
7cd4763
feat: enhance path validation in runAllGitRepoMigrations to prevent p…
pavkout Apr 23, 2026
fe6a458
feat: enhance path handling in migration functions to prevent directo…
pavkout Apr 23, 2026
a19b7d5
feat: enhance directory traversal protection in moveDirectoryContents…
pavkout Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/insomnia/src/entry.preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const git: GitServiceAPI = {
getGitProviderEmails: options => ipcRenderer.invoke('git.getGitProviderEmails', options),
getCurrentBranchByRepositoryId: options => ipcRenderer.invoke('git.getCurrentBranchByRepositoryId', options),
getBranchRemoteInfo: options => ipcRenderer.invoke('git.getBranchRemoteInfo', options),
runAllGitRepoMigrations: () => ipcRenderer.invoke('git.runAllGitRepoMigrations'),
};

const llm: LLMConfigServiceAPI = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function init(): BaseGitRepository {
hasUncommittedChanges: false,
hasUnpushedChanges: false,
uriNeedsMigration: true,
repoMigrationVersion: 0,
};
}

Expand Down Expand Up @@ -60,6 +61,14 @@ export interface BaseGitRepository {
cachedGitLastAuthor: string | null;
hasUnpushedChanges: boolean;
uriNeedsMigration: boolean;
/**
* Tracks which version of the on-disk repo structure migration has run.
* When an older app version processes this document via docUpdate it will
* prune this field (since its init() doesn't include it), which causes the
* migration to re-run on the next upgrade — exactly the desired behaviour
* for version-rollback scenarios.
*/
repoMigrationVersion: number;
}

export const isGitRepository = (model: Pick<BaseModel, 'type'>): model is GitRepository => model.type === type;
Expand Down
103 changes: 95 additions & 8 deletions packages/insomnia/src/main/git-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ import { fromUrl } from 'hosted-git-info';
import { Errors, type PromiseFsClient } from 'isomorphic-git';
import YAML, { parse } from 'yaml';

import type { GitRemoteProviderType, GitRepository, Workspace, WorkspaceMeta, WorkspaceScope } from '~/insomnia-data';
import type {
GitProject,
GitRemoteProviderType,
GitRepository,
Workspace,
WorkspaceMeta,
WorkspaceScope,
} from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { GitVCSOperationErrors } from '~/sync/git/git-vcs-operation-errors';
import {
Expand All @@ -38,7 +45,7 @@ import { migrateToLatestYaml } from '../common/insomnia-schema-migrations';
import { insomniaSchemaTypeToScope } from '../common/insomnia-v5';
import * as models from '../models';
import { fsClient } from '../sync/git/fs-client';
import { migrateRepoStructureIfNeeded } from '../sync/git/git-repo-migration';
import { CURRENT_MIGRATION_VERSION, migrateRepoStructureIfNeeded } from '../sync/git/git-repo-migration';
import GitVCS, {
fetchRemoteBranches,
GIT_CLONE_DIR,
Expand Down Expand Up @@ -472,12 +479,6 @@ export async function loadGitRepository({ projectId, workspaceId }: { projectId:
`version-control/git/${gitRepository._id}`,
);

// Migrate old directory structure (git/ → .git/, other/ → root) if needed.
// Only runs for the modern project-scoped flow (no workspaceId).
if (!workspaceId) {
await migrateRepoStructureIfNeeded(baseDir, projectId, gitRepository._id);
}

const bufferId = await database.bufferChanges();
const fsClient = await getGitFSClient({ gitRepositoryId: gitRepository._id, projectId, workspaceId });

Expand Down Expand Up @@ -2840,6 +2841,90 @@ async function getCurrentBranchByRepositoryId({
});
}

export interface MigrationSummary {
logs: string[];
failedProjects: { id: string; name: string }[];
}

export async function runAllGitRepoMigrations(): Promise<MigrationSummary> {
const logs: string[] = [];
const failedProjects: { id: string; name: string }[] = [];

const allProjects = await services.project.all();
const gitProjects = allProjects.filter(
(p): p is GitProject => models.project.isGitProject(p) && !models.project.isEmptyGitProject(p),
);

if (gitProjects.length === 0) return { logs, failedProjects };

// Batch-fetch all git repositories in one query instead of N individual lookups.
const repoIds = gitProjects.map(p => p.gitRepositoryId);
const gitRepositories = await database.find<GitRepository>(models.gitRepository.type, {
_id: { $in: repoIds },
});
const repoById = new Map(gitRepositories.map(r => [r._id, r]));

// Hoist — same value for every repo.
const baseDataPath = process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData');

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

await Promise.all(
gitProjects.map(async project => {
const gitRepository = repoById.get(project.gitRepositoryId);
if (!gitRepository) return;

const repoId = gitRepository._id;
const logger = (level: 'info' | 'warn' | 'error', message: string) => {
logs.push(`${ts()} [${level.toUpperCase()}] ["${project.name}"] ${message}`);
};

const allowedBase = path.resolve(baseDataPath);
const baseDir = path.resolve(allowedBase, 'version-control', 'git', repoId);
if (!baseDir.startsWith(allowedBase + path.sep)) {
logger('warn', `Skipping repo with unsafe path — repoId may contain path traversal: ${repoId}`);
return;
}

const success = await migrateRepoStructureIfNeeded(baseDir, project._id, repoId, logger);
if (!success) {
failedProjects.push({ id: project._id, name: project.name });
}
}),
);

// In case we have any failed projects, convert them to local projects.
await Promise.all(
failedProjects.map(async ({ id, name }) => {
logs.push(`${ts()} [INFO] ["${name}"] Converting to local project`);
try {
const project = await services.project.getById(id);
if (!project || !project.gitRepositoryId) {
logs.push(`${ts()} [WARN] ["${name}"] Project not found or already local — skipping`);
return;
}

const gitRepository = await services.gitRepository.getById(project.gitRepositoryId);
if (gitRepository) {
await services.gitRepository.remove(gitRepository);
logs.push(`${ts()} [INFO] ["${name}"] Removed git repository ${project.gitRepositoryId}`);
}

await services.project.update(project, { name, gitRepositoryId: null });
logs.push(`${ts()} [INFO] ["${name}"] Successfully converted to local`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error && err.stack ? `\n${err.stack}` : '';
logs.push(`${ts()} [ERROR] ["${name}"] Failed to convert to local: ${message}${stack}`);
}
}),
);

return { logs, failedProjects };
}

export interface GitServiceAPI {
loadGitRepository: typeof loadGitRepository;
getGitBranches: typeof getGitBranches;
Expand Down Expand Up @@ -2883,6 +2968,7 @@ export interface GitServiceAPI {
getGitProviderEmails: typeof getGitProviderEmails;
listGitProviders: typeof listGitProviders;
getBranchRemoteInfo: typeof getBranchRemoteInfo;
runAllGitRepoMigrations: typeof runAllGitRepoMigrations;
}

export const registerGitServiceAPI = () => {
Expand Down Expand Up @@ -2989,4 +3075,5 @@ export const registerGitServiceAPI = () => {
ipcMainHandle('git.getBranchRemoteInfo', (_, options: Parameters<typeof getBranchRemoteInfo>[0]) =>
getBranchRemoteInfo(options),
);
ipcMainHandle('git.runAllGitRepoMigrations', () => runAllGitRepoMigrations());
};
1 change: 1 addition & 0 deletions packages/insomnia/src/main/ipc/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type HandleChannels =
| 'git.pullFromGitRemote'
| 'git.pushToGitRemote'
| 'git.resetGitRepo'
| 'git.runAllGitRepoMigrations'
| 'git.getCurrentBranchByRepositoryId'
| 'git.getBranchRemoteInfo'
| 'git.stageChanges'
Expand Down
209 changes: 209 additions & 0 deletions packages/insomnia/src/routes/git-migration.$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { useState } from 'react';
import { Link } from 'react-router';

import { Button } from '~/basic-components/button';
import { CopyButton } from '~/ui/components/base/copy-button';
import { InsomniaLogo } from '~/ui/components/insomnia-icon';
import { TrailLinesContainer } from '~/ui/components/trail-lines-container';
import git_for_all from '~/ui/images/onboarding/git_for_all.png';

import { CURRENT_MIGRATION_VERSION } from '../sync/git/git-migration-version';

type MigrationStatus = 'default' | 'running' | 'completed' | 'partiallyCompleted' | 'error';

const MigrationView = () => {
const [status, setStatus] = useState<MigrationStatus>('default');
const [migrationLogs, setMigrationLogs] = useState<string[]>([]);
const [failedProjects, setFailedProjects] = useState<{ id: string; name: string }[]>([]);

const handleMigration = () => {
setStatus('running');
window.main.git
.runAllGitRepoMigrations()
.then((result: { logs: string[]; failedProjects: { id: string; name: string }[] }) => {
setMigrationLogs(result.logs);
setFailedProjects(result.failedProjects);
setStatus(result.failedProjects.length > 0 ? 'partiallyCompleted' : 'completed');
localStorage.setItem('gitMigrationScreenShown', String(CURRENT_MIGRATION_VERSION));
})
.catch((err: unknown) => {
const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred.';
setMigrationLogs(prev => [...prev, `[ERROR] ${errorMsg}`]);
setStatus('error');
});
};

const isUpdateRunning = status === 'running';
const isUpdateCompletedSuccessfully = status === 'completed';
const isUpdateErrored = status === 'error';
const isUpdateCompletedWithErrors = status === 'partiallyCompleted';

return (
<div className="flex h-full min-h-[500px] w-[600px] flex-col items-center justify-center">
<div className="relative flex w-full flex-col items-center justify-center gap-(--padding-sm) rounded-md border border-solid border-(--hl-sm) bg-(--hl-xs) p-8">
<div className="relative flex min-h-[150px] flex-col justify-between gap-4 text-left text-(--color-font)">
<h1 className="text-xl">
{isUpdateCompletedSuccessfully
? 'Update Successful'
: isUpdateErrored
? 'Something went wrong'
: isUpdateCompletedWithErrors
? 'Update successful with some warnings'
: 'Required file system update'}
</h1>

{isUpdateCompletedSuccessfully ? (
<p className="text-sm">
Your file system has been successfully updated. Now you can explore all of your Insomnia files on your
local system and use git on your CLI to manage changes.
</p>
) : isUpdateCompletedWithErrors ? (
<>
<p className="text-sm">
The following Git Sync projects were disconnected from remote as a result of the file system update:
</p>
<ol className="ml-3 list-disc text-sm">
{failedProjects.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ol>
<p className="text-sm">
These projects will need to be reconnected to the git remote server to continue with push, pull, and
fetch actions.
</p>
</>
) : isUpdateErrored ? (
<>
<p className="text-sm">We hit an unexpected error while updating your file system. Please try again.</p>
<p className="text-sm text-[#828282]">
If the issue persists, please{' '}
<Link className="underline" to="https://github.com/Kong/insomnia/issues/new/choose">
raise a support ticket.
</Link>{' '}
You may also re-install the previous version by following the steps{' '}
<Link className="underline" to="https://insomnia.rest/changelog#12.5.0">
here
</Link>
.
</p>
</>
) : (
<>
<p className="text-sm">
In order to continue with this update, we need to adjust your local file system. This is required to
enable managing Insomnia changes using git on the CLI.
</p>
<p className="text-sm">
{isUpdateRunning
? 'Note: Your data is safe and the update only takes seconds.'
: 'Note: This update does NOT change your data and only affects how your local Insomnia files are stored.'}
</p>
</>
)}

<div className="flex h-[32px] w-full justify-end">
{isUpdateCompletedSuccessfully ? (
<Link
className="h-[32px] rounded-xs border border-solid border-(--hl-md) bg-(--color-surprise) px-3 py-2 text-sm text-(--color-font-surprise) transition-colors hover:bg-(--color-surprise)/90 hover:no-underline"
to="/organization"
>
Open Insomnia
</Link>
) : isUpdateCompletedWithErrors ? (
<div className="flex h-[32px] w-full items-center justify-between gap-3">
<CopyButton
className="flex h-[32px] w-[150px] items-center gap-2 rounded-xs p-2 text-sm"
content={migrationLogs.length > 0 ? migrationLogs.join('\n') : 'No logs available.'}
title="Copy error logs to clipboard"
>
<i className="fa fa-copy" />
Copy Error Logs
</CopyButton>
<Link
className="h-[32px] rounded-xs border border-solid border-(--hl-md) bg-(--color-surprise) px-3 py-2 text-sm text-(--color-font-surprise) transition-colors hover:bg-(--color-surprise)/90 hover:no-underline"
to="/organization"
>
Open Insomnia
</Link>
</div>
) : isUpdateErrored ? (
<div className="flex h-[32px] w-full items-center justify-between gap-3">
<CopyButton
className="flex h-[32px] w-[150px] items-center gap-2 rounded-xs p-2 text-sm"
content={migrationLogs.length > 0 ? migrationLogs.join('\n') : 'No logs available.'}
title="Copy error logs to clipboard"
>
<i className="fa fa-copy" />
Copy Error Logs
</CopyButton>
<Button
className="h-[32px] rounded-xs border border-solid border-(--hl-md) bg-(--color-surprise) px-3 py-2 text-sm text-(--color-font-surprise) transition-colors hover:bg-(--color-surprise)/90 hover:no-underline"
onClick={handleMigration}
isDisabled={isUpdateRunning}
>
{isUpdateRunning ? 'Updating...' : 'Retry Update'}
</Button>
</div>
) : (
<Button
className="h-[32px] rounded-xs border border-solid border-(--hl-md) bg-(--color-surprise) px-3 py-2 text-sm text-(--color-font-surprise) transition-colors hover:bg-(--color-surprise)/90 hover:no-underline"
onClick={handleMigration}
isDisabled={isUpdateRunning}
>
{isUpdateRunning ? 'Updating...' : 'Update Now'}
</Button>
)}
</div>
</div>
</div>
</div>
);
};

const Component = () => {
const [showMigrationView, setShowMigrationView] = useState(false);

return (
<div className="relative flex h-full w-full bg-(--color-bg) text-left">
<TrailLinesContainer>
{showMigrationView ? (
<MigrationView />
) : (
<div className="flex h-full min-h-[500px] w-[600px] flex-col items-center justify-center">
<div className="relative flex w-full flex-col items-center justify-center gap-(--padding-sm) rounded-md border border-solid border-(--hl-sm) bg-(--hl-xs) p-(--padding-lg) pt-12">
<InsomniaLogo className="absolute top-0 left-1/2 h-16 w-16 translate-x-[-50%] translate-y-[-50%] transform" />
<div className="flex flex-col items-center gap-6 text-(--color-font)">
<h1 className="text-center text-xl">What's new in v12.6.0</h1>
<div className="relative flex h-96 flex-col gap-4 bg-(--color-bg) p-4 text-left">
<h1 className="flex justify-between text-lg">
<span>Manage Insomnia changes using git CLI actions</span>
</h1>
<div className="flex flex-1 flex-col items-center gap-3 overflow-y-auto">
<p className="text-sm text-[#828282]">
Now you can use traditional git actions on your CLI to manage changes to your Git Sync projects.
</p>
<div className="h-48 flex-1">
<img className="aspect-auto max-h-48" src={git_for_all} />
</div>
</div>
</div>
<div className="flex w-full justify-end">
<Button
className="h-[32px] rounded-xs border border-solid border-(--hl-md) bg-(--color-surprise) px-3 py-2 text-sm text-(--color-font-surprise) transition-colors hover:bg-(--color-surprise)/90"
onClick={() => {
setShowMigrationView(true);
}}
>
Continue
</Button>
</div>
</div>
</div>
</div>
)}
</TrailLinesContainer>
</div>
);
};

export default Component;
Loading
Loading