Skip to content

feat(import): initialize git for non-git folders on v2 folder import#5036

Open
AviPeltz wants to merge 2 commits into
mainfrom
git-local-import-issue
Open

feat(import): initialize git for non-git folders on v2 folder import#5036
AviPeltz wants to merge 2 commits into
mainfrom
git-local-import-issue

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Jun 2, 2026

Problem

Importing a folder that isn't a git repo yet dead-ends on Not a git repository: <path> in v2 — there's no offer to initialize one. Likely the root cause of #5033 ("why cannot import local git folder without remote?"), which is really about a non-git folder (remote-less git repos already import fine).

The throw happens at project.findByPathresolveLocalReporevParseGitRoot, before create importLocal is reached.

What this does

Detect → confirm → init + import. The folder is git init'd only after the user confirms (it writes into their directory), then imported as a local-only project — no remote required.

Server (packages/host-service)

  • resolve-repo.tstryRevParseGitRoot (non-throwing) + initLocalRepoInPlace (git-inits an existing, populated folder in place + empty initial commit so ensureMainWorkspaceStrict has a real branch; idempotent; a subdir of an existing repo resolves to the parent root). Exported validateDirectoryPath; revParseGitRoot now wraps the non-throwing variant.
  • project.tsfindByPath returns an additive optional needsGitInit: true instead of throwing on a non-git folder (matches the existing "server returns a discriminated result, client branches" pattern). create importLocal gains initIfNeeded.
  • handlers.tscreateFromImportLocal inits in place only when initIfNeeded. Cloud path unchanged (local-only repo → no clone URL, already supported by empty/template).

Desktop

  • New imperative git-init-confirm store + GitInitConfirmDialog, mounted once via AddRepositoryModals. useFolderFirstImport branches on needsGitInit, confirms, then creates with initIfNeeded: true. All 5 hook consumers untouched (the confirm is encapsulated in the hook/store rather than threaded through each call site).

Design note

initIfNeeded is a flag on importLocal rather than a separate initLocal create mode, because the input shape ({ repoPath }) is identical. Easy to switch to a distinct mode if preferred — see the plan doc (plans/20260601-v2-import-git-init-non-git-folder.md).

Out of scope / unchanged

  • Remote-less git repos already import fine.
  • resolveGithubRepo (the "no GitHub remote" throw) stays GitHub-feature-only (PRs/Issues).
  • No auto-init without explicit confirmation.

Testing

  • resolve-repo.test.ts: 30/30 (6 new — init, non-empty adopt, idempotency, nested subdir, missing/file rejects).
  • useFolderFirstImport.test.ts: 2 new (confirm → create-with-init; cancel → no-op).
  • @superset/host-service + @superset/desktop typecheck: ✅
  • bun run lint: ✅
  • Desktop full suite: 2012 pass, 1 fail — pre-existing unrelated settings search … Git tab visible in v2 (confirmed failing with these changes stashed).

Open in Stage

Summary by cubic

Adds git init on v2 folder import when the selected folder isn’t a git repo. We confirm with the user, then initialize in place and import as a local-only project to avoid the “Not a git repository” dead end.

  • New Features

    • Server (packages/host-service): project.findByPath can return needsGitInit; project.create importLocal accepts initIfNeeded; in-place init via initLocalRepoInPlace with an empty initial commit; includes tryRevParseGitRoot; idempotent and respects parent repos.
    • Desktop (apps/desktop): GitInitConfirmDialog + git-init-confirm store; useFolderFirstImport shows confirm when needsGitInit and creates with initIfNeeded: true; mounted via AddRepositoryModals.
    • Tests: Server and UI tests for init-in-place, idempotency, nested repo detection, and confirm/cancel flows.
  • Bug Fixes

    • Use cross-platform getBaseName for the confirm dialog’s folder label to handle Windows paths and trailing slashes.

Written for commit 70b131e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Import non-git folders: UI prompts when a selected folder isn't a git repo and, if confirmed, initializes the folder and creates an initial commit to complete import.
  • Behavior
    • Folder detection now reports when git initialization is needed so imports can opt into init.
  • Tests
    • Added coverage for the confirm/init flow.
  • Documentation
    • Added a design plan describing the import+git-init flow.

Importing a folder that wasn't a git repo yet dead-ended on
"Not a git repository". v2 now detects the case and offers to `git init`
the folder (with user confirmation), then imports it as a local-only
project — no remote required. Likely fixes #5033.

Server (host-service):
- resolve-repo: add tryRevParseGitRoot (non-throwing) and
  initLocalRepoInPlace (git init in place + empty initial commit;
  idempotent; nested dirs resolve to parent root). Export
  validateDirectoryPath; revParseGitRoot now wraps the non-throwing variant.
- project.findByPath returns an additive optional `needsGitInit` instead
  of throwing on a non-git folder.
- create importLocal gains `initIfNeeded`; createFromImportLocal inits
  in place only when set. Cloud path unchanged (local-only => no clone URL).

Desktop:
- New imperative git-init-confirm store + GitInitConfirmDialog, mounted
  once via AddRepositoryModals. useFolderFirstImport branches on
  needsGitInit, confirms, then creates with initIfNeeded. All 5 hook
  consumers untouched.

Tests: 6 new resolve-repo cases; 2 new hook cases (confirm/cancel).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be17375b-df73-4e4d-9678-cbd9a6e0c71a

📥 Commits

Reviewing files that changed from the base of the PR and between 36bdf63 and 70b131e.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx

📝 Walkthrough

Walkthrough

Detect non-git folders server-side, prompt desktop user for git-init confirmation, and on confirmation initialize the folder and import the project (opt-in initIfNeeded path).

Changes

Git Init Confirmation for Non-Git Folders

Layer / File(s) Summary
Git Init Confirmation Store and Dialog
apps/desktop/src/renderer/stores/git-init-confirm.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/*
Zustand store manages dialog state and promise-based confirmation; GitInitConfirmDialog renders the AlertDialog UI and resolves user choice back to callers.
Hook Git Init Detection and Tests
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/*
useFolderFirstImport calls requestGitInit when findByPath returns needsGitInit; on confirm it imports with initIfNeeded: true. Tests cover confirm and cancel flows and mock the store.
AddRepositoryModals Dialog Integration
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/AddRepositoryModals.tsx
AddRepositoryModals now imports and renders GitInitConfirmDialog alongside NewProjectModal within a fragment.
Server Git Utilities
packages/host-service/src/trpc/router/project/utils/resolve-repo.ts, packages/host-service/src/trpc/router/project/utils/resolve-repo.test.ts
Exports validateDirectoryPath, adds non-throwing tryRevParseGitRoot, and implements initLocalRepoInPlace to initialize folders with an initial commit; tests exercise initialization and edge cases.
Server Router Endpoints
packages/host-service/src/trpc/router/project/project.ts
findByPath probes git root non-throwing and returns needsGitInit: true for non-git directories; create.importLocal input includes optional initIfNeeded boolean.
Server Handler
packages/host-service/src/trpc/router/project/handlers.ts
createFromImportLocal accepts initIfNeeded and uses resolveOrInitLocalRepo to either resolve or initialize the repo in place when requested.
Implementation Plan
plans/20260601-v2-import-git-init-non-git-folder.md
Design doc describing server detection, opt-in init path, and desktop confirmation flow.

Sequence Diagram

sequenceDiagram
  participant User
  participant Desktop as useFolderFirstImport
  participant Store as GitInitConfirmStore
  participant Dialog as GitInitConfirmDialog
  participant Server as projectRouter
  participant Handler as createFromImportLocal
  User->>Desktop: select folder for import
  Desktop->>Server: findByPath(folderPath)
  Server-->>Desktop: needsGitInit=true
  Desktop->>Store: request(repoPath)
  Store->>Dialog: open(repoPath)
  User->>Dialog: click Initialize & import
  Dialog->>Store: resolve(true)
  Store-->>Desktop: Promise<true>
  Desktop->>Server: create(importLocal, initIfNeeded=true)
  Server->>Handler: createFromImportLocal(repoPath, initIfNeeded=true)
  Handler->>Handler: resolveOrInitLocalRepo
  Handler-->>Server: ResolvedRepo
  Server-->>Desktop: CreateResult
  Desktop-->>User: import complete
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly Related PRs

  • superset-sh/superset#4547: Modifies useFolderFirstImport hook decision logic; overlaps in the same hook affected by this PR.

Poem

A rabbit hops through folders green,
"Not git yet?" the prompt is seen,
Click to init, then import with care—
New repos bloom from anywhere! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature being added: git initialization support for non-git folders during v2 folder import.
Description check ✅ Passed The description includes all template sections with comprehensive detail: clear Problem statement, What this does overview, Server and Desktop implementation details, Design notes, scope clarification, Testing results, and additional context via cubic summary.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch git-local-import-issue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@stage-review
Copy link
Copy Markdown

stage-review Bot commented Jun 2, 2026

Ready to review this PR? Stage has broken it down into 5 individual chapters for you:

Title
1 Document the git-init import plan
2 Implement in-place git initialization utilities
3 Update project API to support git-init
4 Add git-init confirmation UI
5 Wire git-init into folder import flow
Open in Stage

Chapters generated by Stage for commit 70b131e on Jun 2, 2026 7:54am UTC.

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented Jun 2, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR fixes a dead-end UX where importing a folder that isn't a git repository threw Not a git repository: <path> with no recovery path. The fix detects the non-git case in findByPath (returning needsGitInit: true instead of throwing), shows a confirmation dialog, and then git inits the folder in place before importing it as a local-only project.

  • Server: tryRevParseGitRoot (non-throwing) and initLocalRepoInPlace (idempotent in-place git init + empty commit) are added to resolve-repo.ts; findByPath returns an additive needsGitInit flag; importLocal mode gains an initIfNeeded boolean that is false by default, preserving backwards compatibility.
  • Desktop: A new imperative zustand store (git-init-confirm) and GitInitConfirmDialog component drive the confirmation flow; useFolderFirstImport branches on needsGitInit and all 5 existing call sites are untouched.
  • Tests: 6 new integration tests for initLocalRepoInPlace and 2 new hook tests cover confirm and cancel paths.

Confidence Score: 4/5

Safe to merge — the feature is well-encapsulated, opt-in, and idempotent; the only issue is a display-only path-splitting inconsistency on Windows.

The detect-confirm-init flow is correctly structured: validation gates are in place before any filesystem writes, the TOCTOU re-check inside initLocalRepoInPlace prevents double-init, error propagation through the try/catch chain is consistent, and backwards compatibility is preserved by the false-defaulted initIfNeeded flag. The one gap is in GitInitConfirmDialog: repoPath?.split("/").pop() uses a Unix-only path separator, so on Windows the dialog would display the full absolute path instead of the folder name. The rest of the codebase uses getBaseName for this — it's a display-only issue and doesn't affect the import itself.

GitInitConfirmDialog.tsx — the folder name derivation should use getBaseName from renderer/lib/pathBasename rather than split("/").pop() for correct behaviour on Windows.

Important Files Changed

Filename Overview
packages/host-service/src/trpc/router/project/utils/resolve-repo.ts Adds tryRevParseGitRoot (non-throwing), initLocalRepoInPlace (idempotent git-init in place + empty commit), and exports validateDirectoryPath; refactors revParseGitRoot to wrap the new non-throwing variant. Logic is solid and TOCTOU handling is correct.
packages/host-service/src/trpc/router/project/project.ts findByPath now uses tryRevParseGitRoot, returning needsGitInit:true instead of throwing for non-git folders; still 400s on missing/non-dir paths via validateDirectoryPath. The importLocal schema gets initIfNeeded boolean flag.
packages/host-service/src/trpc/router/project/handlers.ts Adds resolveOrInitLocalRepo helper and threads initIfNeeded through createFromImportLocal. Double tryRevParseGitRoot call is intentional for TOCTOU safety. Cloud path is correctly unchanged.
apps/desktop/src/renderer/stores/git-init-confirm.ts New imperative zustand store for the git-init confirm dialog. Module-level pendingResolve pattern matches the existing add-repository-modal precedent; concurrent-call safety is documented and correctly implemented.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.ts Branches on needsGitInit, awaits confirmation, then calls create with initIfNeeded:true. The create.mutate call sits inside the same try/catch as findByPath so PRECONDITION_FAILED errors are correctly surfaced via onError.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx New confirm dialog driven by git-init-confirm store. Uses split('/').pop() for the folder display name, which is incorrect on Windows where paths use backslashes; getBaseName from pathBasename should be used instead.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/AddRepositoryModals.tsx Mounts GitInitConfirmDialog alongside NewProjectModal inside a Fragment; minimal and correct change.
packages/host-service/src/trpc/router/project/utils/resolve-repo.test.ts Adds 6 integration tests for initLocalRepoInPlace covering plain folder, non-empty adoption, idempotency, nested subdir resolution, missing path, and file path. Identity env vars are correctly saved/restored.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.test.ts Adds two test cases for the needsGitInit branch (confirm → create with initIfNeeded:true; cancel → null return). Mock plumbing is correct.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant UI as useFolderFirstImport
    participant D as GitInitConfirmDialog
    participant HS as host-service (findByPath)
    participant HC as host-service (create)
    participant FS as Filesystem

    U->>UI: select folder
    UI->>HS: "project.findByPath({ repoPath })"
    alt path is a git repo
        HS-->>UI: "{ candidates, cloudErrors }"
        UI->>UI: existing import / setup flow
    else path is NOT a git repo
        HS->>HS: tryRevParseGitRoot → null
        HS->>HS: validateDirectoryPath (400 if missing/not-dir)
        HS-->>UI: "{ candidates:[], cloudErrors:[], needsGitInit:true }"
        UI->>D: requestGitInit(repoPath)
        D-->>U: Initialize git repository? dialog
        alt User confirms
            U->>D: "click Initialize & import"
            D-->>UI: "confirmed = true"
            UI->>HC: "project.create({ importLocal, initIfNeeded:true })"
            HC->>HC: resolveOrInitLocalRepo
            HC->>HC: tryRevParseGitRoot (TOCTOU re-check)
            HC->>FS: "git init --initial-branch=main"
            HC->>FS: git commit --allow-empty
            HC->>HC: persistFromResolved (DB + cloud saga)
            HC-->>UI: "{ projectId, repoPath, mainWorkspaceId }"
            UI->>UI: finalizeSetup
        else User cancels
            U->>D: click Cancel / close
            D-->>UI: "confirmed = false"
            UI-->>U: return null (no-op)
        end
    end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx:22
The folder name is derived using `split("/").pop()`, which silently falls back to the full path on Windows because Windows paths use backslashes (`C:\Users\me\myproject`.split("/") returns the entire string as one element). `useFolderFirstImport` already imports `getBaseName` from `renderer/lib/pathBasename` for the same job — use that here for consistent cross-platform behaviour.

```suggestion
	const folderName = repoPath ? getBaseName(repoPath) : "this folder";
```

Reviews (1): Last reviewed commit: "feat(import): initialize git for non-git..." | Re-trigger Greptile

const repoPath = useGitInitConfirmStore((s) => s.repoPath);
const resolve = useGitInitConfirmStore((s) => s.resolve);

const folderName = repoPath?.split("/").pop() ?? repoPath ?? "this folder";
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.

P2 The folder name is derived using split("/").pop(), which silently falls back to the full path on Windows because Windows paths use backslashes (C:\Users\me\myproject.split("/") returns the entire string as one element). useFolderFirstImport already imports getBaseName from renderer/lib/pathBasename for the same job — use that here for consistent cross-platform behaviour.

Suggested change
const folderName = repoPath?.split("/").pop() ?? repoPath ?? "this folder";
const folderName = repoPath ? getBaseName(repoPath) : "this folder";
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx
Line: 22

Comment:
The folder name is derived using `split("/").pop()`, which silently falls back to the full path on Windows because Windows paths use backslashes (`C:\Users\me\myproject`.split("/") returns the entire string as one element). `useFolderFirstImport` already imports `getBaseName` from `renderer/lib/pathBasename` for the same job — use that here for consistent cross-platform behaviour.

```suggestion
	const folderName = repoPath ? getBaseName(repoPath) : "this folder";
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx`:
- Line 22: The computed folderName currently does repoPath?.split("/").pop()
which yields an empty string for paths ending with a slash; update the logic to
normalize repoPath before extracting the basename (either trim trailing slashes
from repoPath or call the shared basename helper used elsewhere) so folderName
is never empty—use the normalized path (repoPathNormalized) when computing
folderName (keep the existing fallback to repoPath or "this folder") and update
the GitInitConfirmDialog component to reference the normalized value.

In `@packages/host-service/src/trpc/router/project/handlers.ts`:
- Around line 206-213: The change allows file paths to be converted into repos
because resolveOrInitLocalRepo calls tryRevParseGitRoot before validating the
original repoPath; fix resolveOrInitLocalRepo (and the analogous call at the
other site) by first verifying repoPath is a directory (e.g., fs.stat/exists +
isDirectory) and, if not a directory, immediately call
resolveLocalRepo(repoPath) so non-directory paths are rejected as before; only
if repoPath is a directory and initIfNeeded is true should you call
tryRevParseGitRoot and fall back to initLocalRepoInPlace(repoPath).

In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 172-182: The code calls tryRevParseGitRoot(input.repoPath) before
validating that input.repoPath is a directory, which lets file paths inside a
repo be accepted; move or add the directory validation so the path is confirmed
a directory before probing Git. Specifically, invoke
validateDirectoryPath(input.repoPath, "Path") (or a direct fs.stat check for
directory) prior to calling tryRevParseGitRoot, and only call
tryRevParseGitRoot/resolveLocalRepo if the path is a directory; preserve the
existing return shape (candidates/cloudErrors/needsGitInit) when validation
fails.

In `@packages/host-service/src/trpc/router/project/utils/resolve-repo.ts`:
- Around line 103-110: The current tryRevParseGitRoot function swallows all
errors from createUserSimpleGit(path).revparse, which hides permission, broken
.git, or missing-git failures; change the catch to only return null when the
error is the canonical "not a git repository" case (e.g., error message/stderr
contains "not a git repository" case-insensitive), and rethrow any other errors
so callers can surface/init properly; reference the function tryRevParseGitRoot
and the revparse call on createUserSimpleGit(path) when making this change.

In `@plans/20260601-v2-import-git-init-non-git-folder.md`:
- Line 142: Change the prose instance of `discriminatedUnion` to either the
spaced phrase "discriminated union" or render it as an inline code identifier
(e.g., `discriminatedUnion`) for clarity and consistency; locate the token
"discriminatedUnion" in the sentence starting "The create modes are a
discriminatedUnion" and replace it with the preferred form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b72c4cf-dfed-485e-be9d-150c4a9c01cb

📥 Commits

Reviewing files that changed from the base of the PR and between 5881fdc and 36bdf63.

📒 Files selected for processing (11)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/AddRepositoryModals.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/GitInitConfirmDialog.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/GitInitConfirmDialog/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.ts
  • apps/desktop/src/renderer/stores/git-init-confirm.ts
  • packages/host-service/src/trpc/router/project/handlers.ts
  • packages/host-service/src/trpc/router/project/project.ts
  • packages/host-service/src/trpc/router/project/utils/resolve-repo.test.ts
  • packages/host-service/src/trpc/router/project/utils/resolve-repo.ts
  • plans/20260601-v2-import-git-init-non-git-folder.md

Comment on lines +206 to +213
async function resolveOrInitLocalRepo(
repoPath: string,
initIfNeeded: boolean,
): Promise<ResolvedRepo> {
if (!initIfNeeded) return resolveLocalRepo(repoPath);
const root = await tryRevParseGitRoot(repoPath);
return root ? resolveLocalRepo(root) : initLocalRepoInPlace(repoPath);
}
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 issue | 🟠 Major | ⚡ Quick win

initIfNeeded now lets file paths slip through as repo imports.

Before this change, createFromImportLocal always went through resolveLocalRepo(repoPath), which rejected non-directories. With the new probe-first flow, passing /repo/src/file.ts plus initIfNeeded: true resolves /repo and imports it successfully. Please validate the original repoPath before tryRevParseGitRoot so the mutation stays folder-only.

Also applies to: 219-222

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/project/handlers.ts` around lines 206 -
213, The change allows file paths to be converted into repos because
resolveOrInitLocalRepo calls tryRevParseGitRoot before validating the original
repoPath; fix resolveOrInitLocalRepo (and the analogous call at the other site)
by first verifying repoPath is a directory (e.g., fs.stat/exists + isDirectory)
and, if not a directory, immediately call resolveLocalRepo(repoPath) so
non-directory paths are rejected as before; only if repoPath is a directory and
initIfNeeded is true should you call tryRevParseGitRoot and fall back to
initLocalRepoInPlace(repoPath).

Comment on lines +172 to +182
const root = await tryRevParseGitRoot(input.repoPath);
if (root === null) {
validateDirectoryPath(input.repoPath, "Path"); // 400 on missing / not-a-dir
return {
candidates: [],
cloudErrors: [] as { url: string; message: string }[],
needsGitInit: true as const,
};
}

const resolved = await resolveLocalRepo(root);
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 issue | 🟠 Major | ⚡ Quick win

Validate input.repoPath before probing Git.

git rev-parse --show-toplevel succeeds for file paths inside a worktree, so this branch now accepts something like /repo/src/index.ts and resolves the containing repo instead of rejecting it as "Path is not a directory". That changes the import contract from "pick a folder" to "pick anything inside a repo" and can import the wrong project from an accidental file selection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/project/project.ts` around lines 172 -
182, The code calls tryRevParseGitRoot(input.repoPath) before validating that
input.repoPath is a directory, which lets file paths inside a repo be accepted;
move or add the directory validation so the path is confirmed a directory before
probing Git. Specifically, invoke validateDirectoryPath(input.repoPath, "Path")
(or a direct fs.stat check for directory) prior to calling tryRevParseGitRoot,
and only call tryRevParseGitRoot/resolveLocalRepo if the path is a directory;
preserve the existing return shape (candidates/cloudErrors/needsGitInit) when
validation fails.

Comment on lines +103 to +110
export async function tryRevParseGitRoot(path: string): Promise<string | null> {
try {
return (
await createUserSimpleGit(path).revparse(["--show-toplevel"])
).trim();
} catch {
return null;
}
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 issue | 🟠 Major | ⚡ Quick win

Don't collapse every rev-parse failure into "not a repo".

This now turns permission errors, broken .git metadata, or a missing git executable into null, which makes callers treat the folder as safe to initialize. In particular, project.findByPath can surface needsGitInit for a real repo that just failed to probe, and initLocalRepoInPlace can reinitialize it instead of surfacing the underlying failure. Only swallow the canonical "not a git repository" case here; rethrow the rest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/project/utils/resolve-repo.ts` around
lines 103 - 110, The current tryRevParseGitRoot function swallows all errors
from createUserSimpleGit(path).revparse, which hides permission, broken .git, or
missing-git failures; change the catch to only return null when the error is the
canonical "not a git repository" case (e.g., error message/stderr contains "not
a git repository" case-insensitive), and rethrow any other errors so callers can
surface/init properly; reference the function tryRevParseGitRoot and the
revparse call on createUserSimpleGit(path) when making this change.

Comment on lines +165 to +179
const existingRoot = await tryRevParseGitRoot(repoPath);
if (existingRoot) return resolveLocalRepo(existingRoot);

await gitInitMainBranch(repoPath);
try {
await createUserSimpleGit(repoPath).raw([
"commit",
"--allow-empty",
"-m",
"Initial commit",
]);
} catch (err) {
throw asInitialCommitTrpcError(err);
}
return resolveLocalRepo(repoPath);
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 issue | 🟠 Major | 🏗️ Heavy lift

initLocalRepoInPlace is still race-prone.

The "already initialized" check and the empty commit are separate steps. If another import initializes the same folder after Line 165 but before Line 170, this path will re-run git init and append a second empty Initial commit, which breaks the idempotency promised in the docstring and mutates user history on a retry/race. This needs a lock or a revalidation flow that can detect "someone else initialized it" before creating a new commit.

}),
```

> **Design tension (flag vs. separate mode).** The create modes are a discriminatedUnion
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 issue | 🟡 Minor | ⚡ Quick win

Clarify wording: discriminatedUnion in prose reads like a typo.

In narrative text, prefer “discriminated union” (or wrap `discriminatedUnion` as a code identifier) for readability and consistency.

🧰 Tools
🪛 LanguageTool

[grammar] ~142-~142: Ensure spelling is correct
Context: ...eparate mode).** The create modes are a discriminatedUnion > where each kind has fixed init semant...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/20260601-v2-import-git-init-non-git-folder.md` at line 142, Change the
prose instance of `discriminatedUnion` to either the spaced phrase
"discriminated union" or render it as an inline code identifier (e.g.,
`discriminatedUnion`) for clarity and consistency; locate the token
"discriminatedUnion" in the sentence starting "The create modes are a
discriminatedUnion" and replace it with the preferred form.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plans/20260601-v2-import-git-init-non-git-folder.md">

<violation number="1" location="plans/20260601-v2-import-git-init-non-git-folder.md:89">
P2: The proposed non-throwing `tryRevParseGitRoot` swallows all errors, which can misclassify real operational failures as "non-git" and hide diagnostics.</violation>
</file>

<file name="packages/host-service/src/trpc/router/project/utils/resolve-repo.ts">

<violation number="1" location="packages/host-service/src/trpc/router/project/utils/resolve-repo.ts:165">
P2: There is a TOCTOU window between the `tryRevParseGitRoot` check (line 165) and the `gitInitMainBranch` + commit sequence. If a concurrent import initializes the same folder in between, this creates a duplicate "Initial commit", breaking the idempotency guarantee stated in the docstring. Consider rechecking after `gitInitMainBranch` (which is itself idempotent) whether a commit already exists before adding one.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant UI as Desktop UI
    participant Store as GitInitConfirm Store
    participant Dialog as GitInitConfirmDialog
    participant Hook as useFolderFirstImport
    participant Host as Host Service
    participant Repo as Resolve-Repo Utils
    participant FS as File System

    Note over UI,FS: V2 Folder Import — Non-Git Folder Flow

    Hook->>Host: project.findByPath.query({ repoPath })

    Host->>Repo: tryRevParseGitRoot(repoPath)
    Repo->>FS: git rev-parse --show-toplevel
    FS-->>Repo: Error (not a git repo)

    alt Not a git repo & path exists
        Host->>Repo: validateDirectoryPath(repoPath)
        Repo-->>Host: OK
        Host-->>Hook: { candidates: [], cloudErrors: [], needsGitInit: true }
    else Is a git repo
        Host-->>Hook: { candidates: [...], cloudErrors: [] }
    end

    alt needsGitInit detected
        Hook->>Store: request(repoPath)
        Store-->>Dialog: Opens modal
        Dialog-->>Store: User clicks "Initialize & import"
        Store-->>Hook: true (confirmed)

        Hook->>Host: project.create.mutate({ mode: { kind: "importLocal", repoPath, initIfNeeded: true } })

        Host->>Repo: initLocalRepoInPlace(repoPath)
        Repo->>FS: validateDirectoryPath(repoPath)
        Repo->>FS: tryRevParseGitRoot(repoPath) [idempotency check]
        alt Not already a repo
            Repo->>FS: git init --initial-branch=main
            Repo->>FS: git commit --allow-empty -m "Initial commit"
            Repo->>FS: resolveLocalRepo(repoPath)
            FS-->>Repo: { repoPath, remoteName: null, parsed: null }
        else Already a repo
            Repo->>FS: resolveLocalRepo(existingRoot)
            FS-->>Repo: { repoPath, remoteName, parsed }
        end
        Host-->>Hook: { projectId, repoPath, mainWorkspaceId }
        Hook->>Hook: finalizeSetup(activeHostUrl, result)
    else User cancels
        Dialog-->>Store: User clicks "Cancel"
        Store-->>Hook: false (cancelled)
        Hook-->>UI: null (no-op)
    end

    Note over UI,FS: On confirmation: git init leaves parent repos undisturbed<br/>Nested subdir resolves to parent root automatically
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

async function tryRevParseGitRoot(path: string): Promise<string | null> {
try {
return (await createUserSimpleGit(path).revparse(["--show-toplevel"])).trim();
} catch {
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.

P2: The proposed non-throwing tryRevParseGitRoot swallows all errors, which can misclassify real operational failures as "non-git" and hide diagnostics.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/20260601-v2-import-git-init-non-git-folder.md, line 89:

<comment>The proposed non-throwing `tryRevParseGitRoot` swallows all errors, which can misclassify real operational failures as "non-git" and hide diagnostics.</comment>

<file context>
@@ -0,0 +1,262 @@
+async function tryRevParseGitRoot(path: string): Promise<string | null> {
+  try {
+    return (await createUserSimpleGit(path).revparse(["--show-toplevel"])).trim();
+  } catch {
+    return null;
+  }
</file context>

): Promise<ResolvedRepo> {
validateDirectoryPath(repoPath, "Path");

const existingRoot = await tryRevParseGitRoot(repoPath);
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.

P2: There is a TOCTOU window between the tryRevParseGitRoot check (line 165) and the gitInitMainBranch + commit sequence. If a concurrent import initializes the same folder in between, this creates a duplicate "Initial commit", breaking the idempotency guarantee stated in the docstring. Consider rechecking after gitInitMainBranch (which is itself idempotent) whether a commit already exists before adding one.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/project/utils/resolve-repo.ts, line 165:

<comment>There is a TOCTOU window between the `tryRevParseGitRoot` check (line 165) and the `gitInitMainBranch` + commit sequence. If a concurrent import initializes the same folder in between, this creates a duplicate "Initial commit", breaking the idempotency guarantee stated in the docstring. Consider rechecking after `gitInitMainBranch` (which is itself idempotent) whether a commit already exists before adding one.</comment>

<file context>
@@ -129,6 +142,43 @@ export async function resolveLocalRepo(
+): Promise<ResolvedRepo> {
+	validateDirectoryPath(repoPath, "Path");
+
+	const existingRoot = await tryRevParseGitRoot(repoPath);
+	if (existingRoot) return resolveLocalRepo(existingRoot);
+
</file context>

…r label

split("/") mishandled Windows backslash paths and trailing slashes (empty
label). getBaseName handles both separators and drops empty segments.
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.

1 participant