1+ /**
2+ * Local tracking checkouts for remote Git references, one per remote and
3+ * branch. Each checkout permanently tracks a single ref: the requested branch
4+ * when the cache key has one, otherwise the remote default branch. Content
5+ * follows "newest wins": refresh fetches and hard-resets, so readers may
6+ * observe the checkout move underneath them.
7+ */
18import path from "path"
29import { Context , Effect , Layer , Schema } from "effect"
310import { FSUtil } from "./fs-util"
@@ -135,29 +142,32 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | EffectFl
135142 if ( input . branch ) yield * validateBranch ( input . branch )
136143
137144 const repository = input . reference . label
138- const localPath = Repository . cachePath ( global . repos , input . reference )
145+ const localPath = Repository . cachePath ( global . repos , input . reference , input . branch )
139146 const cloneTarget = Repository . parse ( input . reference . remote ) ?? input . reference
140147
141148 return yield * flock
142149 . withLock (
143150 Effect . gen ( function * ( ) {
144151 yield * cacheOperation ( fs . ensureDir ( path . dirname ( localPath ) ) , "ensure cache directory" , localPath )
145152
146- const exists = yield * fs . existsSafe ( localPath )
147153 const existing = yield * git . repo . discover ( AbsolutePath . make ( localPath ) )
148154 const origin = existing ? yield * git . remote . get ( existing ) : undefined
149155 const originReference = origin ? Repository . parse ( origin ) : undefined
150- const reuse = Boolean ( existing && originReference && Repository . same ( originReference , cloneTarget ) )
151- if ( exists && ! reuse ) {
156+ // Discovery walks upward, so an enclosing repository with a
157+ // matching origin could masquerade as the cache entry; reuse
158+ // requires the checkout to live exactly at the cache path.
159+ const worktree = existing ? yield * fs . resolve ( localPath ) : undefined
160+ const reuse = Boolean (
161+ existing &&
162+ existing . worktree === worktree &&
163+ originReference &&
164+ Repository . same ( originReference , cloneTarget ) ,
165+ )
166+ if ( ! reuse && ( yield * fs . existsSafe ( localPath ) ) ) {
152167 yield * cacheOperation ( fs . remove ( localPath , { recursive : true } ) , "remove stale cache" , localPath )
153168 }
154169
155- const currentBranch = reuse && existing ? yield * git . history . branch ( existing ) : undefined
156- const status = statusForRepository ( {
157- reuse,
158- refresh : input . refresh ,
159- branchMatches : input . branch ? currentBranch === input . branch : undefined ,
160- } )
170+ const status = ! reuse ? ( "cloned" as const ) : input . refresh ? ( "refreshed" as const ) : ( "cached" as const )
161171
162172 if ( status === "cloned" ) {
163173 yield * git . repo
@@ -177,25 +187,28 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | EffectFl
177187 . pipe ( Effect . mapError ( ( error ) => new FetchFailedError ( { repository, message : error . message } ) ) )
178188
179189 if ( input . branch ) {
180- const requestedBranch = input . branch
181190 yield * git . sync
182- . fetchBranch ( existing , { branch : requestedBranch } )
191+ . fetchBranch ( existing , { branch : input . branch } )
183192 . pipe ( Effect . mapError ( ( error ) => new FetchFailedError ( { repository, message : error . message } ) ) )
193+ }
184194
185- yield * git . sync . checkoutRemoteBranch ( existing , { branch : requestedBranch } ) . pipe (
186- Effect . mapError (
187- ( error ) =>
188- new CheckoutFailedError ( {
189- repository,
190- branch : requestedBranch ,
191- message : error . message ,
192- } ) ,
193- ) ,
194- )
195+ // Checking out the tracked ref before resetting keeps the
196+ // checkout self-healing even if it was left on another
197+ // branch.
198+ const branch = input . branch ?? ( yield * git . history . defaultRemoteBranch ( existing ) )
199+ if ( branch ) {
200+ yield * git . sync
201+ . checkoutRemoteBranch ( existing , { branch } )
202+ . pipe (
203+ Effect . mapError (
204+ ( error ) => new CheckoutFailedError ( { repository, branch, message : error . message } ) ,
205+ ) ,
206+ )
195207 }
196208
209+ const target = branch ?? ( yield * git . history . branch ( existing ) )
197210 yield * git . sync
198- . resetHard ( existing , yield * resetTarget ( git , existing , input . branch ) )
211+ . resetHard ( existing , target ? `origin/ ${ target } ` : "HEAD" )
199212 . pipe ( Effect . mapError ( ( error ) => new ResetFailedError ( { repository, message : error . message } ) ) )
200213 }
201214
@@ -229,12 +242,6 @@ export const node = makeGlobalNode({
229242 deps : [ EffectFlock . node , FSUtil . node , Git . node , Global . node ] ,
230243} )
231244
232- function statusForRepository ( input : { reuse : boolean ; refresh ?: boolean ; branchMatches ?: boolean } ) {
233- if ( ! input . reuse ) return "cloned" as const
234- if ( input . branchMatches === false || input . refresh ) return "refreshed" as const
235- return "cached" as const
236- }
237-
238245function errorMessage ( error : unknown ) {
239246 return error instanceof globalThis . Error ? error . message : String ( error )
240247}
@@ -245,17 +252,4 @@ function cacheOperation<A, E, R>(effect: Effect.Effect<A, E, R>, operation: stri
245252 )
246253}
247254
248- const resetTarget = Effect . fnUntraced ( function * (
249- git : Git . Interface ,
250- repository : Git . Repository ,
251- requestedBranch ?: string ,
252- ) {
253- if ( requestedBranch ) return `origin/${ requestedBranch } `
254- const remoteHead = yield * git . history . defaultRemoteBranch ( repository )
255- if ( remoteHead ) return `origin/${ remoteHead } `
256- const currentBranch = yield * git . history . branch ( repository )
257- if ( currentBranch ) return `origin/${ currentBranch } `
258- return "HEAD"
259- } )
260-
261255export * as RepositoryCache from "./repository-cache"
0 commit comments