Skip to content

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

Open
gatzjames wants to merge 15 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 15 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
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 24, 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

invariant(repo, 'Git Repository not found');

const currentBaseDir = await getRepoBaseDir(repo._id, repo.directory);
if (path.resolve(currentBaseDir) === targetDir) {

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

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 24, 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(resolvedDirectory, directory).startsWith('..') || path.isAbsolute(path.relative(resolvedDirectory, directory))) {
return { errors: ['Invalid directory path.'] };
}

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 24, 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 directory path.'] };
}
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 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 from dffadc1 to 4c77706 Compare June 24, 2026 14:44
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.

2 participants