Skip to content

fix: stop the idle-mirror sweep reaping a live mirror under a mirror.git path segment [patch] - #49

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/gbsc-42-mirror-segment
Sep 28, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/gbsc-42-mirror-segment

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #42

What was wrong

Clients send repository paths with a .git suffix, so the repository studio/mirror is stored at <root>/github/studio/mirror.git/mirror.git. MirrorStore.Enumerate() returned every directory named mirror.git, including the parent path segment. That parent never gets a marker, so LastTouched fell back to its creation time. After MirrorIdleMaxAge the sweep reaped it recursively, and the live mirror inside went with it. Until then, the mirror's bytes were also counted twice.

Change

  • MirrorStore.Enumerate() drops a mirror.git candidate that contains another candidate and has no files of its own. A real mirror is a bare repository, so it always has at least HEAD. A path-segment directory has only subdirectories. Leaf mirror.git directories are still listed as before, including empty ones and half-written ones.
  • MirrorMaintenanceService.Sweep() covers the remaining case, where a real mirror holds another real mirror (repository studio alongside studio/mirror.git):
    • It won't reap a mirror while another enumerated mirror sits inside it.
    • It measures only a mirror's own files, not those of a nested mirror.
  • Added MirrorStore.IsInside(path, directory), a strict-descendant check that both classes share.

I didn't reject mirror.git in IsSafeSegment, which the issue also suggested. That would make a repository literally named mirror impossible to serve.

Tests

  • MirrorStoreTests.Enumerate_ARepositoryNamedMirror_ListsOnlyTheMirrorAndNotItsPathSegment
  • MirrorStoreTests.Enumerate_AMirrorHoldingAnotherMirror_ListsBoth
  • MirrorMaintenanceServiceTests.Sweep_ARepositoryNamedMirrorStillBeingQueried_IsKept: the issue's scenario, 40 days in with fresh markers. MockFileSystem stamps creation times from the real clock, so the test pins the path segment's creation time to the fake clock.
  • MirrorMaintenanceServiceTests.Sweep_AnIdleMirrorHoldingALiveMirror_IsKept

Results:

  • With Mirrors/*.cs reverted, 3 of the new tests fail. Enumerate_AMirrorHoldingAnotherMirror_ListsBoth is a guard and passes either way.
  • With the fix, the full suite passes: 207/207 (dotnet test --project GitBranchStateCache.Tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PtWVWaDh3nqJL1eKD7vFxU


Generated by Claude Code

…git path segment [patch]

The repository studio/mirror is stored at studio/mirror.git/mirror.git,
so Enumerate() also returned the parent path segment as a mirror. The
parent never carries a marker, looked idle from the day it was created,
and was reaped along with the live mirror inside it. It was also
measured, which double-counted the mirror's bytes.

Enumerate() now drops a mirror.git directory that holds another
candidate and has no files of its own, which a bare repository always
has. The sweep also refuses to reap a mirror that holds another
enumerated mirror, and measures only its own files.

Fixes #42

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PtWVWaDh3nqJL1eKD7vFxU
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit e3d25ae into main Sep 28, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/gbsc-42-mirror-segment branch September 28, 2026 01:44
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.

Idle-mirror sweep deletes an actively used mirror when the repository is named mirror (path segment mirror.git)

2 participants