@@ -14,6 +14,8 @@ import (
14
14
15
15
"github.com/gonum/graph"
16
16
"github.com/gonum/graph/encoding/dot"
17
+
18
+ depgraph "github.com/openshift/origin/tools/depcheck/pkg/graph"
17
19
)
18
20
19
21
var traceImportsExample = `# create a dependency graph
@@ -30,6 +32,7 @@ type TraceImportsOpts struct {
30
32
excludes []string
31
33
filters []string
32
34
35
+ Analyze string
33
36
ExcludeFileName string
34
37
CollapseFileName string
35
38
@@ -44,13 +47,15 @@ type TraceImportsFlags struct {
44
47
OutputFormat string
45
48
Exclude string
46
49
Collapse string
50
+ Analyze string
47
51
}
48
52
49
53
func (o * TraceImportsFlags ) ToOptions (out , errout io.Writer ) (* TraceImportsOpts , error ) {
50
54
return & TraceImportsOpts {
51
55
Roots : o .Roots ,
52
56
ExcludeFileName : o .Exclude ,
53
57
CollapseFileName : o .Collapse ,
58
+ Analyze : o .Analyze ,
54
59
55
60
OutputFormat : o .OutputFormat ,
56
61
@@ -91,6 +96,7 @@ func NewCmdTraceImports(parent string, out, errout io.Writer) *cobra.Command {
91
96
cmd .Flags ().StringVarP (& flags .Exclude , "exclude" , "e" , "" , "json file containing a list of import-paths of packages to recursively exclude when traversing the set of given entrypoints specified through --root." )
92
97
cmd .Flags ().StringVarP (& flags .OutputFormat , "output" , "o" , "" , "output generated dependency graph in specified format. One of: dot." )
93
98
cmd .Flags ().StringVarP (& flags .Collapse , "collapse" , "c" , "" , "json file containing a list of import-paths of packages to collapse sub-packages into." )
99
+ cmd .Flags ().StringVarP (& flags .Analyze , "analyze" , "a" , "" , "output a summary report on the dependency set of a given repository. Mutually exclusive with --output" )
94
100
return cmd
95
101
}
96
102
@@ -151,7 +157,10 @@ func (o *TraceImportsOpts) Validate() error {
151
157
if len (o .Roots ) == 0 {
152
158
return errors .New ("at least one root package must be provided" )
153
159
}
154
- if len (o .OutputFormat ) != 0 && o .OutputFormat != "dot" {
160
+ if len (o .OutputFormat ) > 0 && len (o .Analyze ) > 0 {
161
+ return fmt .Errorf ("--output and --analyze are mutually exclusive" )
162
+ }
163
+ if len (o .OutputFormat ) > 0 && o .OutputFormat != "dot" {
155
164
return fmt .Errorf ("invalid output format provided: %s" , o .OutputFormat )
156
165
}
157
166
@@ -209,6 +218,10 @@ func (o *TraceImportsOpts) Run() error {
209
218
return o .outputGraph (g )
210
219
}
211
220
221
+ if len (o .Analyze ) > 0 {
222
+ return o .analyzeGraph (g )
223
+ }
224
+
212
225
return nil
213
226
}
214
227
@@ -225,3 +238,118 @@ func (o *TraceImportsOpts) outputGraph(g graph.Directed) error {
225
238
fmt .Fprintf (o .Out , "%v\n " , string (data ))
226
239
return nil
227
240
}
241
+
242
+ // analyzeGraph receives a MutableDirectedGraph and outputs
243
+ // - "Yours": a list of every node in the set unique to the dependency tree of a given target node.
244
+ // - "Mine": a list of every node in the set unique to the dependency tree of the root nodes (set non-overlapping with given target node)
245
+ // - "Ours": a list of every node in the overlapping set between the dependency tree of the root nodes and a given target node
246
+ func (o * TraceImportsOpts ) analyzeGraph (g * depgraph.MutableDirectedGraph ) error {
247
+ remove , exists := g .NodeByName (o .Analyze )
248
+ if ! exists {
249
+ return fmt .Errorf ("unable to find package with import path %q" , o .Analyze )
250
+ }
251
+
252
+ // determine existing orphans and roots
253
+ keep := []graph.Node {}
254
+ roots := []graph.Node {}
255
+ for _ , n := range g .Nodes () {
256
+ node , ok := n .(* depgraph.Node )
257
+ if ! ok {
258
+ continue
259
+ }
260
+
261
+ to := g .To (n )
262
+ from := g .From (n )
263
+
264
+ // found an existing orphaned node to exclude
265
+ if len (to ) == 0 && len (from ) == 0 {
266
+ continue
267
+ }
268
+
269
+ // found a root
270
+ if len (from ) > 0 && len (to ) == 0 {
271
+ roots = append (roots , n )
272
+ continue
273
+ }
274
+
275
+ // skip package we are analyzing
276
+ if node .UniqueName == o .Analyze {
277
+ continue
278
+ }
279
+
280
+ keep = append (keep , n )
281
+ }
282
+
283
+ // remove package we are analyzing from the graph
284
+ g .RemoveNode (remove )
285
+
286
+ // find nodes not reachable from our given roots
287
+ yours := findOrphans (g , roots , keep )
288
+
289
+ fmt .Printf ("\" Yours\" packages exclusive to %q\n " , o .Analyze )
290
+ for _ , n := range yours {
291
+ pkg , ok := n .(* depgraph.Node )
292
+ if ! ok {
293
+ continue
294
+ }
295
+
296
+ fmt .Printf (" - %s\n " , pkg .UniqueName )
297
+ }
298
+
299
+ return nil
300
+ }
301
+
302
+ // findOrphans receives a set of root nodes and a set of child nodes and
303
+ // returns the set of child nodes not reachable from any root node.
304
+ func findOrphans (g * depgraph.MutableDirectedGraph , roots []graph.Node , nodes []graph.Node ) []graph.Node {
305
+ orphans := []graph.Node {}
306
+
307
+ for _ , n := range nodes {
308
+ isOrphan := true
309
+ for _ , root := range roots {
310
+ if hasPathFromTo (g , root , n ) {
311
+ isOrphan = false
312
+ break
313
+ }
314
+ }
315
+
316
+ if ! isOrphan {
317
+ continue
318
+ }
319
+
320
+ orphans = append (orphans , n )
321
+ }
322
+
323
+ return orphans
324
+ }
325
+
326
+ // hasPathFromTo receives a node A and determines
327
+ // if a node B can be reached from it.
328
+ // Returns false if the source and destination are the same.
329
+ func hasPathFromTo (g graph.Directed , A graph.Node , B graph.Node ) bool {
330
+ if A .ID () == B .ID () {
331
+ return false
332
+ }
333
+ if ! g .Has (A ) || ! g .Has (B ) {
334
+ return false
335
+ }
336
+
337
+ seen := map [int ]bool {}
338
+ unseen := []graph.Node {B }
339
+ for len (unseen ) > 0 {
340
+ n := unseen [0 ]
341
+ unseen = unseen [1 :]
342
+
343
+ if _ , isSeen := seen [n .ID ()]; isSeen {
344
+ continue
345
+ }
346
+ if n .ID () == A .ID () {
347
+ return true
348
+ }
349
+
350
+ seen [n .ID ()] = true
351
+ unseen = append (unseen , g .To (n )... )
352
+ }
353
+
354
+ return false
355
+ }
0 commit comments