Skip to content

feat(Git Sync): Git projects can live anywhere on disk#10155

Open
gatzjames wants to merge 26 commits into
Kong:developfrom
gatzjames:feat/git-project-local-storage
Open

feat(Git Sync): Git projects can live anywhere on disk#10155
gatzjames wants to merge 26 commits into
Kong:developfrom
gatzjames:feat/git-project-local-storage

Conversation

@gatzjames

Copy link
Copy Markdown
Contributor

Overview

Today every Git-backed project is stored in a hidden, app-managed folder ({userData}/version-control/git/{id}). Users can't point Insomnia at a repo they already cloned, can't choose where a clone lands, and can't reach their working tree with native Git tooling. This PR makes a Git project's location user-owned and arbitrary, while keeping the managed default fully intact.

Features

  • Clone anywhere — pick a destination folder when cloning (defaults to last-used).
  • Open an existing folder as a Git project — adopts existing Insomnia content, or git inits an empty/plain folder.
  • Open from the OS — insomnia /path, macOS Finder Open With, and an insomnia://app/open-folder deep link.
  • Relocate a project (managed or custom) to a new folder from project settings — moves the working tree + history, no re-clone.
  • Credentials can be associated when adopting a folder, so push/pull works.

Design principles

The whole feature hinges on a single new field — GitRepository.directory — and one resolver (getRepoBaseDir) that every path now flows through; the prior layout is just directory === null, so existing projects need zero migration. The governing principle is ownership: Insomnia owns the managed folder (and may delete it), the user owns theirs (we never do). That rule drives deletion, relocation, and missing-folder handling.

Safety

  • No new dependencies — built on existing primitives (isomorphic-git, native dialogs, node:fs).
  • Folder-trust prompt before adopting any folder; because we use isomorphic-git (not the git CLI), opening a folder never executes git hooks — residual risk is limited to imported Insomnia content.
  • No data loss / no surprise deletes — deleting a user folder on disk is treated as unavailable (collections preserved, re-sync on return), never as "wipe the DB"; removing a project never touches a user-owned folder.
  • All mutations go through React Router actions so the UI stays consistent.

Testing

  • Added E2E smoke tests that cover open-folder (incl. git init), the collision guard, and clone-into-folder.
  • Manual OS-integration testing is available via a dev-only Developer → Open folder in Insomnia… menu item.

Scope / follow-ups

  • Default to "native git credentials" when choosing to use a folder since it's the most probably use-case.
  • Windows Explorer context-menu and a sidebar "repository unavailable" badge are intentionally deferred (noted in the design doc). macOS Finder association requires a signed packaged build to verify.

Closes INS-2708

@gatzjames gatzjames requested a review from a team June 24, 2026 14:31
@gatzjames gatzjames self-assigned this Jun 24, 2026
Copilot AI review requested due to automatic review settings June 24, 2026 14:31
@gatzjames gatzjames force-pushed the feat/git-project-local-storage branch from 8b4d975 to dffadc1 Compare June 24, 2026 14:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Insomnia’s Git Sync to support user-owned repository locations on disk (clone-to-folder, adopt an existing folder, OS “open folder” integrations, and repository relocation) while preserving the existing managed default when no custom directory is set.

Changes:

  • Introduces GitRepository.directory and centralizes repo path resolution across renderer + main flows.
  • Adds UX flows for cloning into a chosen destination, opening/adopting an existing folder (with a trust prompt), and relocating an existing repo.
  • Updates deletion/cleanup + file-watching behavior to avoid data loss when a repo folder disappears (deleted/moved/unmounted).

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/insomnia/src/ui/utils/select-file-or-folder.ts Adds defaultPath support to seed directory/file pickers.
packages/insomnia/src/ui/utils/git-repo-path.ts Adds renderer-side helper to compute repo base directory from GitRepository.directory.
packages/insomnia/src/ui/utils/git-folder-trust.tsx Adds a trust confirmation modal for adopting/opening arbitrary folders as Git projects.
packages/insomnia/src/ui/components/project/utils.tsx Adds clone destination support + helpers (last-used dir, repo-name derivation).
packages/insomnia/src/ui/components/project/project-settings-form.tsx Adds repo relocation action and updates repo path display to use the resolver.
packages/insomnia/src/ui/components/project/project-create-form.tsx Adds “Clone from URL” vs “Open existing folder” modes and trust prompt gating.
packages/insomnia/src/ui/components/project/git-repo-form.tsx Adds “Clone location” UI and persists last-used clone parent dir.
packages/insomnia/src/ui/components/modals/git-repository-settings-modal/git-repository-settings-modal.tsx Shows resolved local folder path + “Reveal” action.
packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx Uses resolved repo base dir for “open folder” behaviors.
packages/insomnia/src/ui/components/git-credentials/git-credential-select.tsx Adds an optional credentials picker for the “open existing folder” flow.
packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx Uses resolved repo base dir for “Open folder” action.
packages/insomnia/src/sync/git/repo-file-watcher.ts Prevents DB wipes when repo directory is unavailable; adds availability checks.
packages/insomnia/src/routes/organization.$organizationId.project.new.tsx Adds support for directory (clone dest) and openExistingDirectory (adopt).
packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx Resolves git ruleset path relative to repo base dir (supports custom dirs).
packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx Ensures cleanup respects user-owned vs managed repo directories.
packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx Ensures cleanup respects user-owned vs managed repo directories.
packages/insomnia/src/routes/git.relocate.tsx Adds route action + fetcher for relocating repos via IPC.
packages/insomnia/src/root.tsx Handles insomnia://app/open-folder deep link via project creation action + trust prompt.
packages/insomnia/src/main/window-utils.ts Adds dev-only “Open folder in Insomnia…” menu to simulate OS integration.
packages/insomnia/src/main/ipc/electron.ts Adds IPC channel type definitions for new git actions.
packages/insomnia/src/main/git-service.ts Implements path resolver, adopt/open folder, cleanup, and relocation logic.
packages/insomnia/src/entry.preload.ts Exposes new git IPC methods to the renderer.
packages/insomnia/src/entry.main.ts Normalizes OS-opened folder paths into a deep link and wires macOS open-file handling.
packages/insomnia/electron-builder.config.js Registers folder document type on macOS for Finder “Open With” integration.
packages/insomnia-smoke-test/tests/smoke/git-local-repos.test.ts Adds smoke coverage for adopting local folders + cloning into chosen destinations.
packages/insomnia-smoke-test/playwright/pages/project/index.ts Adds Playwright page helpers for the new git-local-repo flows.
packages/insomnia-data/src/models/git-repository.ts Adds `directory: string
packages/insomnia-data/node-src/services/git-repository.ts Adds lookup by directory for collision-guard enforcement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 864 to 882
/** Recursively collect all `.yaml` files under `dir` as normalised absolute paths, skipping `.git`. */
/**
* Whether the repository's working-tree directory still exists on disk.
*
* When the user deletes/moves the folder (or unmounts its drive) the disk
* appears empty. We must NOT interpret that as "all files deleted" and wipe the
* database — instead we treat the repo as temporarily unavailable and skip
* syncing, so the project's collections survive (and re-sync if the folder
* returns).
*/
private async repoDirIsAvailable(): Promise<boolean> {
try {
return (await fs.promises.stat(this.repoDir)).isDirectory();
} catch {
return false;
}
}

private async collectYamlFiles(dir: string): Promise<string[]> {
Comment on lines +217 to +220
// Move into `<chosen-parent>/<repo-name>`, matching the clone flow.
const folderName = deriveRepoName(gitRepository.uri) || gitRepository._id;
const newDirectory = window.path.join(picked.filePath, folderName);

@gatzjames gatzjames force-pushed the feat/git-project-local-storage branch 3 times, most recently from 9e0392f to 96d0321 Compare June 30, 2026 09:02
if (!stats.isDirectory()) {
return { errors: [`Not a folder: ${resolvedDirectory}`] };
}
await fs.promises.access(resolvedDirectory, fs.constants.R_OK | fs.constants.W_OK);

@aikido-pr-checks aikido-pr-checks Bot Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Suggested change
await fs.promises.access(resolvedDirectory, fs.constants.R_OK | fs.constants.W_OK);
const realPath = await fs.promises.realpath(resolvedDirectory);
await fs.promises.access(realPath, fs.constants.R_OK | fs.constants.W_OK);

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

if (!newDirectory || !path.isAbsolute(newDirectory)) {
return { errors: ['A valid absolute folder path is required.'] };
}
const targetDir = path.resolve(newDirectory);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Show fix

Remediation: Ignore this issue only after you've verified or sanitized the input going into this function. This issue is only relevant in the backend, not in the frontend!

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info


// The destination's parent must exist and be writable.
try {
await fs.promises.access(path.dirname(targetDir), fs.constants.W_OK);

@aikido-pr-checks aikido-pr-checks Bot Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Show fix
Suggested change
await fs.promises.access(path.dirname(targetDir), fs.constants.W_OK);
const targetDirParent = path.dirname(targetDir);
const resolvedBase = path.resolve(targetDir);
const resolvedTarget = path.resolve(resolvedBase, targetDirParent);
const relative = path.relative(resolvedBase, resolvedTarget);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return { errors: ['Invalid path specified.'] };
}
await fs.promises.access(targetDirParent, fs.constants.W_OK);

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

@gatzjames gatzjames force-pushed the feat/git-project-local-storage branch from 8b63e73 to 6fb2c04 Compare June 30, 2026 12:34
if (!path.isAbsolute(directory)) {
return { errors: ['Clone location must be an absolute path.'] };
}
const resolvedDirectory = path.resolve(directory);

@aikido-pr-checks aikido-pr-checks Bot Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Suggested change
const resolvedDirectory = path.resolve(directory);
const resolvedDirectory = path.resolve(directory);
if (path.relative(path.parse(resolvedDirectory).root, resolvedDirectory).startsWith('..') || path.isAbsolute(path.relative(path.parse(resolvedDirectory).root, resolvedDirectory))) {
return { errors: ['Invalid directory path.'] };
}

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

gatzjames added 9 commits July 6, 2026 17:05
Rename 'Clone from URL' to 'Clone from Remote' and 'Open existing
folder' to 'Open local folder'. Update the smoke test selector and a
stale comment accordingly.
Standardize the credential select, author email select, and clone
location box to match the repository/branch comboboxes: shared
--line-height-xs height, consistent label spacing, and input-sized
value text and padding.
Default the clone parent directory to the folder the user last cloned
into, and render the resulting target path middle-truncated with a full
path tooltip via a new MiddleTruncate component.
Move the helper text directly below the Folder label, and align the
folder box and Choose folder button to the shared control height with
middle-truncated path display.
Add a git.checkGitRepoDirectory IPC that resolves the project adopting
a folder. The Open local folder flow checks at folder-pick time and
shows a red, no-background warning below the input offering to open the
existing project, and blocks continuing while the warning is present.
Replace the vertically centered grid with a top-aligned, page-scrolling
layout so the new project form no longer jumps when switching project
types and the scrollbar spans the whole pane.
Top-align the project modal overlay so it no longer jumps as the form
height changes, and drop the dimmer color override on the Open local
folder helper text so it matches the repository URL field description.
@CurryYangxx CurryYangxx force-pushed the feat/git-project-local-storage branch from 1c51ea7 to 8d2f624 Compare July 6, 2026 09:05
<Button
onPress={() => {
navigate(
`/organization/${existingFolderProject.organizationId}/project/${existingFolderProject._id}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a project from another account on the same laptop

@@ -0,0 +1,99 @@
import type { GitCredentials } from 'insomnia-data';

@CurryYangxx CurryYangxx Jul 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we reuse the credential selection component which used in the Git project creation flow?
OR we can use this select ux in the creation flow?

@CurryYangxx CurryYangxx requested a review from yaoweiprc July 7, 2026 03:25
// redirect navigates to the new project.
return openFolderSubmit({
organizationId: targetOrg,
projectData: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't set git credential for this path.

Comment on lines +752 to 758
if (fileStat.isSymbolicLink()) {
return null;
}

if (fileStat.isSymbolicLink()) {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 8 comments.

Comment on lines +9 to +14
export const resolveGitRepoBaseDir = (gitRepository: Pick<GitRepository, '_id' | 'directory'>): string => {
if (gitRepository.directory) {
return gitRepository.directory;
}
return window.path.join(window.app.getPath('userData'), `version-control/git/${gitRepository._id}`);
};
Comment on lines +45 to +55
export const deriveRepoName = (uri?: string): string => {
if (!uri) {
return 'repository';
}
const cleaned = uri
.trim()
.replace(/\.git$/i, '')
.replace(/[/\\]+$/, '');
const last = cleaned.split(/[/\\:]/).pop();
return last || 'repository';
};
Comment on lines 996 to 1014
/** Recursively collect all `.yaml` files under `dir` as normalised absolute paths, skipping `.git`. */
/**
* Whether the repository's working-tree directory still exists on disk.
*
* When the user deletes/moves the folder (or unmounts its drive) the disk
* appears empty. We must NOT interpret that as "all files deleted" and wipe the
* database — instead we treat the repo as temporarily unavailable and skip
* syncing, so the project's collections survive (and re-sync if the folder
* returns).
*/
private async repoDirIsAvailable(): Promise<boolean> {
try {
return (await fs.promises.stat(this.repoDir)).isDirectory();
} catch {
return false;
}
}

private async collectYamlFiles(dir: string): Promise<string[]> {
directory: gitRepository.directory,
});

const repoBaseDir = getGitBaseDir(gitRepository._id);
Comment on lines +38 to +40
selectedKey={selected?._id ?? null}
onSelectionChange={key => onChange(key as string)}
>
Comment on lines +206 to +227
const onRelocateRepo = async () => {
if (!project || !gitRepository) {
return;
}
const picked = await selectFileOrFolder({
itemTypes: ['directory'],
defaultPath: window.app.getPath('home'),
});
if (picked.canceled || !picked.filePath) {
return;
}
// Move into `<chosen-parent>/<repo-name>`, matching the clone flow.
const folderName = deriveRepoName(gitRepository.uri) || gitRepository._id;
const newDirectory = window.path.join(picked.filePath, folderName);

setError(null);
relocateFetcher.submit({
gitRepositoryId: gitRepository._id,
projectId: project._id,
newDirectory,
});
};
Comment on lines +12 to +15
* rather than the native `git` binary, so opening a folder never executes git
* hooks — but a folder from an untrusted source can still bring in unwanted
* content, so we surface a one-time trust prompt.
*
* @param directory - Optional user-chosen repo directory (resolved from DB when omitted)
* @returns File system client configured for the appropriate context
*/
function getGitBaseDir(gitRepositoryId: string): string {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants