-
-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathdescribe_affected_changed_files_index.go
More file actions
235 lines (200 loc) · 9.03 KB
/
Copy pathdescribe_affected_changed_files_index.go
File metadata and controls
235 lines (200 loc) · 9.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package exec
import (
"path/filepath"
"strings"
"sync"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/schema"
)
// changedFilesIndex provides efficient lookup of changed files by base path.
// This reduces PathMatch operations from O(stacks × components × files) to O(stacks × components × relevant_files).
// Expected impact: 60-80% reduction in PathMatch calls.
type changedFilesIndex struct {
// filesByBasePath maps component base paths to changed files in that path.
filesByBasePath map[string][]string
// allFiles contains all changed files for fallback scenarios.
allFiles []string
// allFilesSet provides constant-time lookup for dependencies that may live
// outside a component base path, such as native Helm values_files.
allFilesSet map[string]struct{}
mu sync.RWMutex
}
// newChangedFilesIndex creates an index of changed files organized by base path.
// This enables efficient filtering of relevant files for each component type.
// All file paths are normalized to absolute paths to ensure consistent matching with absolute patterns.
// The gitRepoRoot parameter is the absolute path to the git repository root, used to resolve
// relative file paths from git diff (which are relative to the repo root, not the current working directory).
func newChangedFilesIndex(atmosConfig *schema.AtmosConfiguration, changedFiles []string, gitRepoRoot string) *changedFilesIndex {
defer perf.Track(atmosConfig, "exec.newChangedFilesIndex")()
index := &changedFilesIndex{
filesByBasePath: make(map[string][]string),
allFiles: nil, // Set after normalization.
allFilesSet: make(map[string]struct{}, len(changedFiles)),
}
// Pre-compute absolute base paths for each component type.
normalizedBasePaths := buildNormalizedBasePaths(atmosConfig)
// Normalize all changed files to absolute paths once.
// Changed files from git diff are relative to the git repository root, not the current working directory.
// We must resolve them relative to gitRepoRoot to ensure correct path matching.
// This fixes issue #1978 where component changes were not detected when atmos.yaml
// is in a subdirectory of the git repository.
absAllFiles := make([]string, 0, len(changedFiles))
for _, f := range changedFiles {
var absF string
switch {
case filepath.IsAbs(f):
// Already absolute, use as-is.
absF = f
case gitRepoRoot != "":
// Resolve relative path against git repo root.
absF = filepath.Join(gitRepoRoot, f)
default:
// Fallback to current working directory if no git repo root provided.
var err error
absF, err = filepath.Abs(f)
if err != nil {
absF = f
}
}
absF = filepath.Clean(absF)
absAllFiles = append(absAllFiles, absF)
index.allFilesSet[absF] = struct{}{}
}
index.allFiles = absAllFiles
// Initialize empty slices for each base path.
for _, absPath := range normalizedBasePaths {
index.filesByBasePath[absPath] = make([]string, 0)
}
// Index each absolute file by its base path.
for _, absFile := range absAllFiles {
indexChangedFile(index, absFile, normalizedBasePaths)
}
return index
}
// buildNormalizedBasePaths constructs absolute base paths for all component types.
// Only includes non-empty component base paths to avoid indexing files under the root basePath.
func buildNormalizedBasePaths(atmosConfig *schema.AtmosConfiguration) []string {
// Collect base paths, skipping empty ones to prevent root basePath collisions.
basePaths := make([]string, 0, 6)
// Add terraform base path if configured.
if atmosConfig.Components.Terraform.BasePath != "" {
basePaths = append(basePaths, filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Terraform.BasePath))
}
// Add helmfile base path if configured.
if atmosConfig.Components.Helmfile.BasePath != "" {
basePaths = append(basePaths, filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Helmfile.BasePath))
}
// Add packer base path if configured.
if atmosConfig.Components.Packer.BasePath != "" {
basePaths = append(basePaths, filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Packer.BasePath))
}
// Add kubernetes base path if configured.
if atmosConfig.Components.Kubernetes.BasePath != "" {
basePaths = append(basePaths, filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Kubernetes.BasePath))
}
// Add native Helm base path if configured.
if atmosConfig.Components.Helm.BasePath != "" {
basePaths = append(basePaths, filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Helm.BasePath))
}
// Add stacks base path if configured.
if atmosConfig.Stacks.BasePath != "" {
basePaths = append(basePaths, filepath.Join(atmosConfig.BasePath, atmosConfig.Stacks.BasePath))
}
normalizedBasePaths := make([]string, 0, len(basePaths))
for _, basePath := range basePaths {
absPath, err := filepath.Abs(basePath)
if err != nil {
// If conversion fails, use original path.
absPath = basePath
}
normalizedBasePaths = append(normalizedBasePaths, absPath)
}
return normalizedBasePaths
}
// indexChangedFile indexes a single changed file by finding its matching base path.
// The input file path must already be absolute (normalized in newChangedFilesIndex).
// Files that don't match any base path are not indexed (they may still be checked via
// module patterns or dependency paths, which are independent mechanisms).
func indexChangedFile(index *changedFilesIndex, absFile string, normalizedBasePaths []string) {
// Find which base path this file belongs to.
// Use filepath.Rel to properly check path boundaries, not just string prefixes.
// This prevents sibling paths like "components/terraform" and "components/terraform-modules"
// from colliding due to shared prefixes.
if matchedPath := findMatchingBasePath(absFile, normalizedBasePaths); matchedPath != "" {
index.filesByBasePath[matchedPath] = append(index.filesByBasePath[matchedPath], absFile)
}
// Files that don't match any base path are NOT indexed for base path checking.
// They will still be checked via:
// - Module pattern cache (if referenced as Terraform modules)
// - Dependency checking (if specified in component dependencies)
// This maintains independence between component folder checks, module checks, and dependency checks.
}
// findMatchingBasePath returns the base path that contains the given file, or empty string if none match.
func findMatchingBasePath(absFile string, normalizedBasePaths []string) string {
for _, basePath := range normalizedBasePaths {
if isFileInBasePath(absFile, basePath) {
return basePath
}
}
return ""
}
// isFileInBasePath checks if a file is within a base path using proper path boundary checking.
func isFileInBasePath(absFile, basePath string) bool {
rel, err := filepath.Rel(basePath, absFile)
if err != nil {
return false
}
// File is inside basePath if:
// - rel is "." (file is directly at basePath), OR
// - rel doesn't start with ".." (file is within basePath, not outside or in a sibling)
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
}
// getRelevantFiles returns changed files relevant to a specific component as absolute paths.
// This significantly reduces the number of PathMatch operations needed.
func (idx *changedFilesIndex) getRelevantFiles(componentType string, atmosConfig *schema.AtmosConfiguration) []string {
idx.mu.RLock()
defer idx.mu.RUnlock()
var basePath string
switch componentType {
case cfg.TerraformComponentType:
basePath = filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Terraform.BasePath)
case cfg.HelmfileComponentType:
basePath = filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Helmfile.BasePath)
case cfg.PackerComponentType:
basePath = filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Packer.BasePath)
case cfg.KubernetesComponentType:
basePath = filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Kubernetes.BasePath)
case cfg.HelmComponentType:
basePath = filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Helm.BasePath)
default:
// Unknown component type - return all files as fallback.
return idx.allFiles
}
absBasePath, err := filepath.Abs(basePath)
if err != nil {
// If conversion fails, return all files as fallback.
return idx.allFiles
}
if files, ok := idx.filesByBasePath[absBasePath]; ok {
return files
}
// If base path not found in index, return all files as fallback.
return idx.allFiles
}
// getAllFiles returns all changed files as absolute paths (for operations that need to check everything).
// All paths are normalized to absolute during index creation for consistent pattern matching.
func (idx *changedFilesIndex) getAllFiles() []string {
idx.mu.RLock()
defer idx.mu.RUnlock()
return idx.allFiles
}
// isChangedFile reports whether the normalized absolute path is in the git
// change set. It is used for component dependencies that can be located
// outside the component's indexed base path.
func (idx *changedFilesIndex) isChangedFile(path string) bool {
idx.mu.RLock()
defer idx.mu.RUnlock()
_, ok := idx.allFilesSet[filepath.Clean(path)]
return ok
}