fix(desktop): confirm the repo root when the picked folder isn't one

Closed
LE
1mo ago
+423-66

What & why

getGitRoot resolves a picked folder with git rev-parse --show-toplevel, which walks up the directory tree. If the folder you pick isn't a git repository but any ancestor is, the app silently opens that ancestor instead. NotGitRepoError is the only trigger for the existing "Initialize Git Repository?" dialog, and it's only thrown when no ancestor is a repo, so you're never asked.

The case that hurts is a home directory tracked as a dotfiles repo. Pick ~/projects/new-thing in Add repository → Open project and Superset adds ~ as a project, named after your home directory, without a prompt. Drag-and-drop does the same thing through openFromPath.

This does not change which repository gets opened. A subdirectory still resolves to the repo that contains it, and that stays the default action. The only change is that the app no longer makes the choice silently. plans/20260601-v2-import-git-init-non-git-folder.md:100-102 deferred this case on purpose:

Edge — folder nested inside a parent git repo: git rev-parse --show-toplevel succeeds and resolves to the parent root, so detection reports "already a git repo" and we never offer init — we import the parent root, which is the current behavior. Leave it unchanged.

That's still the behavior. getGitRoot now returns { root, isRoot }, and openNew / openFromPath show the dialog when isRoot is false rather than writing a project row. The dialog gets a third button, Open <root>, which is the primary action and produces exactly the project you get today. Initialize Git was already wired to initGitAndOpen, which inits and upserts the path verbatim, so initializing the folder you actually picked works without a router change — it just wasn't reachable whenever an ancestor repo existed.

The root test is --show-prefix, which is empty exactly when the path is the work tree root. That keeps two cases right that a filesystem or string check would get wrong:

  • Worktree and submodule roots. At a git worktree root .git is a file, not a directory, so statSync(join(p, ".git")).isDirectory() would call it "not a repo" and offer to git init a worktree Superset created itself.
  • Path comparison. Comparing the picked path against --show-toplevel fails on macOS three separate ways: /tmp and /var canonicalize to /private/..., symlinks resolve to their realpath, and a case-insensitive volume returns canonical casing.

Bare repos and .git directories still fail with "this operation must be run in a work tree" rather than "not a git repository", so they keep falling through as a generic error and are never offered a git init. There are tests for both.

Scope is the Electron open-project path only. packages/host-service has the same walk-up in tryRevParseGitRoot / findByPath, but initLocalRepoInPlace documents ancestor adoption as intentional idempotence, so offering "initialize here" there needs a new wire flag through project.create. That's a separate PR.

How I tested it

bun test, bun run lint and bun run typecheck on macOS, bun 1.3.5, git 2.50.1.

Nine new cases in apps/desktop/src/lib/trpc/routers/workspaces/utils/git.test.ts covering repo root, subdirectory, untracked child folder, no ancestor repo, worktree root, worktree subdirectory, symlinked repo root, bare repo and .git directory. getGitRoot had no coverage before this.

$ cd apps/desktop && bun test src/lib/trpc/routers/workspaces/utils/git.test.ts -t getGitRoot
 9 pass
 0 fail
Ran 9 tests across 1 file.

The subdirectory and worktree cases are the regression guards — they assert the walk-up still happens and that a worktree root is still a root.

packages/host-service is untouched, and the two tests that lock the walk-up semantics pass unmodified:

$ cd packages/host-service && bun test src/trpc/router/project/utils/resolve-repo.test.ts
 30 pass
 0 fail

Full desktop suite, bun run lint and bun run typecheck:

$ cd apps/desktop && bun test
 2558 pass
 2 fail
Ran 2560 tests across 267 files.

$ bun run lint          # exit 0
$ bun run typecheck     # Tasks: 37 successful, 37 total

The 2 failures are pre-existing. I confirmed they fail identically on a clean main (a316a1f) with my changes stashed: Shell Environment > getProcessEnvWithShellPath applies shell PATH and preserves string vars and createWorktree hook tolerance > continues when git is killed mid-hook (timeout shape) but worktree is created. Baseline there is 37 pass / 2 fail in that file; with this change it's 46 pass / 2 fail.

src/no-main-process-blocking.test.ts passes with allowedCounts unedited (git.ts stays at 21, projects.ts at 6) — the change reuses the existing git client rather than adding one.

I haven't captured screenshots. I don't have a working desktop app to drive over CDP, and I'd rather not put a mocked-up image in a PR description. Happy to add them if you'd like, or the dialog copy is:

  • Title: Open the enclosing repository?
  • Body: new-thing isn't a git repository. It's inside dotfiles (/Users/me).
  • Buttons: Cancel · Initialize Git · Open dotfiles

When there's no enclosing repo the dialog is unchanged, both copy and buttons.

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (CI fails on lint warnings too)
  • "Allow edits from maintainers" is checked on fork PRs

Summary by cubic

Ask the user to confirm when they pick a folder inside a Git repo. The dialog lets them open the enclosing repo or initialize Git in the selected folder; default stays “open enclosing,” and only shows when all selected folders are nested.

  • Bug Fixes
    • Desktop: detect non-root selections and show a three-button dialog (Cancel · Initialize Git · Open ). Multi-select supported; “Open enclosing” appears only when every folder has an enclosing repo. Track which action is running so each button shows accurate progress; dedupe projects when a repo root and its subfolder are both selected; surface typed error results from openFromPath.
    • getGitRoot now returns { root, isRoot }; reads --show-toplevel and --show-prefix in two calls to handle names with newlines; uses raw to preserve trailing whitespace and strips only the trailing LF (preserves a trailing CR). Keeps worktrees, submodules, symlinks, and macOS path canonicalization correct.
    • Tests: add cases for roots ending in trailing whitespace and trailing CR, and retain coverage for repo root, subdirectory, untracked child, no ancestor, worktree root/subdir, symlinked root, bare repo, and .git directory.

Summary by CodeRabbit

  • New Features

    • Improved project opening for folders nested inside existing Git repositories.
    • Added an option to open the enclosing repository directly.
    • Git initialization prompts now distinguish standalone folders from folders within repositories.
    • Preserved selected folder paths for confirmation and initialization.
  • Bug Fixes

    • Improved repository-root detection for nested, linked, symlinked, and bare repositories.
    • Improved handling of repository paths containing special characters.