@@ -766,6 +766,77 @@ type repoRef struct {
766766 commitSHA string
767767 // key is the name of the key which was used to reference this repo.
768768 key string
769+ // worktreePath is the path to a git worktree if this ref required a separate checkout.
770+ // Empty if using the main repository checkout.
771+ worktreePath string
772+ }
773+
774+ // worktreeCleanup holds references needed to clean up a git worktree.
775+ type worktreeCleanup struct {
776+ client git.Client
777+ worktreePath string
778+ repoURL string
779+ commitSHA string
780+ gitRepoPaths utilio.TempPaths
781+ }
782+
783+ // cleanup removes the worktree and cleans up the path registration.
784+ func (w * worktreeCleanup ) cleanup () {
785+ if err := w .client .RemoveWorktree (w .worktreePath ); err != nil {
786+ log .Warnf ("Failed to remove worktree at %s: %v" , w .worktreePath , err )
787+ }
788+ // Remove from gitRepoPaths
789+ w .gitRepoPaths .Remove (w .repoURL + "@" + w .commitSHA )
790+ }
791+
792+ // createAndRegisterWorktree creates a git worktree at a temporary path for the given revision,
793+ // registers it in gitRepoPaths, and returns the path along with a cleanup function.
794+ // The caller should defer the cleanup function.
795+ func (s * Service ) createAndRegisterWorktree (
796+ gitClient git.Client ,
797+ normalizedRepoURL string ,
798+ commitSHA string ,
799+ ) (string , * worktreeCleanup , error ) {
800+ worktreePath := filepath .Join (os .TempDir (), "argocd-worktree-" + uuid .New ().String ())
801+
802+ if err := gitClient .CreateWorktree (commitSHA , worktreePath ); err != nil {
803+ return "" , nil , fmt .Errorf ("failed to create worktree at revision %s: %w" , commitSHA , err )
804+ }
805+
806+ // Register the worktree path so getResolvedRefValueFile can find it
807+ s .gitRepoPaths .Add (normalizedRepoURL + "@" + commitSHA , worktreePath )
808+
809+ cleanup := & worktreeCleanup {
810+ client : gitClient ,
811+ worktreePath : worktreePath ,
812+ repoURL : normalizedRepoURL ,
813+ commitSHA : commitSHA ,
814+ gitRepoPaths : s .gitRepoPaths ,
815+ }
816+
817+ return worktreePath , cleanup , nil
818+ }
819+
820+ // checkWorktreeSymlinks checks for out-of-bounds symlinks in a worktree path.
821+ func (s * Service ) checkWorktreeSymlinks (worktreePath string , repo v1alpha1.Repository , revision string ) error {
822+ if s .initConstants .AllowOutOfBoundsSymlinks {
823+ return nil
824+ }
825+ err := apppathutil .CheckOutOfBoundsSymlinks (worktreePath )
826+ if err != nil {
827+ oobError := & apppathutil.OutOfBoundsSymlinkError {}
828+ if errors .As (err , & oobError ) {
829+ log .WithFields (log.Fields {
830+ common .SecurityField : common .SecurityHigh ,
831+ "repo" : repo ,
832+ "revision" : revision ,
833+ "file" : oobError .File ,
834+ }).Warn ("repository worktree contains out-of-bounds symlink" )
835+ return fmt .Errorf ("repository contains out-of-bounds symlinks. file: %s" , oobError .File )
836+ }
837+ return err
838+ }
839+ return nil
769840}
770841
771842func (s * Service ) runManifestGenAsync (ctx context.Context , repoRoot , commitSHA , cacheKey string , opContextSrc operationContextSrc , q * apiclient.ManifestRequest , ch * generateManifestCh ) {
@@ -816,24 +887,47 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
816887 return
817888 }
818889 normalizedRepoURL := git .NormalizeGitURL (refSourceMapping .Repo .Repo )
819- closer , ok := repoRefs [normalizedRepoURL ]
820- if ok {
821- if closer .revision != refSourceMapping .TargetRevision {
822- ch .errCh <- fmt .Errorf ("cannot reference multiple revisions for the same repository (%s references %q while %s references %q)" , refVar , refSourceMapping .TargetRevision , closer .key , closer .revision )
823- return
824- }
825- } else {
826- gitClient , referencedCommitSHA , err := s .newClientResolveRevision (& refSourceMapping .Repo , refSourceMapping .TargetRevision , git .WithCache (s .cache , ! q .NoRevisionCache && ! q .NoCache ))
890+ existingRef , ok := repoRefs [normalizedRepoURL ]
891+ if ok && existingRef .revision == refSourceMapping .TargetRevision {
892+ // Same repo and same revision already referenced - reuse existing checkout
893+ continue
894+ }
895+
896+ // Resolve the git client and commit SHA for this ref source
897+ gitClient , referencedCommitSHA , err := s .newClientResolveRevision (& refSourceMapping .Repo , refSourceMapping .TargetRevision , git .WithCache (s .cache , ! q .NoRevisionCache && ! q .NoCache ))
898+ if err != nil {
899+ ch .errCh <- fmt .Errorf ("failed to get git client for repo %s: %w" , refSourceMapping .Repo .Repo , err )
900+ return
901+ }
902+
903+ // Determine if we need a worktree (same repo at different revision)
904+ needsWorktree := ok || // Already have a ref to this repo at a different revision
905+ (git .NormalizeGitURL (q .ApplicationSource .RepoURL ) == normalizedRepoURL && commitSHA != referencedCommitSHA ) // Same as primary source at different revision
906+
907+ if needsWorktree {
908+ // Create a worktree for this different revision
909+ worktreePath , cleanup , err := s .createAndRegisterWorktree (gitClient , normalizedRepoURL , referencedCommitSHA )
827910 if err != nil {
828- log .Errorf ("Failed to get git client for repo %s: %v" , refSourceMapping .Repo .Repo , err )
829- ch .errCh <- fmt .Errorf ("failed to get git client for repo %s" , refSourceMapping .Repo .Repo )
911+ ch .errCh <- fmt .Errorf ("failed to create worktree for repo %s: %w" , refSourceMapping .Repo .Repo , err )
830912 return
831913 }
914+ defer cleanup .cleanup ()
832915
833- if git .NormalizeGitURL (q .ApplicationSource .RepoURL ) == normalizedRepoURL && commitSHA != referencedCommitSHA {
834- ch .errCh <- fmt .Errorf ("cannot reference a different revision of the same repository (%s references %q which resolves to %q while the application references %q which resolves to %q)" , refVar , refSourceMapping .TargetRevision , referencedCommitSHA , q .Revision , commitSHA )
916+ // Symlink check for the worktree
917+ if err := s .checkWorktreeSymlinks (worktreePath , refSourceMapping .Repo , refSourceMapping .TargetRevision ); err != nil {
918+ ch .errCh <- err
835919 return
836920 }
921+
922+ // Use a unique key that includes the commit SHA for worktree refs
923+ repoRefs [normalizedRepoURL + "@" + referencedCommitSHA ] = repoRef {
924+ revision : refSourceMapping .TargetRevision ,
925+ commitSHA : referencedCommitSHA ,
926+ key : refVar ,
927+ worktreePath : worktreePath ,
928+ }
929+ } else {
930+ // Different repo - use normal checkout flow
837931 closer , err := s .repoLock .Lock (gitClient .Root (), referencedCommitSHA , true , func () (goio.Closer , error ) {
838932 return s .checkoutRevision (gitClient , referencedCommitSHA , s .initConstants .SubmoduleEnabled , q .Repo .Depth )
839933 })
@@ -850,23 +944,9 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
850944 }(closer )
851945
852946 // Symlink check must happen after acquiring lock.
853- if ! s .initConstants .AllowOutOfBoundsSymlinks {
854- err := apppathutil .CheckOutOfBoundsSymlinks (gitClient .Root ())
855- if err != nil {
856- oobError := & apppathutil.OutOfBoundsSymlinkError {}
857- if errors .As (err , & oobError ) {
858- log .WithFields (log.Fields {
859- common .SecurityField : common .SecurityHigh ,
860- "repo" : refSourceMapping .Repo ,
861- "revision" : refSourceMapping .TargetRevision ,
862- "file" : oobError .File ,
863- }).Warn ("repository contains out-of-bounds symlink" )
864- ch .errCh <- fmt .Errorf ("repository contains out-of-bounds symlinks. file: %s" , oobError .File )
865- return
866- }
867- ch .errCh <- err
868- return
869- }
947+ if err := s .checkWorktreeSymlinks (gitClient .Root (), refSourceMapping .Repo , refSourceMapping .TargetRevision ); err != nil {
948+ ch .errCh <- err
949+ return
870950 }
871951
872952 repoRefs [normalizedRepoURL ] = repoRef {revision : refSourceMapping .TargetRevision , commitSHA : referencedCommitSHA , key : refVar }
@@ -1414,7 +1494,23 @@ func getResolvedRefValueFile(
14141494 gitRepoPaths utilio.TempPaths ,
14151495) (pathutil.ResolvedFilePath , error ) {
14161496 pathStrings := strings .Split (rawValueFile , "/" )
1417- repoPath := gitRepoPaths .GetPathIfExists (git .NormalizeGitURL (refSourceRepo ))
1497+ normalizedRepoURL := git .NormalizeGitURL (refSourceRepo )
1498+
1499+ // First, try the standard repo path (for same-revision or non-worktree cases)
1500+ repoPath := gitRepoPaths .GetPathIfExists (normalizedRepoURL )
1501+
1502+ // If not found, check for worktree paths (keyed as "repoURL@commitSHA")
1503+ if repoPath == "" {
1504+ allPaths := gitRepoPaths .GetPaths ()
1505+ for key , path := range allPaths {
1506+ // Check if this key starts with our repo URL followed by "@" (worktree pattern)
1507+ if strings .HasPrefix (key , normalizedRepoURL + "@" ) {
1508+ repoPath = path
1509+ break
1510+ }
1511+ }
1512+ }
1513+
14181514 if repoPath == "" {
14191515 return "" , fmt .Errorf ("failed to find repo %q" , refSourceRepo )
14201516 }
0 commit comments