feat(Git Sync): Git projects can live anywhere on disk#10155
feat(Git Sync): Git projects can live anywhere on disk#10155gatzjames wants to merge 15 commits into
Conversation
| if (!stats.isDirectory()) { | ||
| return { errors: [`Not a folder: ${resolvedDirectory}`] }; | ||
| } | ||
| await fs.promises.access(resolvedDirectory, fs.constants.R_OK | fs.constants.W_OK); |
There was a problem hiding this comment.
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.
| 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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
| 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
8b4d975 to
dffadc1
Compare
There was a problem hiding this comment.
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.directoryand 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.
| /** 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[]> { |
| // 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); | ||
|
|
…o file watcher for directory availability
dffadc1 to
4c77706
Compare
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
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
Testing
Scope / follow-ups
Closes INS-2708