@@ -159,35 +159,29 @@ func (c *CAS) SynthStore() *Store { return c.synthStore }
159159//
160160// TODO: Make options optional
161161func (c * CAS ) Clone (ctx context.Context , l log.Logger , opts * CloneOptions , url string ) error {
162- // Ensure the store paths exist
163- if err := c .fs .MkdirAll (c .blobStore .Path (), DefaultDirPerms ); err != nil {
164- return fmt .Errorf ("failed to create blob store path: %w" , err )
165- }
166-
167- if err := c .fs .MkdirAll (c .treeStore .Path (), DefaultDirPerms ); err != nil {
168- return fmt .Errorf ("failed to create tree store path: %w" , err )
162+ if err := c .ensureCloneStores (); err != nil {
163+ return err
169164 }
170165
171166 return telemetry .TelemeterFromContext (ctx ).Collect (ctx , "cas_clone" , map [string ]any {
172167 "url" : url ,
173168 "branch" : opts .Branch ,
174169 }, func (childCtx context.Context ) error {
175- hash , err := c .resolveReference (childCtx , url , opts .Branch )
170+ ref , err := c .resolveReference (childCtx , url , opts .Branch )
176171 if err != nil {
177172 return err
178173 }
179174
180175 targetDir := c .prepareTargetDirectory (opts .Dir , url )
181176
182- if c .treeStore .NeedsWrite (hash ) {
183- if err := c .populateTreeFromGit (childCtx , l , opts , url , hash ); err != nil {
184- return err
185- }
177+ canonicalHash , err := c .populateTreeFromRef (childCtx , l , opts , ref )
178+ if err != nil {
179+ return err
186180 }
187181
188182 treeContent := NewContent (c .treeStore )
189183
190- treeData , err := treeContent .Read (hash )
184+ treeData , err := treeContent .Read (canonicalHash )
191185 if err != nil {
192186 return err
193187 }
@@ -201,46 +195,172 @@ func (c *CAS) Clone(ctx context.Context, l log.Logger, opts *CloneOptions, url s
201195 })
202196}
203197
204- // populateTreeFromGit fetches the commit at hash and stores its tree and
205- // reachable blobs in the CAS. It tries the central git store first. Any
206- // error from the central store is logged as a warning, and the function
207- // then falls back to a bare clone in a temporary directory.
208- func (c * CAS ) populateTreeFromGit (ctx context.Context , l log.Logger , opts * CloneOptions , url , hash string ) error {
198+ // ensureCloneStores creates the blob and tree store directories that
199+ // [CAS.Clone] writes to. Defensive: [New] already creates them, but a
200+ // long-lived [CAS] instance could see them removed between calls.
201+ func (c * CAS ) ensureCloneStores () error {
202+ for _ , s := range []* Store {c .blobStore , c .treeStore } {
203+ if err := c .fs .MkdirAll (s .Path (), DefaultDirPerms ); err != nil {
204+ return fmt .Errorf ("create CAS store path %s: %w" , s .Path (), err )
205+ }
206+ }
207+
208+ return nil
209+ }
210+
211+ // populateTreeFromRef dispatches by ref kind, short-circuiting on a
212+ // cached [symbolicRef] and otherwise calling the kind-specific
213+ // populate. Returns the canonical commit hash.
214+ func (c * CAS ) populateTreeFromRef (
215+ ctx context.Context ,
216+ l log.Logger ,
217+ opts * CloneOptions ,
218+ ref resolvedRef ,
219+ ) (string , error ) {
220+ switch ref := ref .(type ) {
221+ case * symbolicRef :
222+ if ! c .treeStore .NeedsWrite (ref .Hash ) {
223+ return ref .Hash , nil
224+ }
225+
226+ if err := c .populateTreeFromSymbolicRef (ctx , l , opts , ref ); err != nil {
227+ return "" , err
228+ }
229+
230+ return ref .Hash , nil
231+
232+ case * commitRef :
233+ return c .populateTreeFromCommitRef (ctx , l , opts , ref )
234+
235+ default :
236+ return "" , fmt .Errorf ("unsupported resolved ref type %T" , ref )
237+ }
238+ }
239+
240+ // populateTreeFromSymbolicRef stores the tree and reachable blobs
241+ // for ref.Hash in the CAS. Tries the central [GitStore] first; on
242+ // any error from it, logs a warning and falls back to a bare clone
243+ // in a temporary directory.
244+ func (c * CAS ) populateTreeFromSymbolicRef (
245+ ctx context.Context ,
246+ l log.Logger ,
247+ opts * CloneOptions ,
248+ ref * symbolicRef ,
249+ ) error {
209250 depth := resolveCloneDepth (opts .Depth , c .cloneDepth )
210251
211- repoPath , unlocker , err := c .gitStore .EnsureRef (ctx , l , c .fs , url , opts .Branch , hash , depth )
252+ repo , err := c .gitStore .EnsureRef (ctx , l , c .fs , ref .URL , ref .Branch , ref .Hash , depth )
253+ if err == nil {
254+ defer repo .Release (l )
255+
256+ runner := c .git .WithWorkDir (repo .Path )
257+
258+ return c .storeRootTreeFrom (ctx , l , runner , ref .Hash , opts )
259+ }
260+
261+ l .Warnf ("central git store unavailable for %s, falling back to temporary clone: %v" , ref .URL , err )
262+
263+ tempDir , cleanup , err := c .makeFallbackCloneDir (l )
264+ if err != nil {
265+ return err
266+ }
267+
268+ defer cleanup ()
269+
270+ runner := c .git .WithWorkDir (tempDir )
271+
272+ if err := runner .Clone (ctx , ref .URL , true , depth , ref .Branch ); err != nil {
273+ return err
274+ }
275+
276+ return c .storeRootTreeFrom (ctx , l , runner , ref .Hash , opts )
277+ }
278+
279+ // populateTreeFromCommitRef resolves ref via [GitStore.EnsureCommit]
280+ // (full-depth fetch on a cache miss) and stores its tree in the CAS.
281+ // Returns the canonical commit hash. Falls back to a temporary bare
282+ // clone if the central [GitStore] is unavailable.
283+ func (c * CAS ) populateTreeFromCommitRef (
284+ ctx context.Context ,
285+ l log.Logger ,
286+ opts * CloneOptions ,
287+ ref * commitRef ,
288+ ) (string , error ) {
289+ repo , err := c .gitStore .EnsureCommit (ctx , l , c .fs , ref .URL , ref .RawRef )
212290 if err == nil {
213- defer func () {
214- if unlockErr := unlocker .Unlock (); unlockErr != nil {
215- l .Warnf ("git store: failed to release lock for %s: %v" , url , unlockErr )
291+ defer repo .Release (l )
292+
293+ if ! c .treeStore .NeedsWrite (repo .Hash ) {
294+ return repo .Hash , nil
295+ }
296+
297+ runner := c .git .WithWorkDir (repo .Path )
298+
299+ if err := c .storeRootTreeFrom (ctx , l , runner , repo .Hash , opts ); err != nil {
300+ return "" , err
301+ }
302+
303+ return repo .Hash , nil
304+ }
305+
306+ if errors .Is (err , git .ErrNoMatchingReference ) {
307+ return "" , err
308+ }
309+
310+ l .Warnf ("central git store unavailable for %s, falling back to temporary clone: %v" , ref .URL , err )
311+
312+ tempDir , cleanup , err := c .makeFallbackCloneDir (l )
313+ if err != nil {
314+ return "" , err
315+ }
316+
317+ defer cleanup ()
318+
319+ runner := c .git .WithWorkDir (tempDir )
320+
321+ if err := runner .Clone (ctx , ref .URL , true , 0 , "" ); err != nil {
322+ return "" , err
323+ }
324+
325+ canonicalHash , err := runner .RevParseCommit (ctx , ref .RawRef )
326+ if err != nil {
327+ if errors .Is (err , git .ErrUnknownRevision ) {
328+ return "" , & git.WrappedError {
329+ Op : "git_clone_resolve" ,
330+ Context : fmt .Sprintf ("%q in %s" , ref .RawRef , ref .URL ),
331+ Err : git .ErrNoMatchingReference ,
216332 }
217- }()
333+ }
218334
219- runner := c .git .WithWorkDir (repoPath )
335+ return "" , err
336+ }
337+
338+ if ! c .treeStore .NeedsWrite (canonicalHash ) {
339+ return canonicalHash , nil
340+ }
220341
221- return c .storeRootTreeFrom (ctx , l , runner , hash , opts )
342+ if err := c .storeRootTreeFrom (ctx , l , runner , canonicalHash , opts ); err != nil {
343+ return "" , err
222344 }
223345
224- l .Warnf ("central git store unavailable for %s, falling back to temporary clone: %v" , url , err )
346+ return canonicalHash , nil
347+ }
225348
349+ // makeFallbackCloneDir creates a temporary directory for a bare clone
350+ // fallback and returns a cleanup function that removes it.
351+ func (c * CAS ) makeFallbackCloneDir (l log.Logger ) (string , func (), error ) {
226352 tempDir , err := vfs .MkdirTemp (c .fs , "" , "terragrunt-cas-fallback-*" )
227353 if err != nil {
228- return fmt .Errorf ("create fallback clone dir: %w" , errors .Join (ErrFallbackCloneDir , err ))
354+ return "" , nil , fmt .Errorf ("create fallback clone dir: %w" , errors .Join (ErrFallbackCloneDir , err ))
229355 }
230356
231- defer func () {
357+ cleanup := func () {
232358 if rmErr := c .fs .RemoveAll (tempDir ); rmErr != nil {
233359 l .Warnf ("cleanup error: %v" , rmErr )
234360 }
235- }()
236-
237- runner := c .git .WithWorkDir (tempDir )
238-
239- if err := runner .Clone (ctx , url , true , depth , opts .Branch ); err != nil {
240- return err
241361 }
242362
243- return c . storeRootTreeFrom ( ctx , l , runner , hash , opts )
363+ return tempDir , cleanup , nil
244364}
245365
246366func resolveCloneDepth (optDepth , casDepth int ) int {
@@ -269,21 +389,59 @@ func (c *CAS) prepareTargetDirectory(dir, url string) string {
269389 return filepath .Clean (targetDir )
270390}
271391
272- func (c * CAS ) resolveReference (ctx context.Context , url , branch string ) (string , error ) {
392+ // resolvedRef is what [CAS.resolveReference] returns: a [symbolicRef]
393+ // when ls-remote resolved the input to a branch, tag, or HEAD; a
394+ // [commitRef] when it did not. Sealed by package visibility.
395+ type resolvedRef interface {
396+ // CommitHash returns the canonical commit hash for [symbolicRef]
397+ // and the user-supplied SHA for [commitRef]. Abbreviated SHAs
398+ // passed in as commit refs remain abbreviated.
399+ CommitHash () string
400+ }
401+
402+ // symbolicRef carries an ls-remote-resolved branch, tag, or HEAD.
403+ type symbolicRef struct {
404+ URL string
405+ Branch string // ref name, used for the per-ref fetch
406+ Hash string // canonical commit hash
407+ }
408+
409+ // CommitHash returns the canonical commit hash ls-remote resolved.
410+ func (r * symbolicRef ) CommitHash () string { return r .Hash }
411+
412+ // commitRef carries a commit-form ref ls-remote did not recognize.
413+ // Resolution against the central git store happens later via
414+ // rev-parse, with a full-depth fetch on a cache miss.
415+ type commitRef struct {
416+ URL string
417+ RawRef string // user-supplied SHA, full or abbreviated
418+ }
419+
420+ // CommitHash returns the user-supplied SHA before central-store
421+ // canonicalization.
422+ func (r * commitRef ) CommitHash () string { return r .RawRef }
423+
424+ // resolveReference resolves branch into a [resolvedRef]. ls-remote is
425+ // the authoritative source for symbolic refs (branches, tags, HEAD);
426+ // a hit returns a [*symbolicRef] carrying the canonical hash. A miss
427+ // returns a [*commitRef] so the caller resolves the input locally
428+ // against the central git store, where commit-form SHAs (full or
429+ // abbreviated) are looked up via rev-parse and a full-history fetch.
430+ func (c * CAS ) resolveReference (ctx context.Context , url , branch string ) (resolvedRef , error ) {
273431 results , err := c .git .LsRemote (ctx , url , branch )
274432 if err != nil {
275- return "" , err
433+ if errors .Is (err , git .ErrNoMatchingReference ) {
434+ return & commitRef {URL : url , RawRef : branch }, nil
435+ }
436+
437+ return nil , err
276438 }
277439
278440 if len (results ) == 0 {
279- return "" , & WrappedError {
280- Op : "clone" ,
281- Context : "no matching reference" ,
282- Err : ErrNoMatchingReference ,
283- }
441+ return & commitRef {URL : url , RawRef : branch }, nil
284442 }
285443
286- return results [0 ].Hash , nil
444+ return & symbolicRef { URL : url , Branch : branch , Hash : results [0 ].Hash } , nil
287445}
288446
289447// storeRootTreeFrom reads the recursive tree at hash from the supplied
0 commit comments