···1919 return "", err
2020 }
21212222- // Clone the repository
2322 cmd := exec.CommandContext(ctx, "git", "clone", "--branch", revision, url, path)
2423 if err := cmd.Run(); err != nil {
2524 return "", err
···3231 logger := activity.GetLogger(ctx)
3332 logger.Info("Getting changed files", "path", path, "oldRevision", oldRevision)
34333535- // Get changed files using git diff --name-only
3634 cmd := exec.CommandContext(ctx, "git", "diff", "--name-only", oldRevision, "HEAD")
3735 cmd.Dir = path
3836 output, err := cmd.Output()
···4038 return nil, err
4139 }
42404343- // Split output by newlines and filter empty lines
4441 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
4542 var files []string
4643 for _, line := range lines {
···5754 logger := activity.GetLogger(ctx)
5855 logger.Info("Getting changed modules", "path", repoPath, "oldRevision", oldRevision)
59566060- // Get all changed files
6157 changedFiles, err := changedFiles(ctx, repoPath, oldRevision)
6258 if err != nil {
6359 return nil, err
6460 }
65616662 seen := make(map[string]struct{})
6767- modules := make([]string, 0)
6363+ var modules []string
68646965 for _, file := range changedFiles {
7070- // Get the directory of the changed file
7166 dir := filepath.Dir(file)
72677373- // Walk up the directory tree to find the closest directory containing terragrunt.hcl
7468 currentDir := dir
7569 for {
7670 terragruntPath := filepath.Join(repoPath, currentDir, "terragrunt.hcl")
7771 if _, err := os.Stat(terragruntPath); err == nil {
7878- // Found terragrunt.hcl, this is a module directory
7972 modulePath := currentDir
80738181- // Remove infra/<env>/ prefix if present
8274 if strings.HasPrefix(modulePath, "infra/") {
8375 parts := strings.Split(filepath.ToSlash(modulePath), "/")
8476 if len(parts) >= 3 && parts[0] == "infra" {
8585- // Remove "infra" and environment (e.g., "dev", "prod")
8677 modulePath = strings.Join(parts[2:], "/")
8778 }
8879 }
89809090- // Skip empty paths
9181 if modulePath != "" && modulePath != "." {
9292- // Normalize path separators to forward slashes
9382 modulePath = filepath.ToSlash(modulePath)
94839584 if _, exists := seen[modulePath]; !exists {
···10089 break
10190 }
10291103103- // Move up one directory level
10492 parent := filepath.Dir(currentDir)
10593 if parent == currentDir || parent == "." {
106106- // Reached the root, no terragrunt.hcl found
10794 break
10895 }
10996 currentDir = parent
+12-25
controller/activities/graph.go
···155155 var b strings.Builder
156156 b.WriteString("digraph {\n")
157157158158- // Write edges first (they implicitly declare nodes)
159158 for src, dests := range g.Edges {
160159 for _, dest := range dests {
161160 b.WriteString(fmt.Sprintf(" %q -> %q;\n", src, dest))
162161 }
163162 }
164163165165- // Write standalone nodes (nodes without edges)
164164+ // Write standalone nodes (those not in any edge)
165165+ edgeNodes := make(map[string]bool)
166166+ for src, dests := range g.Edges {
167167+ edgeNodes[src] = true
168168+ for _, dest := range dests {
169169+ edgeNodes[dest] = true
170170+ }
171171+ }
172172+166173 for node := range g.Nodes {
167167- hasEdge := false
168168- // Check if node appears in any edge
169169- if _, exists := g.Edges[node]; exists {
170170- hasEdge = true
171171- }
172172- if !hasEdge {
173173- for _, dests := range g.Edges {
174174- for _, dest := range dests {
175175- if dest == node {
176176- hasEdge = true
177177- break
178178- }
179179- }
180180- if hasEdge {
181181- break
182182- }
183183- }
184184- }
185185- if !hasEdge {
174174+ if !edgeNodes[node] {
186175 b.WriteString(fmt.Sprintf(" %q;\n", node))
187176 }
188177 }
···191180 return b.String()
192181}
193182194194-// TopologicalSort returns modules grouped by dependency levels.
195195-// Modules in the same level can be executed in parallel.
196196-// Each level must complete before the next level can start.
197197-// An edge from A to B means A depends on B, so B must run before A.
183183+// TopologicalSort returns modules grouped by dependency levels for parallel execution.
184184+// Edge from A to B means A depends on B, so B must run before A.
198185func (g *Graph) TopologicalSort() [][]string {
199186 // Build adjacency list and in-degree count
200187 adjList := make(map[string][]string)