@@ -22,12 +22,23 @@ type Repo struct {
22
22
Packages map [string ]* Package
23
23
}
24
24
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.
25
35
func NewRepo (path string ) (* Repo , error ) {
26
36
repo := & Repo {
27
37
path : path ,
28
38
Packages : map [string ]* Package {},
29
39
}
30
40
41
+ // Parse go.mod
31
42
b , err := os .ReadFile (filepath .Join (path , "go.mod" ))
32
43
if err != nil {
33
44
return nil , err
@@ -40,9 +51,12 @@ func NewRepo(path string) (*Repo, error) {
40
51
41
52
repo .Module = mod
42
53
54
+ // Find all go packages starting from path
43
55
err = filepath .Walk (path , func (p string , f os.FileInfo , err error ) error {
44
56
if f .IsDir () && ! directoryShouldBeIgnored (p ) {
45
57
fset := token .NewFileSet ()
58
+
59
+ // We're interested in each package imports at this point
46
60
pkgs , err := parser .ParseDir (fset , p , nil , parser .ImportsOnly )
47
61
if err != nil {
48
62
return err
@@ -55,13 +69,14 @@ func NewRepo(path string) (*Repo, error) {
55
69
56
70
var imports []string
57
71
for _ , file := range pkg .Files {
72
+ // Don't map test packages
58
73
if ! strings .HasSuffix (file .Name .Name , "_test" ) {
59
74
for _ , imp := range file .Imports {
60
75
imports = append (imports , strings .ReplaceAll (imp .Path .Value , `"` , "" ))
61
76
}
62
77
}
63
78
}
64
- repo .AddPackage (strings .TrimPrefix (p , path + "/" ), imports )
79
+ repo .addPackage (strings .TrimPrefix (p , path + "/" ), imports )
65
80
}
66
81
}
67
82
return nil
@@ -73,18 +88,51 @@ func NewRepo(path string) (*Repo, error) {
73
88
return repo , nil
74
89
}
75
90
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 ) {
77
120
var pkgName string
78
121
122
+ // if path has vendor/ prefix, that needs to be removed to get the actual
123
+ // package name
79
124
if strings .HasPrefix (path , "vendor/" ) {
80
125
pkgName = strings .TrimPrefix (path , "vendor/" )
81
126
} 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.
82
129
pkgName = r .ModuleName ()
83
130
if path != r .path {
84
131
pkgName += "/" + path
85
132
}
86
133
}
87
134
135
+ // add the new package to the repo if it didn't exist already
88
136
pkg , exists := r .Packages [pkgName ]
89
137
if ! exists {
90
138
pkg = & Package {
@@ -94,17 +142,21 @@ func (r *Repo) AddPackage(path string, imports []string) {
94
142
r .Packages [pkgName ] = pkg
95
143
}
96
144
145
+ // imports might not be a unique list, but we only want to add pkg as a
146
+ // dependant to those packages once
97
147
alreadyProcessedImports := map [string ]interface {}{}
98
148
for _ , dependency := range imports {
99
149
if _ , alreadyProcessed := alreadyProcessedImports [dependency ]; alreadyProcessed {
100
150
continue
101
151
}
102
- r .AddDependant (pkg , dependency )
152
+ r .addDependant (pkg , dependency )
103
153
alreadyProcessedImports [dependency ] = struct {}{}
104
154
}
105
155
}
106
156
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 ) {
108
160
dependency , exists := r .Packages [dependencyName ]
109
161
if ! exists {
110
162
dependency = & Package {
@@ -117,29 +169,10 @@ func (r *Repo) AddDependant(dependant *Package, dependencyName string) {
117
169
dependency .Dependants = append (dependency .Dependants , dependant )
118
170
}
119
171
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
141
175
func (r * Repo ) detectInternalChangesFrom (revision string ) error {
142
- // git diff go files
143
176
repo , err := git .PlainOpen (r .path )
144
177
if err != nil {
145
178
return err
@@ -150,11 +183,13 @@ func (r *Repo) detectInternalChangesFrom(revision string) error {
150
183
return err
151
184
}
152
185
186
+ // Get the HEAD commit
153
187
now , err := repo .CommitObject (head .Hash ())
154
188
if err != nil {
155
189
return err
156
190
}
157
191
192
+ // Get the tree for HEAD
158
193
nowTree , err := now .Tree ()
159
194
if err != nil {
160
195
return err
@@ -165,31 +200,38 @@ func (r *Repo) detectInternalChangesFrom(revision string) error {
165
200
return err
166
201
}
167
202
203
+ // Find the commit for given revision
168
204
then , err := repo .CommitObject (* ref )
169
205
if err != nil {
170
206
return err
171
207
}
172
208
209
+ // Get the tree for given revision
173
210
thenTree , err := then .Tree ()
174
211
if err != nil {
175
212
return err
176
213
}
177
214
215
+ // Get a diff between the two trees
178
216
diff , err := nowTree .Diff (thenTree )
179
217
if err != nil {
180
218
return err
181
219
}
182
220
183
221
for _ , change := range diff {
222
+ // we're only interested in Go files
184
223
if ! strings .HasSuffix (change .From .Name , ".go" ) {
185
224
continue
186
225
}
187
226
188
227
var pkgName string
228
+ // if the changed file is in vendor/ stripping "vendor/" will give us the
229
+ // package name
189
230
if strings .HasPrefix (change .From .Name , "vendor/" ) {
190
231
pkgName = strings .TrimPrefix (filepath .Dir (change .From .Name ), "vendor/" )
191
232
}
192
233
234
+ // package is part of our module
193
235
if pkgName == "" {
194
236
pkgName = r .ModuleName () + "/" + filepath .Dir (change .From .Name )
195
237
}
@@ -200,53 +242,67 @@ func (r *Repo) detectInternalChangesFrom(revision string) error {
200
242
return nil
201
243
}
202
244
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.
203
248
func (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 )
207
250
if err != nil {
208
251
return err
209
252
}
210
253
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
+
211
270
ref , err := repo .ResolveRevision (plumbing .Revision (revision ))
212
271
if err != nil {
213
- return err
272
+ return nil , err
214
273
}
215
274
216
275
then , err := repo .CommitObject (* ref )
217
276
if err != nil {
218
- return err
277
+ return nil , err
219
278
}
220
279
221
280
file , err := then .File ("go.mod" )
222
281
if err != nil {
223
- return err
282
+ return nil , err
224
283
}
225
284
226
285
reader , err := file .Reader ()
227
286
if err != nil {
228
- return err
287
+ return nil , err
229
288
}
230
289
defer reader .Close ()
231
290
232
291
b , err := ioutil .ReadAll (reader )
233
292
if err != nil {
234
- return err
293
+ return nil , err
235
294
}
236
295
237
296
mod , err := modfile .Parse (filepath .Join (r .path , "go.mod" ), b , nil )
238
297
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
245
299
}
246
300
247
- return nil
301
+ return mod , nil
248
302
}
249
303
304
+ // flagPackageAsChanged flags the package with the given name and all of its
305
+ // dependant as changed, recursively.
250
306
func (r * Repo ) flagPackageAsChanged (name string ) {
251
307
pkg , exists := r .Packages [name ]
252
308
if ! exists {
@@ -273,13 +329,6 @@ func (r *Repo) OwnsPackage(pkgName string) bool {
273
329
return strings .HasPrefix (pkgName , r .ModuleName ())
274
330
}
275
331
276
- type Package struct {
277
- Name string
278
- PartOfModule bool
279
- Dependants []* Package
280
- Changed bool
281
- }
282
-
283
332
func directoryShouldBeIgnored (path string ) bool {
284
333
return strings .Contains (path , ".git" )
285
334
}
0 commit comments