github.phpd.cn/thought-machine/please@v12.2.0+incompatible/tools/please_diff_graphs/diff/plz_diff_graphs.go (about) 1 // Package diff contains the implementation of the graph differ. 2 package diff 3 4 import ( 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "sort" 9 10 "gopkg.in/op/go-logging.v1" 11 12 "core" 13 "query" 14 ) 15 16 var log = logging.MustGetLogger("diff") 17 18 // ParseGraphOrDie reads a graph file, or dies if anything goes wrong. 19 func ParseGraphOrDie(filename string) *query.JSONGraph { 20 graph := query.JSONGraph{} 21 data, err := ioutil.ReadFile(filename) 22 if err != nil { 23 log.Fatalf("Error reading graph: %s", err) 24 } 25 if err := json.Unmarshal(data, &graph); err != nil { 26 log.Fatalf("Error parsing graph: %s", err) 27 } 28 return &graph 29 } 30 31 // Graphs calculates the differences between two graphs. 32 func Graphs(before, after *query.JSONGraph, changedFiles, include, exclude []string, recurse bool) []core.BuildLabel { 33 changedFileMap := toMap(changedFiles) 34 allChanges := map[string]bool{} 35 for pkgName, afterPkg := range after.Packages { 36 beforePkg, present := before.Packages[pkgName] 37 if !present { 38 // Package didn't exist before, add every target in it. 39 for targetName := range afterPkg.Targets { 40 label := core.BuildLabel{PackageName: pkgName, Name: targetName} 41 allChanges[label.String()] = true 42 } 43 continue 44 } 45 for targetName, afterTarget := range afterPkg.Targets { 46 beforeTarget := beforePkg.Targets[targetName] 47 if targetChanged(&beforeTarget, &afterTarget, pkgName, changedFileMap) { 48 label := core.BuildLabel{PackageName: pkgName, Name: targetName} 49 allChanges[label.String()] = true 50 } 51 } 52 } 53 // Now we have all the targets that are directly changed, we locate all transitive ones 54 // in a second pass. We can't do this above because we've got no sensible ordering for it. 55 ret := core.BuildLabels{} 56 for pkgName, pkg := range after.Packages { 57 for targetName, target := range pkg.Targets { 58 if depsChanged(after, allChanges, pkgName, targetName, recurse) && 59 shouldInclude(&target, include, exclude) { 60 ret = append(ret, core.BuildLabel{PackageName: pkgName, Name: targetName}) 61 } 62 } 63 } 64 sort.Sort(ret) 65 return ret 66 } 67 68 func targetChanged(before, after *query.JSONTarget, pkgName string, changedFiles map[string]bool) bool { 69 if before.Hash != after.Hash { 70 return true 71 } 72 // Note that if the set of sources etc has changed, the hash will have changed also, 73 // so here we're only worrying about the content. 74 for _, src := range after.Sources { 75 if _, present := changedFiles[src]; present { 76 return true 77 } 78 } 79 // Same for data files. 80 for _, data := range after.Data { 81 if _, present := changedFiles[data]; present { 82 return true 83 } 84 } 85 return false 86 } 87 88 // depsChanged returns true if any of the transitive dependencies of this target have changed. 89 // It marks any changes in allChanges for efficiency. 90 func depsChanged(graph *query.JSONGraph, allChanges map[string]bool, pkgName, targetName string, recurse bool) bool { 91 label := fmt.Sprintf("//%s:%s", pkgName, targetName) 92 changed, present := allChanges[label] 93 if present { 94 return changed 95 } 96 target := graph.Packages[pkgName].Targets[targetName] 97 if !recurse { 98 return false 99 } 100 for _, dep := range target.Deps { 101 depLabel := core.ParseBuildLabel(dep, "") 102 if depsChanged(graph, allChanges, depLabel.PackageName, depLabel.Name, recurse) { 103 allChanges[label] = true 104 return true 105 } 106 } 107 allChanges[label] = false 108 return false 109 } 110 111 // toMap is a utility function to convert a slice of strings to a map for faster lookup. 112 func toMap(in []string) map[string]bool { 113 ret := map[string]bool{} 114 for _, s := range in { 115 ret[s] = true 116 } 117 return ret 118 } 119 120 // shouldInclude returns true if the given combination of labels means we should return this target. 121 func shouldInclude(target *query.JSONTarget, include, exclude []string) bool { 122 return (len(include) == 0 || hasAnyLabel(target, include)) && !hasAnyLabel(target, exclude) 123 } 124 125 func hasAnyLabel(target *query.JSONTarget, labels []string) bool { 126 for _, l1 := range labels { 127 for _, l2 := range target.Labels { 128 if l1 == l2 { 129 return true 130 } 131 } 132 } 133 return false 134 }