< Summary - go-semantic-release Coverage

Information
Line coverage
100%
Covered lines: 50
Uncovered lines: 0
Coverable lines: 50
Total lines: 131
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Analyze0%00100%
findAffectedProjects0%00100%
filterFiles0%00100%
propagateDependencies0%00100%

File(s)

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/git/impact_analyzer.go

#LineLine coverage
 1package git
 2
 3import (
 4  "path/filepath"
 5  "strings"
 6
 7  "github.com/jedi-knights/go-semantic-release/internal/domain"
 8  "github.com/jedi-knights/go-semantic-release/internal/ports"
 9)
 10
 11// Compile-time interface compliance check.
 12var _ ports.ProjectImpactAnalyzer = (*PathBasedImpactAnalyzer)(nil)
 13
 14// PathBasedImpactAnalyzer maps changed files to projects based on path prefixes.
 15type PathBasedImpactAnalyzer struct {
 16  propagateDeps bool
 17  includePaths  []string
 18  excludePaths  []string
 19}
 20
 21// NewPathBasedImpactAnalyzer creates a new path-based impact analyzer.
 22func NewPathBasedImpactAnalyzer(propagateDeps bool, includePaths, excludePaths []string) *PathBasedImpactAnalyzer {
 23  return &PathBasedImpactAnalyzer{
 24    propagateDeps: propagateDeps,
 25    includePaths:  includePaths,
 26    excludePaths:  excludePaths,
 27  }
 28}
 29
 830func (a *PathBasedImpactAnalyzer) Analyze(projects []domain.Project, commits []domain.Commit) map[string][]domain.Commit
 831  result := make(map[string][]domain.Commit)
 832
 833  for i := range commits {
 2234    affected := a.findAffectedProjects(projects, commits[i].FilesChanged)
 1635    for _, projName := range affected {
 1636      result[projName] = append(result[projName], commits[i])
 1637    }
 38  }
 39
 140  if a.propagateDeps {
 141    a.propagateDependencies(projects, result)
 142  }
 43
 844  return result
 45}
 46
 2247func (a *PathBasedImpactAnalyzer) findAffectedProjects(projects []domain.Project, files []string) []string {
 2248  seen := make(map[string]bool)
 2249  var affected []string
 2250
 2251  filtered := a.filterFiles(files)
 2052  for _, file := range filtered {
 2053    for _, proj := range projects {
 554      if seen[proj.Name] {
 555        continue
 56      }
 1657      if proj.IsRoot() || fileInProject(file, proj.Path) {
 1658        seen[proj.Name] = true
 1659        affected = append(affected, proj.Name)
 1660      }
 61    }
 62  }
 2263  return affected
 64}
 65
 66// filterFiles applies include/exclude glob patterns to the file list.
 2267func (a *PathBasedImpactAnalyzer) filterFiles(files []string) []string {
 768  if len(a.includePaths) == 0 && len(a.excludePaths) == 0 {
 769    return files
 770  }
 71
 1572  result := make([]string, 0, len(files))
 1573  for _, file := range files {
 774    if len(a.includePaths) > 0 && !matchesAny(file, a.includePaths) {
 775      continue
 76    }
 377    if matchesAny(file, a.excludePaths) {
 378      continue
 79    }
 1180    result = append(result, file)
 81  }
 1582  return result
 83}
 84
 85// matchesAny returns true if the file matches any of the glob patterns.
 86func matchesAny(file string, patterns []string) bool {
 87  for _, pattern := range patterns {
 88    if matched, _ := filepath.Match(pattern, file); matched {
 89      return true
 90    }
 91    // Also try matching against just the filename for simple patterns.
 92    if matched, _ := filepath.Match(pattern, filepath.Base(file)); matched {
 93      return true
 94    }
 95    // Support prefix-based patterns like "services/api/**" by checking prefix.
 96    if strings.HasSuffix(pattern, "/**") {
 97      prefix := strings.TrimSuffix(pattern, "/**")
 98      if strings.HasPrefix(file, prefix+"/") || file == prefix {
 99        return true
 100      }
 101    }
 102  }
 103  return false
 104}
 105
 106func fileInProject(file, projectPath string) bool {
 107  if projectPath == "" || projectPath == "." {
 108    return true
 109  }
 110  prefix := projectPath + "/"
 111  return strings.HasPrefix(file, prefix) || file == projectPath
 112}
 113
 1114func (a *PathBasedImpactAnalyzer) propagateDependencies(projects []domain.Project, result map[string][]domain.Commit) {
 1115  projectMap := make(map[string]domain.Project, len(projects))
 1116  for _, p := range projects {
 3117    projectMap[p.Name] = p
 3118  }
 119
 120  // Simple single-pass propagation: if a dependency has commits, mark dependents.
 1121  for _, proj := range projects {
 2122    for _, dep := range proj.Dependencies {
 2123      if commits, ok := result[dep]; ok && len(commits) > 0 {
 2124        if _, alreadyAffected := result[proj.Name]; !alreadyAffected {
 2125          // Propagate with a synthetic marker — use the dependency's commits.
 2126          result[proj.Name] = commits
 2127        }
 128      }
 129    }
 130  }
 131}