1package release 2 3import ( 4 "fmt" 5 "github.com/aws/aws-sdk-go-v2/internal/repotools" 6 "github.com/aws/aws-sdk-go-v2/internal/repotools/changelog" 7 "github.com/aws/aws-sdk-go-v2/internal/repotools/git" 8 "github.com/aws/aws-sdk-go-v2/internal/repotools/gomod" 9 "log" 10 "path" 11 "path/filepath" 12 "sort" 13) 14 15// ModuleFinder is a type that 16type ModuleFinder interface { 17 Root() string 18 19 ModulesRel() (map[string][]string, error) 20} 21 22// Calculate calculates the modules to be released and their next versions based on the Git history, previous tags, 23// module configuration, and associated changelog annotaitons. 24func Calculate(finder ModuleFinder, tags git.ModuleTags, config repotools.Config, annotations []changelog.Annotation) (map[string]*Module, error) { 25 rootDir := finder.Root() 26 27 repositoryModules, err := finder.ModulesRel() 28 if err != nil { 29 log.Fatalf("failed to modules: %v", err) 30 } 31 32 moduleAnnotations := make(map[string][]changelog.Annotation) 33 for _, annotation := range annotations { 34 for _, am := range annotation.Modules { 35 moduleAnnotations[am] = append(moduleAnnotations[am], annotation) 36 } 37 } 38 39 modules := make(map[string]*Module) 40 var repositoryModuleTombstonePaths []string 41 42 for moduleDir := range tags { 43 if _, ok := repositoryModules[moduleDir]; !ok { 44 repositoryModuleTombstonePaths = append(repositoryModuleTombstonePaths, moduleDir) 45 } 46 } 47 48 for moduleDir := range repositoryModules { 49 moduleFile, err := gomod.LoadModuleFile(filepath.Join(rootDir, moduleDir), nil, true) 50 if err != nil { 51 return nil, fmt.Errorf("failed to load module file: %w", err) 52 } 53 54 modulePath, err := gomod.GetModulePath(moduleFile) 55 if err != nil { 56 return nil, fmt.Errorf("failed to read module path: %w", err) 57 } 58 59 var latestVersion string 60 var hasChanges bool 61 62 latestVersion, ok := tags.Latest(moduleDir) 63 if ok { 64 startTag, err := git.ToModuleTag(moduleDir, latestVersion) 65 if err != nil { 66 log.Fatalf("failed to convert module path and version to tag: %v", err) 67 } 68 69 changes, err := git.Changes(finder.Root(), startTag, "HEAD", moduleDir) 70 if err != nil { 71 log.Fatalf("failed to get git changes: %v", err) 72 } 73 74 subModulePaths := repositoryModules[moduleDir] 75 76 ignoredModulePaths := make([]string, 0, len(subModulePaths)+len(repositoryModuleTombstonePaths)) 77 ignoredModulePaths = append(ignoredModulePaths, subModulePaths...) 78 79 if len(repositoryModuleTombstonePaths) > 0 { 80 ignoredModulePaths = append(ignoredModulePaths, repositoryModuleTombstonePaths...) 81 // IsModuleChanged expects the provided list of ignored modules paths to be sorted 82 sort.Strings(ignoredModulePaths) 83 } 84 85 hasChanges, err = gomod.IsModuleChanged(moduleDir, ignoredModulePaths, changes) 86 if err != nil { 87 return nil, fmt.Errorf("failed to determine module changes: %w", err) 88 } 89 90 if !hasChanges { 91 // Check if any of the submodules have been "carved out" of this module since the last tagged release 92 for _, subModuleDir := range subModulePaths { 93 if _, ok := tags.Latest(subModuleDir); ok { 94 continue 95 } 96 97 treeFiles, err := git.LsTree(rootDir, startTag, subModuleDir) 98 if err != nil { 99 return nil, fmt.Errorf("failed to list git tree: %v", err) 100 } 101 102 carvedOut, err := isModuleCarvedOut(treeFiles, repositoryModules[subModuleDir]) 103 if err != nil { 104 return nil, err 105 } 106 if carvedOut { 107 hasChanges = true 108 break 109 } 110 } 111 } 112 } 113 114 var changeReason ModuleChange 115 if hasChanges && len(latestVersion) > 0 { 116 changeReason |= SourceChange 117 } else if len(latestVersion) == 0 { 118 changeReason |= NewModule 119 } 120 121 modules[modulePath] = &Module{ 122 File: moduleFile, 123 RelativeRepoPath: moduleDir, 124 Latest: latestVersion, 125 Changes: changeReason, 126 ChangeAnnotations: moduleAnnotations[moduleDir], 127 ModuleConfig: config.Modules[moduleDir], 128 } 129 } 130 131 if err := CalculateDependencyUpdates(modules); err != nil { 132 return nil, err 133 } 134 135 for moduleDir := range modules { 136 if modules[moduleDir].Changes == 0 || config.Modules[moduleDir].NoTag { 137 delete(modules, moduleDir) 138 } 139 } 140 141 return modules, nil 142} 143 144// isModuleCarvedOut takes a list of files for a (new) submodule directory. The list of files are the files that are located 145// in the submodule directory path from the parent's previous tagged release. Returns true the new submodule has been 146// carved out of the parent module directory it is located under. This is determined by looking through the file list 147// and determining if Go source is present but no `go.mod` file existed. 148func isModuleCarvedOut(files []string, subModules []string) (bool, error) { 149 hasGoSource := false 150 hasGoMod := false 151 152 isChildPathCache := make(map[string]bool) 153 154 for _, file := range files { 155 dir, fileName := path.Split(file) 156 dir = path.Clean(dir) 157 158 isGoMod := gomod.IsGoMod(fileName) 159 isGoSource := gomod.IsGoSource(fileName) 160 161 if !(isGoMod || isGoSource) { 162 continue 163 } 164 165 if isChild, ok := isChildPathCache[dir]; (isChild && ok) || (!ok && gomod.IsSubmodulePath(dir, subModules)) { 166 isChildPathCache[dir] = true 167 continue 168 } else { 169 isChildPathCache[dir] = false 170 } 171 172 if isGoSource { 173 hasGoSource = true 174 } else if isGoMod { 175 hasGoMod = true 176 } 177 178 if hasGoMod && hasGoSource { 179 break 180 } 181 } 182 183 return !hasGoMod && hasGoSource, nil 184} 185