@@ -22,12 +22,23 @@ type Repo struct {
2222 Packages map [string ]* Package
2323}
2424
25+ type Package struct {
26+ Name string
27+ PartOfModule bool
28+ Dependants []* Package
29+ Changed bool
30+ }
31+
32+ // NewRepo constructs a Repo from path, which needs to contain a go.mod file.
33+ // It builds a map of all packages found in that repo and the dependencies
34+ // between them.
2535func NewRepo (path string ) (* Repo , error ) {
2636 repo := & Repo {
2737 path : path ,
2838 Packages : map [string ]* Package {},
2939 }
3040
41+ // Parse go.mod
3142 b , err := os .ReadFile (filepath .Join (path , "go.mod" ))
3243 if err != nil {
3344 return nil , err
@@ -40,9 +51,12 @@ func NewRepo(path string) (*Repo, error) {
4051
4152 repo .Module = mod
4253
54+ // Find all go packages starting from path
4355 err = filepath .Walk (path , func (p string , f os.FileInfo , err error ) error {
4456 if f .IsDir () && ! directoryShouldBeIgnored (p ) {
4557 fset := token .NewFileSet ()
58+
59+ // We're interested in each package imports at this point
4660 pkgs , err := parser .ParseDir (fset , p , nil , parser .ImportsOnly )
4761 if err != nil {
4862 return err
@@ -55,13 +69,14 @@ func NewRepo(path string) (*Repo, error) {
5569
5670 var imports []string
5771 for _ , file := range pkg .Files {
72+ // Don't map test packages
5873 if ! strings .HasSuffix (file .Name .Name , "_test" ) {
5974 for _ , imp := range file .Imports {
6075 imports = append (imports , strings .ReplaceAll (imp .Path .Value , `"` , "" ))
6176 }
6277 }
6378 }
64- repo .AddPackage (strings .TrimPrefix (p , path + "/" ), imports )
79+ repo .addPackage (strings .TrimPrefix (p , path + "/" ), imports )
6580 }
6681 }
6782 return nil
@@ -73,18 +88,51 @@ func NewRepo(path string) (*Repo, error) {
7388 return repo , nil
7489}
7590
76- func (r * Repo ) AddPackage (path string , imports []string ) {
91+ // ChangesFrom returns a list of all packages within the repository (excluding
92+ // packages in vendor/) that changed since the given revision. A package will
93+ // be flagged as change if any file within the package itself changed or if any
94+ // packages it imports (whether local, vendored or external modules) changed
95+ // since the given revision.
96+ func (r * Repo ) ChangesFrom (revision string ) ([]string , error ) {
97+ err := r .detectInternalChangesFrom (revision )
98+ if err != nil {
99+ return nil , err
100+ }
101+
102+ err = r .detectGoModulesChanges (revision )
103+ if err != nil {
104+ return nil , err
105+ }
106+
107+ var changedOwnedPackages []string
108+ for _ , pkg := range r .Packages {
109+ if pkg .PartOfModule && pkg .Changed {
110+ changedOwnedPackages = append (changedOwnedPackages , pkg .Name )
111+ }
112+ }
113+
114+ return changedOwnedPackages , nil
115+ }
116+
117+ // addPackage adds the package found at path to the repo, and also adds it as a
118+ // dependant to all of the packages it imports.
119+ func (r * Repo ) addPackage (path string , imports []string ) {
77120 var pkgName string
78121
122+ // if path has vendor/ prefix, that needs to be removed to get the actual
123+ // package name
79124 if strings .HasPrefix (path , "vendor/" ) {
80125 pkgName = strings .TrimPrefix (path , "vendor/" )
81126 } else {
127+ // if it doesn't have a vendor/ prefix it means it's part of our module and
128+ // path should be prefixed with the module name.
82129 pkgName = r .ModuleName ()
83130 if path != r .path {
84131 pkgName += "/" + path
85132 }
86133 }
87134
135+ // add the new package to the repo if it didn't exist already
88136 pkg , exists := r .Packages [pkgName ]
89137 if ! exists {
90138 pkg = & Package {
@@ -94,17 +142,21 @@ func (r *Repo) AddPackage(path string, imports []string) {
94142 r .Packages [pkgName ] = pkg
95143 }
96144
145+ // imports might not be a unique list, but we only want to add pkg as a
146+ // dependant to those packages once
97147 alreadyProcessedImports := map [string ]interface {}{}
98148 for _ , dependency := range imports {
99149 if _ , alreadyProcessed := alreadyProcessedImports [dependency ]; alreadyProcessed {
100150 continue
101151 }
102- r .AddDependant (pkg , dependency )
152+ r .addDependant (pkg , dependency )
103153 alreadyProcessedImports [dependency ] = struct {}{}
104154 }
105155}
106156
107- func (r * Repo ) AddDependant (dependant * Package , dependencyName string ) {
157+ // addDependant adds dependant as one of the dependants of the package
158+ // identified by dependencyName (if it doesn't exist yet, it will be created).
159+ func (r * Repo ) addDependant (dependant * Package , dependencyName string ) {
108160 dependency , exists := r .Packages [dependencyName ]
109161 if ! exists {
110162 dependency = & Package {
@@ -117,29 +169,10 @@ func (r *Repo) AddDependant(dependant *Package, dependencyName string) {
117169 dependency .Dependants = append (dependency .Dependants , dependant )
118170}
119171
120- func (r * Repo ) ChangesFrom (revision string ) ([]string , error ) {
121- err := r .detectInternalChangesFrom (revision )
122- if err != nil {
123- return nil , err
124- }
125-
126- err = r .detectGoModulesChanges (revision )
127- if err != nil {
128- return nil , err
129- }
130-
131- var changedOwnedPackages []string
132- for _ , pkg := range r .Packages {
133- if pkg .PartOfModule && pkg .Changed {
134- changedOwnedPackages = append (changedOwnedPackages , pkg .Name )
135- }
136- }
137-
138- return changedOwnedPackages , nil
139- }
140-
172+ // detectInternalChangesFrom will run a git diff (revision...HEAD) and flag as
173+ // changed any packages (part of the module in repo or vendored packages) that
174+ // have *.go files that are part of the that diff and packages that depend on them
141175func (r * Repo ) detectInternalChangesFrom (revision string ) error {
142- // git diff go files
143176 repo , err := git .PlainOpen (r .path )
144177 if err != nil {
145178 return err
@@ -150,11 +183,13 @@ func (r *Repo) detectInternalChangesFrom(revision string) error {
150183 return err
151184 }
152185
186+ // Get the HEAD commit
153187 now , err := repo .CommitObject (head .Hash ())
154188 if err != nil {
155189 return err
156190 }
157191
192+ // Get the tree for HEAD
158193 nowTree , err := now .Tree ()
159194 if err != nil {
160195 return err
@@ -165,31 +200,38 @@ func (r *Repo) detectInternalChangesFrom(revision string) error {
165200 return err
166201 }
167202
203+ // Find the commit for given revision
168204 then , err := repo .CommitObject (* ref )
169205 if err != nil {
170206 return err
171207 }
172208
209+ // Get the tree for given revision
173210 thenTree , err := then .Tree ()
174211 if err != nil {
175212 return err
176213 }
177214
215+ // Get a diff between the two trees
178216 diff , err := nowTree .Diff (thenTree )
179217 if err != nil {
180218 return err
181219 }
182220
183221 for _ , change := range diff {
222+ // we're only interested in Go files
184223 if ! strings .HasSuffix (change .From .Name , ".go" ) {
185224 continue
186225 }
187226
188227 var pkgName string
228+ // if the changed file is in vendor/ stripping "vendor/" will give us the
229+ // package name
189230 if strings .HasPrefix (change .From .Name , "vendor/" ) {
190231 pkgName = strings .TrimPrefix (filepath .Dir (change .From .Name ), "vendor/" )
191232 }
192233
234+ // package is part of our module
193235 if pkgName == "" {
194236 pkgName = r .ModuleName () + "/" + filepath .Dir (change .From .Name )
195237 }
@@ -200,53 +242,67 @@ func (r *Repo) detectInternalChangesFrom(revision string) error {
200242 return nil
201243}
202244
245+ // detectGoModulesChanges finds differences in dependencies required by
246+ // HEAD:go.mod and {revision}:go.mod and flags as changed any packages
247+ // depending on any of the changed dependencies.
203248func (r * Repo ) detectGoModulesChanges (revision string ) error {
204- // get old go.mod
205- // find differences with current one
206- repo , err := git .PlainOpen (r .path )
249+ oldGoMod , err := r .getGoModFromRevision (revision )
207250 if err != nil {
208251 return err
209252 }
210253
254+ differentModules := goModDifferences (oldGoMod , r .Module )
255+ for _ , module := range differentModules {
256+ r .flagPackageAsChanged (module )
257+ }
258+
259+ return nil
260+ }
261+
262+ // getGoModFromRevision returns (if found) the go.mod file from the given
263+ // revision.
264+ func (r * Repo ) getGoModFromRevision (revision string ) (* modfile.File , error ) {
265+ repo , err := git .PlainOpen (r .path )
266+ if err != nil {
267+ return nil , err
268+ }
269+
211270 ref , err := repo .ResolveRevision (plumbing .Revision (revision ))
212271 if err != nil {
213- return err
272+ return nil , err
214273 }
215274
216275 then , err := repo .CommitObject (* ref )
217276 if err != nil {
218- return err
277+ return nil , err
219278 }
220279
221280 file , err := then .File ("go.mod" )
222281 if err != nil {
223- return err
282+ return nil , err
224283 }
225284
226285 reader , err := file .Reader ()
227286 if err != nil {
228- return err
287+ return nil , err
229288 }
230289 defer reader .Close ()
231290
232291 b , err := ioutil .ReadAll (reader )
233292 if err != nil {
234- return err
293+ return nil , err
235294 }
236295
237296 mod , err := modfile .Parse (filepath .Join (r .path , "go.mod" ), b , nil )
238297 if err != nil {
239- return err
240- }
241-
242- differentModules := goModDifferences (mod , r .Module )
243- for _ , module := range differentModules {
244- r .flagPackageAsChanged (module )
298+ return nil , err
245299 }
246300
247- return nil
301+ return mod , nil
248302}
249303
304+ // flagPackageAsChanged flags the package with the given name and all of its
305+ // dependant as changed, recursively.
250306func (r * Repo ) flagPackageAsChanged (name string ) {
251307 pkg , exists := r .Packages [name ]
252308 if ! exists {
@@ -273,13 +329,6 @@ func (r *Repo) OwnsPackage(pkgName string) bool {
273329 return strings .HasPrefix (pkgName , r .ModuleName ())
274330}
275331
276- type Package struct {
277- Name string
278- PartOfModule bool
279- Dependants []* Package
280- Changed bool
281- }
282-
283332func directoryShouldBeIgnored (path string ) bool {
284333 return strings .Contains (path , ".git" )
285334}
0 commit comments