@@ -538,7 +538,7 @@ func DeployStack(
538538 }
539539 }
540540 } else {
541- detectedChanges , err := ProjectFilesHaveChanges (changedFiles , project )
541+ detectedChanges , err := ProjectFilesHaveChanges (externalRepoPath , changedFiles , project )
542542 if err != nil {
543543 errMsg := "failed to check for changed project files"
544544 return fmt .Errorf ("%s: %w" , errMsg , err )
@@ -642,39 +642,54 @@ func getPaths(changedFiles []gitInternal.ChangedFile, basePath string) []string
642642 p = filepath .Join (basePath , p )
643643 }
644644
645- if ! slices .Contains (absPaths , p ) {
646- absPaths = append (absPaths , p )
647- }
645+ absPaths = append (absPaths , p )
648646 }
649647 }
650648
651- return absPaths
649+ return slice .Unique (absPaths )
650+ }
651+
652+ // checkPathAffected checks if a changed file is affected by a used file.
653+ func checkPathAffected (changed string , used string ) bool {
654+ used = filepath .Clean (used )
655+ changed = filepath .Clean (changed )
656+
657+ rel , err := filepath .Rel (used , changed )
658+ if err != nil {
659+ // It' share same reporoot, so it should not happen
660+ slog .Debug ("checkPathAffected " ,
661+ slog .String ("used" , used ),
662+ slog .String ("changed" , changed ),
663+ slog .Any ("error" , err ),
664+ )
665+
666+ return false
667+ }
668+
669+ return ! strings .HasPrefix (rel , ".." )
652670}
653671
654672// HasChangedConfigs checks if any files used in docker compose `configs:` definitions have changed using the Git status.
655- func HasChangedConfigs (changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]string , error ) {
656- // We only need the relative part in this case
657- paths := getPaths (changedFiles , "." )
673+ func HasChangedConfigs (paths []string , project * types.Project ) ([]string , error ) {
674+ configToServicesMap := make (map [string ][]string )
675+
676+ for name , s := range project .Services {
677+ for _ , cfg := range s .Configs {
678+ configToServicesMap [cfg .Source ] = append (configToServicesMap [cfg .Source ], name )
679+ }
680+ }
658681
659682 var changedServices []string
660683
661- for _ , c := range project .Configs {
684+ for name , c := range project .Configs {
662685 // Changes in config.Content are handled in project hash comparison
663686 if c .File == "" {
664687 continue
665688 }
666689
667690 for _ , p := range paths {
668- if strings .HasSuffix (c .File , p ) {
669- // Find services that use if the changed config
670- for _ , s := range project .Services {
671- for _ , cfg := range s .Configs {
672- if strings .HasSuffix (c .File , cfg .Source ) {
673- changedServices = append (changedServices , s .Name )
674- break
675- }
676- }
677- }
691+ if checkPathAffected (p , c .File ) {
692+ changedServices = append (changedServices , configToServicesMap [name ]... )
678693 }
679694 }
680695 }
@@ -683,28 +698,25 @@ func HasChangedConfigs(changedFiles []gitInternal.ChangedFile, project *types.Pr
683698}
684699
685700// HasChangedSecrets checks if any files used in docker compose `secrets:` definitions have changed using the Git status.
686- func HasChangedSecrets (changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]string , error ) {
687- // We only need the relative part in this case
688- paths := getPaths (changedFiles , "." )
701+ func HasChangedSecrets (paths []string , project * types.Project ) ([]string , error ) {
702+ secretsToServicesMap := make (map [string ][]string )
703+
704+ for name , s := range project .Services {
705+ for _ , secret := range s .Secrets {
706+ secretsToServicesMap [secret .Source ] = append (secretsToServicesMap [secret .Source ], name )
707+ }
708+ }
689709
690710 var changedServices []string
691711
692- for _ , s := range project .Secrets {
712+ for name , s := range project .Secrets {
693713 if s .File == "" {
694714 continue
695715 }
696716
697717 for _ , p := range paths {
698- if strings .HasSuffix (s .File , p ) {
699- // Find services that use if the changed config
700- for _ , svc := range project .Services {
701- for _ , secret := range svc .Secrets {
702- if strings .HasSuffix (s .File , secret .Source ) {
703- changedServices = append (changedServices , svc .Name )
704- break
705- }
706- }
707- }
718+ if checkPathAffected (p , s .File ) {
719+ changedServices = append (changedServices , secretsToServicesMap [name ]... )
708720 }
709721 }
710722 }
@@ -713,24 +725,17 @@ func HasChangedSecrets(changedFiles []gitInternal.ChangedFile, project *types.Pr
713725}
714726
715727// HasChangedBindMounts checks if any files used in docker compose `volumes:` definitions with type `bind` have changed using the Git status.
716- func HasChangedBindMounts (changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]string , error ) {
717- paths := getPaths (changedFiles , "." )
718-
728+ func HasChangedBindMounts (paths []string , project * types.Project ) ([]string , error ) {
719729 var changedServices []string
720730
721731 for _ , s := range project .Services {
732+ out:
722733 for _ , v := range s .Volumes {
723734 if v .Type == "bind" && v .Source != "" {
724- bindSourceAbs := v .Source
725-
726- if filesInPath (paths , bindSourceAbs ) {
727- for _ , svc := range project .Services {
728- for _ , vol := range svc .Volumes {
729- if vol .Type == "bind" && vol .Source == v .Source {
730- changedServices = append (changedServices , svc .Name )
731- break
732- }
733- }
735+ for _ , p := range paths {
736+ if checkPathAffected (p , v .Source ) {
737+ changedServices = append (changedServices , s .Name )
738+ break out
734739 }
735740 }
736741 }
@@ -741,25 +746,16 @@ func HasChangedBindMounts(changedFiles []gitInternal.ChangedFile, project *types
741746}
742747
743748// HasChangedEnvFiles checks if any files used in docker compose `env_file:` definitions have changed using the Git status.
744- func HasChangedEnvFiles (changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]string , error ) {
745- // We only need the relative part in this case
746- paths := getPaths (changedFiles , "." )
747-
749+ func HasChangedEnvFiles (paths []string , project * types.Project ) ([]string , error ) {
748750 var changedServices []string
749751
750752 for _ , s := range project .Services {
753+ out:
751754 for _ , envFile := range s .EnvFiles {
752755 for _ , p := range paths {
753- if strings .HasSuffix (envFile .Path , p ) {
754- // Find services that use if the changed env file
755- for _ , svc := range project .Services {
756- for _ , ef := range svc .EnvFiles {
757- if strings .HasSuffix (envFile .Path , ef .Path ) {
758- changedServices = append (changedServices , svc .Name )
759- break
760- }
761- }
762- }
756+ if checkPathAffected (p , envFile .Path ) {
757+ changedServices = append (changedServices , s .Name )
758+ break out
763759 }
764760 }
765761 }
@@ -770,9 +766,7 @@ func HasChangedEnvFiles(changedFiles []gitInternal.ChangedFile, project *types.P
770766
771767// HasChangedBuildFiles checks if any files used as build context in docker compose `build:` definitions have changed using the Git status.
772768// This includes any file within the build context directory for each service. If a changed file is within a build context, it returns true.
773- func HasChangedBuildFiles (changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]string , error ) {
774- paths := getPaths (changedFiles , "." )
775-
769+ func HasChangedBuildFiles (paths []string , project * types.Project ) ([]string , error ) {
776770 var changedServices []string
777771
778772 for _ , s := range project .Services {
@@ -811,15 +805,17 @@ func HasChangedBuildFiles(changedFiles []gitInternal.ChangedFile, project *types
811805 contexts = append (contexts , dockerFile )
812806 }
813807
808+ out:
809+
814810 for _ , ctxFile := range contexts {
815811 if ! path .IsAbs (ctxFile ) {
816812 ctxFile = filepath .Join (project .WorkingDir , ctxFile )
817813 }
818814
819815 for _ , p := range paths {
820- if strings . HasSuffix ( ctxFile , p ) {
816+ if checkPathAffected ( p , ctxFile ) {
821817 changedServices = append (changedServices , s .Name )
822- break
818+ break out
823819 }
824820 }
825821 }
@@ -845,10 +841,10 @@ func sortChanges(changes []Change) {
845841}
846842
847843// ProjectFilesHaveChanges checks if any files related to the compose project have changed.
848- func ProjectFilesHaveChanges (changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]Change , error ) {
844+ func ProjectFilesHaveChanges (repoRootExternal string , changedFiles []gitInternal.ChangedFile , project * types.Project ) ([]Change , error ) {
849845 checks := []struct {
850846 name string
851- fn func ([]gitInternal. ChangedFile , * types.Project ) ([]string , error )
847+ fn func ([]string , * types.Project ) ([]string , error )
852848 }{
853849 {"configs" , HasChangedConfigs },
854850 {"secrets" , HasChangedSecrets },
@@ -857,10 +853,12 @@ func ProjectFilesHaveChanges(changedFiles []gitInternal.ChangedFile, project *ty
857853 {"buildFiles" , HasChangedBuildFiles },
858854 }
859855
856+ paths := getPaths (changedFiles , repoRootExternal )
857+
860858 var changes []Change
861859
862860 for _ , check := range checks {
863- changedServices , err := check .fn (changedFiles , project )
861+ changedServices , err := check .fn (paths , project )
864862 if err != nil {
865863 return nil , fmt .Errorf ("failed to check '%s' for changes: %w" , check .name , err )
866864 }
@@ -1074,58 +1072,6 @@ func CheckDefaultComposeFiles(composeFiles []string, workingDir string) ([]strin
10741072 return composeFiles , nil
10751073}
10761074
1077- // filesInPath checks if at least one of the given filePaths is inside the searchPath.
1078- func filesInPath (filePaths []string , searchPath string ) bool {
1079- for _ , p := range filePaths {
1080- if strings .HasSuffix (p , filepath .Base (searchPath )) {
1081- p = joinPathsWithoutDuplicates (searchPath , p )
1082- }
1083-
1084- rel , err := filepath .Rel (searchPath , p )
1085- if err != nil {
1086- continue
1087- }
1088-
1089- if ! strings .HasPrefix (rel , ".." ) {
1090- return true
1091- }
1092- }
1093-
1094- return false
1095- }
1096-
1097- // joinPathsWithoutDuplicates joins multiple paths, removing duplicate overlapping segments between each pair.
1098- func joinPathsWithoutDuplicates (paths ... string ) string {
1099- if len (paths ) == 0 {
1100- return ""
1101- }
1102-
1103- isAbs := filepath .IsAbs (paths [0 ])
1104-
1105- resultElems := strings .Split (filepath .Clean (paths [0 ]), string (filepath .Separator ))
1106- for _ , next := range paths [1 :] {
1107- nextElems := strings .Split (filepath .Clean (next ), string (filepath .Separator ))
1108- maxOverlap := 0
1109-
1110- m := min (len (resultElems ), len (nextElems ))
1111- for k := m ; k > 0 ; k -- {
1112- if reflect .DeepEqual (resultElems [len (resultElems )- k :], nextElems [:k ]) {
1113- maxOverlap = k
1114- break
1115- }
1116- }
1117-
1118- resultElems = append (resultElems , nextElems [maxOverlap :]... )
1119- }
1120-
1121- joined := filepath .Join (resultElems ... )
1122- if isAbs && ! strings .HasPrefix (joined , string (filepath .Separator )) {
1123- joined = string (filepath .Separator ) + joined
1124- }
1125-
1126- return joined
1127- }
1128-
11291075// DecryptProjectFiles decrypts all files used in the compose project that are encrypted using doco-cd's encryption mechanism.
11301076// This includes configs, secrets, bind mounts, env files and build contexts.
11311077// Since absolute file paths in types.Project are paths on the docker host, repoPath also needs to be the external path to the repository.
0 commit comments