Skip to content

Refresh after a clone on the render thread, and allow one clone per folder [patch] - #474

Merged
matt-edmondson merged 4 commits into
mainfrom
fix/439-clone-on-render-thread
Oct 6, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
fix/439-clone-on-render-thread

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #439

Problem

The Clone button's continuation used TaskScheduler.Current with ExecuteSynchronously. The app installs no SynchronizationContext, so RefreshPage() ran on the worker thread that did the clone. From there it changed Options.ClonedRepos, enumerated Options.Repos and replaced BrowserContentsBase while the render thread was reading all three every frame. Nothing stopped a second click from starting another git clone into the same folder.

Changes

  • New CloneTracker:
    • TryRun(localPath, clone) runs the clone on the thread pool, unless a clone into that folder is already running.
    • The clone is recorded as complete in a finally block, so a clone that fails or throws can be retried.
    • Completion only sets a refresh request, using Interlocked. It never touches UI state.
  • Tick calls Clones.RefreshIfRequested(RefreshPage), so the page is refreshed on the render thread. Other background results, such as OwnerPendingTokenPopup, are already handed over the same way.
  • The Clone button is hidden while its clone runs.
  • ProjectDirector.MakeClone builds the clone action. Only a few lines of ImGui wiring are left untested.

Tests

CloneTrackerTests covers:

  • duplicate refusal, per folder;
  • retrying a folder after its clone completes;
  • no refresh request before completion;
  • completion on a worker thread, and the request being taken exactly once;
  • TryRun refusing a second clone while the first is blocked, then requesting a refresh when it ends;
  • a clone that throws still being recorded as complete;
  • RefreshIfRequested refreshing once per completion;
  • MakeClone cloning a real local repository and logging the result.

With CloneTracker stubbed back to the old behaviour (no in-flight guard, no handoff to the render thread), the duplicate-clone test and the handoff test fail.

The race itself can't be reproduced deterministically, so the tests pin down the handoff and the guard it depends on.

Full suite: 141 of 142 pass locally. The exception, GitCliTests.CloningAnLfsRepositoryRestoresTheFileContentRatherThanThePointer, fails only in this sandbox because of its git-lfs setup. It passes in CI on all three platforms. dotnet format --verify-no-changes reports only the two findings ProjectDirector.cs already has on main.

This PR stands alone on main, and it merges cleanly with #473 (for #438).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KLpJqJzDAe9WoXTVLt65CJ

…older [patch]

The Clone button's continuation ran RefreshPage on the worker thread that
did the clone, since the app has no SynchronizationContext and the
continuation asked to run synchronously. From there it mutated ClonedRepos,
enumerated Repos and replaced the repo browser while the render thread was
reading them, and a second click started another clone into the same folder.

A new CloneTracker records the clones in flight and a refresh request. The
worker only marks its clone complete; Tick takes the request and refreshes
on the render thread, and the button is disabled while its clone runs.

Fixes #439

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KLpJqJzDAe9WoXTVLt65CJ
Comment thread ProjectDirector.Test/CloneTrackerTests.cs Fixed
claude added 3 commits October 6, 2026 17:35
Moves the background clone, its completion and the tick's refresh into
CloneTracker.TryRun, CloneTracker.RefreshIfRequested and
ProjectDirector.MakeClone, so the parts with a rule in them are tested
rather than sitting in the ImGui layer. The Clone button is now hidden
while its clone runs rather than disabled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KLpJqJzDAe9WoXTVLt65CJ
Sonar S4586: a method returning Task should never return null. The task
is now an out parameter, completed when the clone is refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KLpJqJzDAe9WoXTVLt65CJ
@sonarqubecloud

sonarqubecloud Bot commented Oct 6, 2026

Copy link
Copy Markdown

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.

Clone button's continuation runs RefreshPage on a thread-pool thread, racing the render thread over Options.Repos/ClonedRepos and the browser collection

2 participants