Skip to content

fix(repos): stop reporting clones that never ran as successful - #23770

Merged
nwparker merged 7 commits into
stablyai:mainfrom
AmoabaKelvin:fix/clone-stale-project-false-success
Oct 6, 2026
Merged

nwparker merged 7 commits into
stablyai:mainfrom
AmoabaKelvin:fix/clone-stale-project-false-success

Conversation

@AmoabaKelvin

@AmoabaKelvin AmoabaKelvin commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​353 $\color{#cf222e}{\Huge{\mathbf{−}}}$​25 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​328
Prod 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​200 $\color{#cf222e}{\Huge{\mathbf{−}}}$​76 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​124

ELI5

Orca remembers every project you've added. When you cloned a repo into a folder where Orca already had a saved project, it assumed the repo was still there and said "Repository cloned" without running git. If the folder had been deleted or moved, you got a success message and nothing on disk. Now Orca asks git whether that folder really holds the repo you asked for. If it does, Orca opens it. If the folder is gone and the saved project was that same repo, Orca clones it again. Otherwise it tells you which project is in the way instead of pretending it worked.

What Changed

Before: Clone from URL returned any saved project whose path matched the destination, straight away. Git never ran, and nothing looked at the folder. The dialog said "Repository cloned" instantly and the folder could be empty or missing. It looked intermittent because it only happened when a saved project pointed at that folder, and picking another destination "fixed" it. The desktop local clone also matched SSH projects that happened to share the path string, so a local clone could hand back a project on another machine.

After: When a saved project sits at the clone path on the machine doing the clone, one shared decision runs for the desktop local clone, the desktop SSH clone and the runtime clone (orca project setup-clone, repo.clone):

  • The folder is already a clone of that repo: Orca returns the saved project. Git, on the machine that holds the folder, has to show three things:

    • the folder is its own checkout, not a subfolder of another repo (rev-parse --show-cdup prints an empty line);
    • HEAD is one git finished writing: either it resolves (--verify --quiet HEAD), or — for a clone of a repository that has no commits yet — it is an unborn branch that symbolic-ref HEAD resolves and whose exact branch has the origin tracking settings Git writes for a completed empty clone. Git 2.25 can leave a normal unborn branch during an interrupted fetch; that branch has no completed-clone tracking settings and is refused;
    • origin has exactly one URL, and it names the requested repo. Both URLs go through normalizeGitRemoteUrl, so .git, https vs git@ and the username don't matter; local paths must match exactly.

    Repeat clones, a double-clicked Clone and re-running orca project setup-clone still succeed this way, including for a brand-new repository with no commits.

  • Git can't show that, but the saved project was this repo: Orca decides this from the project's stored origin identity. Git clones as usual: into a missing or empty folder it clones, and a non-empty folder it refuses. The project keeps its settings.

  • Anything else: the clone stops with an error naming the saved project, and git never runs. This covers a different repo with the same folder name, a fork, or a project Orca has no record for. The error says the project may hold a different repository or have been moved or deleted, and to remove it from Orca or choose another folder.

Also:

  • Cancel now covers this check. On desktop the cancel handle is registered before it; on SSH it runs after the existing "clone already in progress" guard, using the clone's own cancel handle.
  • The desktop local clone skips saved SSH and runtime-host projects. The runtime clone skips saved SSH projects and retains its existing server-side host matching. runtime: projects without an SSH connection are registered in this process and stay candidates.
  • The SSH clone finds saved projects with the same host check that SSH project registration uses (getRepoSshConnectionId), so a project stored with only executionHostId: ssh:* is found too.
  • After git re-clones into a saved project's folder, the project is brought back to the state a fresh clone would be in, because the deleted folder took its worktree root with it:
    • the desktop notifies that projects changed and the runtime invalidates its worktree caches, so the project's worktrees are re-read;
    • the worktree root is re-created and the authorized-roots cache is dropped, so the first worktree create or file read after the folder comes back doesn't fail on a root that isn't there;
    • on SSH the clone registers the root with the relay. addRemoteRepoFromPath returns an existing project before it gets that far, so without this the relay never learns the path came back.

How it works: reuseSavedCloneTarget (new, src/main/git/saved-clone-target.ts) makes the decision. It runs its git commands through runGitProbeOnHost, which is extracted from probeGitRemoteIdentity so it reuses that function's host routing (local runner, WSL by path, SSH relay) and timeouts. Every command it runs (rev-parse, symbolic-ref, config --get-all) is on the SSH relay's read-only allowlist and has been since SSH support shipped (#590), so older relays work. A probe also stops issuing commands as soon as Cancel fires.

Why

The first version of this PR kept the premise "a saved project at this path means the clone happened" and made it stricter with filesystem checks. As the review pointed out, that still never compared the folder's repo with the one requested, and most of those checks guarded against cases git already refuses. This version checks what "Repository cloned" promises: the requested repo is at that folder. Every other outcome either lets git decide or stops loudly.

Alternatives considered:

  • Always run git and drop the shortcut. This breaks repeat clones: Fix clone abort cleanup ownership #2981's queued-retry tests, Make remote hosts first class: concurrent multi-host workbench #5071's SSH repeat clone and re-running orca project setup-clone all rely on a repeat clone succeeding.
  • Run git whenever the check fails, then refresh the saved project. Refreshing upstream and the icon still leaves the old repo's other settings (base branch, hooks, GitHub account) on a different repo. Adding a second project at the same path duplicates it. Deciding before git whether the saved project was this repo avoids both.
  • Compare against probeGitRemoteIdentity's identity. It prefers the upstream remote, so a fork would match its parent's URL. The check reads origin directly.

Linked Issue

Fixes #23563

Visual Proof

N/A. Nothing is redesigned: this changes the mechanism behind an existing dialog, meaning when a clone reuses a saved project instead of running git. The only visible difference is which existing outcome you get: a real clone, the existing project, or an error shown in the clone dialog's existing error line. The behaviour is covered by the tests below, and by a hidden-app run described under Testing.

Testing

  • Additional review repairs: stored credentials never appear in the refusal error; a quiet missing-HEAD exit preserves localized-host compatibility; timeouts and transport failures do not start another probe; local clone matching excludes runtime-host rows as well as SSH rows. Focused clone/path/provider/relay tests and runtime clone tests pass, as do official node types and the changed-code gate.

  • Regression tests at each boundary. Each fails on main and passes here (checked by reverting the source files):

    • local repos:clone: a stale project for this repo re-clones; a different repo is refused, naming the project; a saved SSH project at the same path doesn't block a local clone; Cancel during the check stops without cloning;
    • SSH repos:cloneRemote: refuses a saved SSH project whose folder isn't this clone, and reuses a finished clone saved with only executionHostId;
    • runtime clone: refuses a mismatched saved project; skips a saved SSH project at the same path;
    • a re-clone into a saved project's folder prepares its worktree root and drops the authorized-roots cache (local), and registers the root with the relay (SSH).
  • Real-git unit tests for the check in saved-clone-target.test.ts:

    • a finished clone is reused, including a differently spelled URL, a real completed clone of an empty repository (unborn HEAD plus tracking settings), and a checkout left on a detached HEAD;
    • a different URL, a different local path, a clone killed before it finished, an origin listing two URLs, a folder nested inside a clone (committed or empty), and a fork project whose stored identity is its upstream are all refused.
  • Updated tests: Fix clone abort cleanup ownership #2981's queued-retry and serialize tests (local and runtime) and Make remote hosts first class: concurrent multi-host workbench #5071's SSH repeat-clone test. The second request now has to pass the check against a finished clone.

  • Hidden-app run, before and after:

    • I launched the real app hidden, with an isolated profile, and drove the Add Project dialog.
    • Steps: clone octocat/Hello-World, delete the folder outside Orca, clone someone-else/Hello-World, then the original URL again.
    • On main the third clone reports "Repository cloned" with the folder still missing.
    • With this change it is refused, naming the saved project, and the fourth clone really re-clones into the same project.
  • Platforms: macOS with real Git 2.55 and a locally built Git 2.25. A real Git 2.25 fetch was paused at its pack response and its isolated process tree killed: the original helper reused the incomplete folder, while the repaired helper refuses it; the real completed empty clone remains reusable. Thirty saved-clone tests pass with Git 2.25. SSH execution-host/provider and relay cancellation paths are covered by mocks; no live SSH, WSL or Windows host was used.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Not covered, on purpose:

  • A clone killed or failed during checkout, after HEAD and origin are written, passes the check. On SSH the relay doesn't clean up a cancelled or failed clone, so this can follow a Cancel there.
  • Hand-deleted objects, a rewrite rule in the repo's own config (insteadOf), and a hand-made origin that was never fetched can also pass.
  • normalizeGitRemoteUrl ignores ports, and treats git@host:x and ssh://host/x as the same. Two git servers on one hostname could be mistaken for each other.
  • These now get an error instead of reusing: a check that times out or can't reach the host's git, a local path spelled differently, and a deleted folder whose project has no stored origin identity, which includes forks. For the last one the project has to be removed from Orca (or another folder chosen) before the URL can be cloned there again. The error distinguishes a recorded different repository from an unknown repository, without displaying the stored remote URL, which may contain credentials.
  • origin is the only stored remote that can name a project's own repo, so there is no second field to fall back on: Repo.upstream is documented as the parent of a fork, and the GitHub avatar repoIcon that getProjectProviderIdentity falls back to is derived from that same upstream. Storing an origin-sourced key alongside the existing one would shrink this case to nothing, and is the follow-up worth doing.

Out of scope (follow-ups):

  • The desktop and runtime clones hold separate locks, so a runtime check can see a desktop clone mid-checkout. On main the same race returns the stale project with nothing on disk.
  • The SSH relay could clean up after a cancelled or failed clone.
  • The file panel keeps showing "Could not load files… ENOENT" for a workspace whose folder came back. It also happens when the folder is re-created outside Orca, so it isn't caused by this change.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

  • Security: stored credential-bearing remote URLs are omitted from refusal errors. The requested URL is never passed to a probe in the saved folder. Clone arguments are unchanged.
  • Performance: new clones and folder-workspace upgrades add no validation reads. A saved finished clone uses two small host Git reads; a saved empty clone uses four, including its completion tracking proof. These replace the old incorrect instant return, so repeat-clone validation has a small added latency: on macOS/Git 2.55, 12 runs each measured median 11.65 ms for 100 files and 10.75 ms for 10,000 files, with exactly two reads in both cases. No tree scan, network lookup or background polling was added. A first timeout, missing cwd or transport failure now stops further probes instead of spending a second timeout budget; a simulated SSH timeout falls from two requests/40 seconds to one request/20 seconds.
  • Wire compatibility: no RPC shape changes. repo.clone can now return an error where it returned a stale project, and older clients show that error.
  • Mobile: nothing in src/shared changed.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred). Focused gates only locally, see Testing; CI covers the rest.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

[Medium risk] Adds validation to prevent reusing incomplete clones.

The PR does not appear safe to merge while clone errors can expose stored credentials and an interrupted clone can still be reported as successful.

Findings

  1. P1 Security Stored credentials appear in errors ▶
  2. P1 Interrupted clone appears complete ▶

Summary

The PR checks a saved project’s checkout and origin before reusing it for a clone, and re-prepares project resources when Git re-clones into a saved path.

  • The latest revision makes refusal errors distinguish a recorded different origin from a project with no recorded origin.
  • That diagnostic exposes an unredacted stored remote URL.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Clone requested at saved project path] --> B[Probe checkout and origin on owning host]
  B -->|Finished clone of requested repo| C[Reuse saved project]
  B -->|Not confirmed; stored origin matches| D[Let Git clone]
  B -->|Not confirmed; origin differs or is unknown| E[Return project-specific error]
Loading

Reviews (5) · Last reviewed commit: "fix(repos): say which reason refused a c..."

Comment thread src/main/git/registered-clone-target.ts Resolved
Comment thread src/main/ipc/repos/remote-repo-clone.ts Resolved

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes

  • Shared checkout checker — the new src/main/git/registered-clone-target.ts inspects <path>/.git asynchronously on both local and SSH hosts, returning valid / absent / invalid / unverifiable under scanWorktreeMarkerSync's validity rules, with WSL confirmation before trusting a negative.
  • Local clone — repo-clone-lifecycle.ts probes the registered checkout before reusing a row and now matches only local-host rows; a missing .git re-clones and keeps the row, an invalid/unverifiable checkout throws instead of reporting success.
  • Runtime clone — runtime-repository-clone-controller.ts applies the same probe and now excludes SSH-owned rows from its target lookup.
  • SSH clone — remote-repo-clone.ts claims the in-flight key before probing, recognizes executionHostId: ssh:*-only rows, and updates a stale row after re-cloning.
  • Bounded local read — runBoundedLocalPathRead runs a read under the shared two-slot probe semaphore; new host-probe tests cover the timeout and both WSL confirmation outcomes.

I also reverted repo-clone-lifecycle.ts to the base revision locally and confirmed 4 of the new repos-clone-registered-target.test.ts cases fail without the fix, so the regression coverage is real.

ℹ️ Clone probes share the polling semaphore, so a hung read elsewhere can block a healthy clone

inspectLocalCloneCheckout reads through runBoundedLocalPathRead, which acquires the module-global localWorktreePathProbeSemaphore (2 slots) also used by sidebar worktree polling. An in-flight read holds its slot until the underlying stat settles, so two hung reads on an unrelated mount can keep both slots busy and make a clone target probe abort at its 2s deadline, reporting could not check the existing project … try again once the folder is reachable for a destination that is perfectly healthy. Not blocking — the result is safe (no false success) — just flagging the coupling in case a dedicated bound for the clone probe is warranted.

Technical details
# Shared semaphore can starve clone probes

## Affected sites
- `src/main/local-worktree-path-presence.ts` — `runBoundedLocalPathRead` releases the semaphore slot only in `read().finally(release)`, so a `stat` that never settles holds the slot.
- `src/main/git/registered-clone-target.ts` — `inspectLocalCloneCheckout` runs up to ~7 reads (`.git`, then `HEAD`/`objects`/`refs`, then `commondir`) under one 2s deadline, so they all queue on the same two slots.

## Required outcome
- A clone probe against a reachable destination should not be reported as "did not respond in time" because unrelated reads are occupying the shared slots.

## Suggested approach (optional)
- Give `inspectLocalCloneCheckout` its own semaphore/limit, or acquire a single slot for the whole inspection so one clone never depends on unrelated polling reads.

## Open questions for the human (optional)
- Is the coupling intentional (the body describes reusing the two-slot limit to cap threads), and if so is a starved-but-healthy clone acceptable?

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5488a454-4550-4d9a-9f14-45e30673210c
📥 Commits

Reviewing files that changed from the base of the PR and between 88ca8a47433ace6471b278cff31b5b41a0b261da and 884cb39.

📒 Files selected for processing (6)
  • src/main/git/saved-clone-probe-budget.test.ts
  • src/main/git/saved-clone-target.test.ts
  • src/main/git/saved-clone-target.ts
  • src/main/ipc/repos-local-clone-lifecycle.test.ts
  • src/main/ipc/repos/repo-clone-lifecycle.ts
  • src/main/runtime/runtime-repository-clone-controller.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Clone handling now verifies saved repositories against Git checkout state and remote identity before reuse. Local, runtime, and SSH paths use the shared saved-target check with host-specific Git probes. The clone paths filter saved repositories by path and execution host. When they return an existing repository, they update worktree or session-root state. Tests cover matching and mismatching remotes, incomplete checkouts, occupied paths, cancellation, and concurrent clone requests.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 884cb

This change makes local, runtime and SSH clones verify a saved project's checkout and remote before reusing it. This prevents false-success clones. No actionable merge-blocking risk was identified in the supplied context.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 884cb

The change replaces path-only reuse with checks that the requested repository exists on the correct machine. Cancellation and restoration handling support that stronger boundary. No introduced security concern was established, but remote enforcement and broader security coverage remain incompletely verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected change affects the requested checkout, its saved project, and dependent root registrations on the executing local/runtime process or selected SSH connection. Host-filtered lookup and explicit dispatch narrow wrong-machine reuse; no new cross-service authority was established in the inspected paths.

Trust Boundaries and Controls

  • observed — The requested URL is compared with origin metadata rather than interpolated into probe commands. Probe arguments are fixed Git reads, with the unborn branch name escaped before use in a configuration regular expression. Clone commands retain the argument separator protecting the URL from interpretation as Git options.

Resilience and Maintainability Implications

  • observed — Local verification runs inside the existing clone-path lock after pending cleanup; SSH verification runs after its in-flight guard; runtime operations retain per-path serialization. Local and SSH cancellation now cover verification. These controls contain concurrent clone work, although probing and cloning are not one atomic filesystem transaction.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The clone changes support [#23563]. The incremental diff also changes .github/actions/install-node-dependencies/action.yml to alter pnpm and native-module caching, and changes `.github/CONTRIBUTING.… Remove the unrelated dependency-cache, contribution-guidance, and mobile golden-file formatting changes from this PR, or move them to a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 51.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed [ #23563 ] requires Orca to stop reporting a clone as successful when the destination does not contain the requested repository. The reviewed clone logic checks the saved destination with Git before r…
Title check ✅ Passed The title clearly summarizes the main change: preventing clone operations from reporting success when Git did not run.
Description check ✅ Passed The description is detailed and covers the required sections, including the change, rationale, linked issue, visual proof, testing, review notes, and checklist. It also documents platform coverage and…
Full details: Out of Scope Changes check

Explanation

The clone changes support [#23563]. The incremental diff also changes .github/actions/install-node-dependencies/action.yml to alter pnpm and native-module caching, and changes .github/CONTRIBUTING.md and .gitattributes for unrelated contribution guidance and mobile golden-file formatting. These changes do not support the clone fix.

  • Fix all pre-merge checks with AI

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.

@AmethystLiang

Copy link
Copy Markdown
Contributor

Design review: the reported case is fixed, but the approach has a basic problem

The diagnosis is right: a saved project outlives its folder, and clone trusted the saved project. The repro is real and the new tests fail on main. Catching SSH projects that share a local path string is also a good find.

The problem is the fix itself. It keeps the assumption that "a saved project at this path means the clone already happened". Then it adds about 200 lines of filesystem checks to predict what git clone would do. Most of those checks protect against failures git already refuses. The one outcome that can actually cause this bug gets the weakest proof.

1. Most of the new checks guard against failures git already refuses

Real git (2.52) at the destination:

  • empty folder → clone succeeds
  • folder containing only an empty .worktrees/ → fatal: destination path ... already exists and is not an empty directory
  • folder that is already a repository → same refusal

Here's what happens when each checker verdict is wrong:

Checker says What the PR does If the verdict is wrong
absent runs git clone git refuses a non-empty folder → an error, no damage
invalid / unverifiable stops with an error nothing runs → safe
valid returns the saved project; git never runs "Repository cloned" with nothing cloned — this bug

The "Why" section says a broken or unclear .git has to stop the clone because "cloning on top of it could hit a folder that's actually there". Git already refuses that. So the four-state checker, the WSL double-check, the 2-second deadline and the shared read limit in runBoundedLocalPathRead mostly guard cases that are already safe. This also makes Greptile's WSL .git-file finding low severity: it ends in git's error, not damage.

2. valid means "some repo is here", not "the repo you asked for"

registeredCheckoutIsReusable only asks whether the folder holds a valid repository. The requested URL never enters the decision.

Example: stablyai/orca is saved at ~/code/orca, and the user clones their fork https://github.com/me/orca.git into ~/code. The clone returns the stablyai/orca project, git never runs, and the dialog says "Repository cloned" (useAddRepoCloneFlow.ts:165). Forks, and common names like api, web or docs, hit this easily.

This already happens on main, so it isn't a regression. But it has the same symptom as #23563, and this PR explicitly keeps it.

Repro test (paste into repos-clone-registered-target.test.ts; passes on this branch)
// needs: import { execFileSync } from 'node:child_process'
it('does not report a clone of a different repo with the same folder name as success', async () => {
  const destination = await createDestination()
  const clonePath = join(destination, 'orca')
  execFileSync('git', ['init', '-q', clonePath])
  execFileSync('git', ['-C', clonePath, 'remote', 'add', 'origin', 'https://github.com/stablyai/orca.git'])
  registeredRows = [registeredRepo(clonePath)]

  const result = await handlers.get('repos:clone')!(null, {
    url: 'https://github.com/me/orca.git',
    destination
  })

  // Passes on this branch: git never runs and the stablyai/orca project comes back.
  expect(gitSpawnMock).not.toHaveBeenCalled()
  expect(result).toMatchObject({ id: 'registered-repo' })
})

3. A re-cloned stale project keeps the old repo's details

When a missing folder is re-cloned into an existing saved project, the local, remote-runtime and SSH paths all keep its upstream, icon and remote identity. None of them re-runs detectRepoIconAndUpstream.

The PR says the background refresh fixes this. It doesn't, for upstream:

  • the remote-identity refresh never writes upstream, and it only refreshes a known remote every 6 hours;
  • the icon refresh is skipped whenever upstream is set;
  • the fork backfill only runs when upstream was never set (runtime-repository-fork-backfill.ts:35).

Fork sync reads upstream. This only matters when the new clone is a different repo with the same folder name, but then the project describes the wrong repository.

4. Why the shortcut exists

#2981 added the early return. Its only two tests are "dedupes retry when abort races with a successful clone close" and "serializes concurrent clones for the same target". Both cover a request that waits behind another clone of the same URL. That second request should get the finished clone. It should not treat "a project was saved here at some point" as proof.

Smaller issues

  • Cancel doesn't cover the new check. It runs before cancel is wired up: local :144 vs :159; SSH :83 vs :98.
    • A slow SSH check that finishes after the user closes the dialog still starts git clone and adds the project.
    • A hung connection keeps the SSH destination reserved for up to 30 seconds per request. Retries then see "A clone is already in progress".
  • Unrelated behavior changes are bundled in. Each should be its own PR:
    • the local clone now ignores runtime:* projects on the same machine, and addRepo doesn't check for an existing path, so a re-clone can create a second project for the same folder;
    • SSH now matches projects by executionHostId;
    • the check shares a read limit with sidebar polling (Pullfrog's starvation note).
  • Git's repo-validity rules are copied instead of reused. They come from scanWorktreeMarkerSync rather than extending repo-git-marker-scan.ts, so two definitions of "valid repository" can now drift apart.

Suggested design: reuse only with proof it's the same repo, and let git decide the rest

  1. A project is saved at the path: run probeGitRemoteIdentity on the machine that owns it, and compare its canonicalKey with normalizeGitRemoteUrl(url). If they match, return the saved project. Both helpers already exist and work on local and SSH machines. The probe has a timeout and cancel support, and it reports "couldn't reach git" as unavailable, which proves nothing either way. Pin the probe to the folder itself (rev-parse --show-toplevel equals the path, or GIT_CEILING_DIRECTORIES), because git searches parent folders.
  2. Anything else (no saved project, folder gone, a different repo, unreadable): run git clone. It clones into a missing or empty folder and refuses everything else.
  3. Git succeeds and a stale project is saved at the path: keep the project only if its stored remote identity matches the URL. Otherwise re-run detectRepoIconAndUpstream, or add a new project.
  4. Git fails and a project is saved at the path: say "This folder is already the project X in Orca". That only needs "the clone failed and a project exists here", not parsing git's error text.

Why this design:

  • Keeps a repeat clone succeeding. orca project setup-clone goes through the same runtime clone controller, and agents may rely on re-running it.
  • Covers both Fix clone abort cleanup ownership #2981 tests. The waiting request sees the just-finished clone's remote and reuses it.
  • Behaves the same on local, WSL and SSH, because git runs on the machine that owns the folder.
  • Removes most of the new code. registered-clone-target.ts and runBoundedLocalPathRead are no longer needed.

How this review was produced

These were the prompts, with the PR checked out locally:

Review it-> it has some foundamental issue. What are they?? What do you think of this PR's design?

how confident are you with your assessment?? … think deeply about it

The follow-up mattered. The first answer proposed "always let git decide". That would break a repeat orca project setup-clone, and it also overstated the cancel issue. Asking for confidence made the agent actually run git against empty, non-empty and existing-repo folders, read #2981's tests, and look for callers that depend on repeat clones.

To redesign this, here is a prompt you could give your agent:

Redesign the fix for #23563 (Clone from URL reports success without cloning). Before writing code:

1. Find out why clone returns a saved project without running git. Read the commit and tests that
   added that early return (#2981) and state exactly which situations it was meant to handle.
2. For each outcome your check can report, write down what happens if that outcome is wrong. Test
   real git: clone into an empty folder, a folder containing only an empty subfolder, and an existing
   repository. Keep only checks that protect against something git does not already refuse.
3. Success must mean the requested repository is at the destination, not just some repository.
   Write a test where two different URLs map to the same folder name and make sure it cannot report
   success.
4. Find every caller of the local, runtime and SSH clone paths, including `orca project setup-clone`,
   and list which ones depend on a repeat clone succeeding.
5. Before writing a new helper, search for existing ones that answer the question on the right
   machine (remote identity probing, URL normalization, git repository detection), and use or
   extend them.
6. When a saved project is reused or kept after a re-clone, list which of its saved details
   (upstream, icon, remote identity) could now describe a different repository, and check whether
   anything refreshes them.

Then propose the smallest design that satisfies all of the above. Keep unrelated behavior changes
(host matching, locking, shared read limits) out of this PR.

@AmoabaKelvin
AmoabaKelvin force-pushed the fix/clone-stale-project-false-success branch from 9e4fd8d to a77ea35 Compare September 29, 2026 13:35

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 5cb6ec56-ba45-4f39-ba1d-9d52a7a3438a

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4fd8d1d1e52c2bd3ff7efcf18c36ac04ee5cce and a77ea35164b1f408de502f2800dd8f3fa50335d3.

📒 Files selected for processing (9)
  • src/main/git/saved-clone-target.test.ts
  • src/main/git/saved-clone-target.ts
  • src/main/ipc/repos-local-clone-lifecycle.test.ts
  • src/main/ipc/repos-remote.test.ts
  • src/main/ipc/repos/remote-repo-clone.ts
  • src/main/ipc/repos/repo-clone-lifecycle.ts
  • src/main/repo-git-remote-identity.ts
  • src/main/runtime/orca-runtime-tests/repository-project-operations-part-02.spec.ts
  • src/main/runtime/runtime-repository-clone-controller.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment thread src/main/ipc/repos/remote-repo-clone.ts Resolved
@AmoabaKelvin

AmoabaKelvin commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks for the review, and for spelling out how it was produced. You were right about the premise: the first version made "a saved project at this path means the clone happened" stricter, instead of checking the thing the dialog promises. I've replaced the branch with a redesign built on your suggested design.

What it does now

When a saved project sits at the clone path on the machine doing the clone, the desktop local clone, the SSH clone and the runtime clone make the same decision (reuseSavedCloneTarget):

  • Reuse only when git, on the machine that holds the folder, proves the folder is a finished clone of the requested repo:
    • rev-parse --show-cdup --verify HEAD prints an empty first line, so the folder is its own top level. That works through symlinked paths without comparing path strings.
    • HEAD resolves. A clone killed during fetch leaves origin set but HEAD -> refs/heads/.invalid, which I reproduced by SIGKILLing a real clone.
    • config --get-all remote.origin.url is a single URL with the same normalizeGitRemoteUrl key as the request. Local paths, which don't normalize, must match exactly.
  • Otherwise, run git only if the saved project's stored gitRemoteIdentity was read from origin and matches the URL, so the project was this repo. Git then clones into the missing or empty folder, or refuses a non-empty one, and the project keeps its settings.
  • Otherwise, stop with an error naming the project, without running git.
  • No saved project: the path is unchanged.

The host routing and timeouts come from probeGitRemoteIdentity, via runGitProbeOnHost extracted from it. Cancel covers the check: on desktop the handle is registered before it runs, and on SSH the check runs after the in-flight guard. The saved project is re-read after the check.

#2981's queued retry still works because the second request finds the first request's finished clone. The same applies to #5071's SSH repeat clone and a repeat orca project setup-clone.

registered-clone-target.ts, the WSL double-check, the deadline and runBoundedLocalPathRead are all gone. The change is about 124 net production lines.

Where I went a different way from your suggestion, and why (happy to switch either one):

  1. A saved project whose folder no longer holds this clone. Your steps 2 and 3 run git clone, then keep the project only if its stored identity matches, and otherwise refresh it or add a new one. I make that decision before git runs: clone only when the stored origin identity matches, and otherwise refuse by name.
    • Refreshing upstream and the icon would still leave the old repo's base branch, hooks and GitHub account on a different repo. A second project at the same path would duplicate it.
    • An SSH clone that is cancelled or fails during checkout leaves a half-written tree, and the relay doesn't clean it up. If git had run under a project for a different repo, the next attempt could reuse that tree.
    • The cost: re-cloning a deleted folder errors when Orca has no origin record for the project. That includes forks, whose stored identity comes from upstream.
  2. The error naming the project. It is raised before git runs, in the refusal case above, not after git fails. When git does run and fails, its own message is shown. Nothing parses git's error text.
  3. The proof also requires HEAD to resolve, which catches the killed clone above. And the origin is read with --get-all, so an origin listing two URLs never matches (fetch uses the first, --get returns the last).
  4. Host filtering stayed in, but narrower. The desktop local lookup matched SSH projects that share the path string, so a local clone could return an SSH project: the same false success. The local and runtime lookups now skip SSH projects only. runtime: projects without an SSH connection stay candidates, as getRepoProbeHostId describes, so this doesn't bring back the duplicate-project problem. The SSH clone now finds saved projects with getRepoSshConnectionId, the same check SSH project registration uses, so a project stored with only executionHostId: ssh:* is found too.

Not covered, on purpose

  • Can still pass the check: a clone killed or failed during checkout, after HEAD and origin are written. On SSH this can follow a Cancel, because the relay doesn't clean up. Also hand-deleted objects, a repo-local insteadOf, and a hand-made origin that was never fetched.
  • Normalizer folding: normalizeGitRemoteUrl ignores ports and folds scp and ssh:// forms, so two servers on one hostname could look like the same repo.
  • Errors where main returned the project: a repeat clone of an empty repository, a check that timed out, a local path spelled differently, and a deleted folder whose project has no stored origin identity.
  • Follow-ups:
    • The desktop and runtime clones use separate locks, so a runtime check can see a desktop clone mid-checkout. On main that race returns the stale project with nothing on disk.
    • The SSH relay should clean up a cancelled or failed clone.

Comment thread src/main/git/saved-clone-target.ts Resolved
Comment thread src/main/git/saved-clone-target.ts Resolved
Comment thread src/main/ipc/repos/remote-repo-clone.ts Resolved
@greptile-apps

This comment has been minimized.

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes

This revision replaces the previous filesystem-marker approach entirely; the prior head (9e4fd8d) is not an ancestor of the new one, so the full diff was reviewed as the delta.

  • New shared decision — reuseSavedCloneTarget (src/main/git/saved-clone-target.ts) returns the saved project only when git on the owning host shows the folder is its own checkout (rev-parse --show-cdup empty, --verify HEAD resolves) with a single origin URL naming the requested repo; otherwise it lets git clone when the project's stored origin identity matches, and throws naming the project.
  • Host-routed probe — runGitProbeOnHost extracted from probeGitRemoteIdentity (src/main/repo-git-remote-identity.ts), preserving local/WSL/SSH routing and the 5s/20s timeouts; both commands are on the relay's read-only allowlist.
  • Call sites — the local (repo-clone-lifecycle.ts), SSH (remote-repo-clone.ts) and runtime (runtime-repository-clone-controller.ts) clones all probe before reusing. Local/runtime skip SSH-owned rows; SSH probes after the in-flight guard using the clone's own cancel handle; a real re-clone into a saved row invalidates/notifies.
  • Tests — real-git unit tests for the decision plus IPC and runtime boundary tests. Verified locally: saved-clone-target (8), repos-remote (35), repos-local-clone-lifecycle (27), repo-git-remote-identity + enrichment (39) all pass.

All four prior inline threads (Greptile) are marked resolved and superseded by this revision.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@AmoabaKelvin
AmoabaKelvin force-pushed the fix/clone-stale-project-false-success branch from a77ea35 to e3ce705 Compare September 29, 2026 13:56

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes

Delta since the prior pullfrog review (a77ea35), i.e. commit e3ce705:

  • SSH saved-project lookup widened — cloneRemoteRepo's findSaved now matches via getRepoSshConnectionId(repo) === args.connectionId (src/main/ipc/repos/remote-repo-clone.ts:65), so a saved SSH project stored with only executionHostId: ssh:<target> is found, matching how SSH registration writes rows. Previously such a row was missed and git would clone over it.
  • Parameterized SSH reuse test — repos-remote.test.ts now runs the "returns an existing SSH repo" case over both connectionId and executionHostId: ssh:*-only ownership, and asserts no clone/add happened.
  • Test-only helper refactor — readGitExecOptions replaces gitOption in the local clone lifecycle test, typing the shared spy's (argv, options) shape.

Verified locally at e3ce705: saved-clone-target (8), repos-remote (36), repos-local-clone-lifecycle (27) all pass. No open Pullfrog inline threads remain.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/git/saved-clone-target.ts Resolved
Comment thread src/main/git/saved-clone-target.ts Resolved

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes

Delta since the prior pullfrog review (e3ce705), commits aa326d2, f5721c4, 786c849, 88ca8a4.

  • Empty-repository repeat clone now reuses — isFinishedCheckout splits the HEAD check: rev-parse --show-cdup --verify HEAD, and on failure rev-parse --show-cdup (top level) plus symbolic-ref HEAD. A finished clone of a repository with no commits has an unborn HEAD, which --verify rejects but symbolic-ref resolves; a killed clone's HEAD -> refs/heads/.invalid is an invalid refname, so symbolic-ref still fails (verified empirically).
  • Worktree root is re-prepared after a local/runtime re-clone — repo-clone-lifecycle.ts and runtime-repository-clone-controller.ts call prepareLocalWorktreeRootForRepo + invalidateAuthorizedRootsCache (and notify/invalidate) when git re-creates a saved project's folder, since the deleted folder took its worktree root with it.
  • SSH re-clone registers the recovered root — remote-repo-clone.ts notifies session.registerRoot when git re-creates a saved SSH project's folder, because addRemoteRepoFromPath returns an existing project without registering it.
  • Refusal message says which reason applies — the error now names the stored origin URL (Orca has that project recorded as <url>) or reports that there is no record, since the two cases need different user action.

The new real-git tests fail on the prior revision (unborn-HEAD reuse, killed-clone refusal, detached HEAD, nested-in-empty-clone) and the IPC/runtime tests assert the new worktree-root and registerRoot side effects. No open Pullfrog inline threads remain.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker force-pushed the fix/clone-stale-project-false-success branch from 88ca8a4 to a4dc55f Compare October 5, 2026 05:44

@nwparker nwparker 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.

Saved clone reuse requires bounded host-targeted Git verification of checkout and requested origin; identity is rechecked after await and cancellation/path locks retained. Reclone repairs roots/cache registration. Rebase retained upstream promptGuardGitEnv. 92 tests, node typecheck and quality gate pass. Refuses ambiguous stale projects instead of reporting false success.

Required PR Checks verify passed on this exact commit: a4dc55f2cbde7d938e0e6409baead18077cc7b9f.

@nwparker nwparker 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.

Pre-merge audit changes the risk assessment to high; this PR is excluded from the requested low-risk bug-fix merge batch.

The current head a4dc55f2cbde7d938e0e6409baead18077cc7b9f still includes the stored origin remoteUrl verbatim in the refusal error in src/main/git/saved-clone-target.ts. Git URLs can contain credentials, so the diagnostic needs redaction or omission of the raw URL before merge.

The unresolved interrupted-clone finding also needs a regression test covering SSH cancellation during fetch and proof that an unborn HEAD is sufficient evidence of a completed empty clone. The existing symbolic-ref fallback does not establish clone completion independently.

CI is green, but these unresolved bot findings prevent the low-risk merge decision. My earlier approval overlooked the credential disclosure and is superseded by this review.

AmoabaKelvin and others added 7 commits October 5, 2026 21:03
A saved project outlived its folder, so Clone reported success without running git.

Fixes stablyai#23563
A clone of a repository with no commits leaves HEAD unborn, which
`rev-parse --verify HEAD` rejects exactly like the `refs/heads/.invalid`
HEAD a killed clone leaves. The saved-project check read both as "not a
finished clone", so cloning a brand-new empty repo a second time — a
double-clicked Clone, a re-run `orca project setup-clone`, or stablyai#2981's
queued retry — fell through to git and died on "destination path already
exists and is not an empty directory".

Ask the two questions apart when `--verify` fails: `symbolic-ref HEAD`
resolves an unborn branch but not a killed clone's HEAD. It is already on
the relay's read-only allowlist. Probes also short-circuit once the clone
is cancelled, so no probe after Cancel spawns git.
…lder

The branch that re-clones into a saved project's folder returned the
project without the two steps every sibling branch runs: the worktree
root that vanished with the deleted folder is never re-created, and the
authorized-roots cache keeps the answers it cached while the folder was
missing. Creating a worktree or reading files in the recovered project
then fails on a root that is not there.
`addRemoteRepoFromPath` returns an existing project before it notifies
`session.registerRoot`, which was unreachable before the saved-project
check could re-clone into a project's own path. The relay therefore never
learned the folder came back, so sessions under it stayed unregistered.
Notify from the clone, mirroring the folder-to-git upgrade beside it.
One message covered both "that project holds a different repository" and
"Orca has no record of what that project held", which need different
things from the user. Name the recorded URL when there is one, and say
plainly that there is no record when there isn't — the second is the case
a project predating remote-identity enrichment lands in, and the user
cannot act on it without being told why.

Also drops the `upstream` remote-name check into a named binding: `origin`
is the only stored remote that names a project's own repo, because
`Repo.upstream` and the GitHub avatar derived from it both name the repo a
fork came from.
@nwparker
nwparker force-pushed the fix/clone-stale-project-false-success branch from a4dc55f to 884cb39 Compare October 6, 2026 04:09

@nwparker nwparker 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.

Reviewed the repaired source and independently verified the two substantive bot findings. The refusal diagnostic no longer includes the stored origin URL; a synthetic credential-bearing regression fails before the change and passes afterward. An actual killed fetch with Git 2.25 reproduces the incomplete-clone false success, and the repaired check rejects it while retaining completed empty-clone reuse.

Performance checks confirm two small owning-host reads for an ordinary reused clone, four for an empty clone, and zero extra checks on fresh clones. Local median validation is about 11 ms with either 100 or 10,000 tracked files. The repair eliminates the second read/timeout after a first transport failure and introduces no polling, network fetch, tree scan, or ref fan-out. This is a small necessary validation cost; live SSH/WSL latency was not measured locally.

Focused tests, real Git 2.25 fixtures, typecheck and changed-code quality pass. Fresh full PR Checks CI on this exact head is green and all review threads are resolved. Approving the repaired change; this review does not merge the PR.

@nwparker
nwparker merged commit 9e8a3a5 into stablyai:main Oct 6, 2026
30 checks passed
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.

[Bug]: Cloning via URL reports success but repo is not cloned

3 participants