kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/services/cli/command_edges.go (about) 1 /* 2 * Copyright 2017 The Kythe Authors. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cli 18 19 import ( 20 "context" 21 "errors" 22 "flag" 23 "fmt" 24 "html" 25 "sort" 26 "strings" 27 28 "kythe.io/kythe/go/services/graph" 29 "kythe.io/kythe/go/util/log" 30 "kythe.io/kythe/go/util/schema/edges" 31 "kythe.io/kythe/go/util/schema/facts" 32 33 "bitbucket.org/creachadair/stringset" 34 35 gpb "kythe.io/kythe/proto/graph_go_proto" 36 ) 37 38 type edgesCommand struct { 39 baseKytheCommand 40 dotGraph bool 41 countOnly bool 42 targetsOnly bool 43 edgeKinds string 44 pageToken string 45 pageSize int 46 } 47 48 func (edgesCommand) Name() string { return "edges" } 49 func (edgesCommand) Synopsis() string { return "retrieve outward edges from a node" } 50 func (c *edgesCommand) SetFlags(flag *flag.FlagSet) { 51 flag.BoolVar(&c.dotGraph, "graphviz", false, "Print resulting edges as a dot graph") 52 flag.BoolVar(&c.countOnly, "count_only", false, "Only print counts per edge kind") 53 flag.BoolVar(&c.targetsOnly, "targets_only", false, "Only display edge targets") 54 flag.StringVar(&c.edgeKinds, "kinds", "", "Comma-separated list of edge kinds to return (default returns all)") 55 flag.StringVar(&c.pageToken, "page_token", "", "Edges page token") 56 flag.IntVar(&c.pageSize, "page_size", 0, "Maximum number of edges returned (0 lets the service use a sensible default)") 57 } 58 func (c edgesCommand) Run(ctx context.Context, flag *flag.FlagSet, api API) error { 59 if c.countOnly && c.targetsOnly { 60 return errors.New("--count_only and --targets_only are mutually exclusive") 61 } else if c.countOnly && c.dotGraph { 62 return errors.New("--count_only and --graphviz are mutually exclusive") 63 } else if c.targetsOnly && c.dotGraph { 64 return errors.New("--targets_only and --graphviz are mutually exclusive") 65 } 66 67 req := &gpb.EdgesRequest{ 68 Ticket: flag.Args(), 69 PageToken: c.pageToken, 70 PageSize: int32(c.pageSize), 71 } 72 if c.edgeKinds != "" { 73 for _, kind := range strings.Split(c.edgeKinds, ",") { 74 req.Kind = append(req.Kind, c.expandEdgeKind(kind)) 75 } 76 } 77 if c.dotGraph { 78 req.Filter = []string{"**"} 79 } 80 LogRequest(req) 81 reply, err := api.GraphService.Edges(ctx, req) 82 if err != nil { 83 return err 84 } 85 if reply.NextPageToken != "" { 86 defer log.InfoContextf(ctx, "Next page token: %s", reply.NextPageToken) 87 } 88 if c.countOnly { 89 return c.displayEdgeCounts(reply) 90 } else if c.targetsOnly { 91 return c.displayTargets(reply.EdgeSets) 92 } else if c.dotGraph { 93 return c.displayEdgeGraph(reply) 94 } 95 return c.displayEdges(reply) 96 } 97 98 func (c edgesCommand) displayEdges(reply *gpb.EdgesReply) error { 99 if DisplayJSON { 100 return PrintJSONMessage(reply) 101 } 102 103 for source, es := range reply.EdgeSets { 104 if _, err := fmt.Fprintln(out, "source:", source); err != nil { 105 return err 106 } 107 for kind, g := range es.Groups { 108 hasOrdinal := edges.OrdinalKind(kind) 109 for _, edge := range g.Edge { 110 var ordinal string 111 if hasOrdinal || edge.Ordinal != 0 { 112 ordinal = fmt.Sprintf(".%d", edge.Ordinal) 113 } 114 if _, err := fmt.Fprintf(out, "%s%s\t%s\n", kind, ordinal, edge.TargetTicket); err != nil { 115 return err 116 } 117 } 118 } 119 } 120 return nil 121 } 122 123 func (c edgesCommand) displayTargets(edges map[string]*gpb.EdgeSet) error { 124 var targets stringset.Set 125 for _, es := range edges { 126 for _, g := range es.Groups { 127 for _, e := range g.Edge { 128 targets.Add(e.TargetTicket) 129 } 130 } 131 } 132 133 if DisplayJSON { 134 return PrintJSON(targets.Elements()) 135 } 136 137 for target := range targets { 138 if _, err := fmt.Fprintln(out, target); err != nil { 139 return err 140 } 141 } 142 return nil 143 } 144 145 func (c edgesCommand) displayEdgeGraph(reply *gpb.EdgesReply) error { 146 nodes := graph.NodesMap(reply.Nodes) 147 esets := make(map[string]map[string]stringset.Set) 148 149 for source, es := range reply.EdgeSets { 150 for gKind, g := range es.Groups { 151 hasOrdinal := edges.OrdinalKind(gKind) 152 for _, edge := range g.Edge { 153 tgt := edge.TargetTicket 154 src, kind := source, gKind 155 if edges.IsReverse(kind) { 156 src, kind, tgt = tgt, edges.Mirror(kind), src 157 } 158 if hasOrdinal || edge.Ordinal != 0 { 159 kind = fmt.Sprintf("%s.%d", kind, edge.Ordinal) 160 } 161 groups, ok := esets[src] 162 if !ok { 163 groups = make(map[string]stringset.Set) 164 esets[src] = groups 165 } 166 targets, ok := groups[kind] 167 if ok { 168 targets.Add(tgt) 169 } else { 170 groups[kind] = stringset.New(tgt) 171 } 172 173 } 174 } 175 } 176 if _, err := fmt.Println("digraph kythe {"); err != nil { 177 return err 178 } 179 for ticket, node := range nodes { 180 if _, err := fmt.Printf(` %q [label=<<table><tr><td colspan="2">%s</td></tr>`, ticket, html.EscapeString(ticket)); err != nil { 181 return err 182 } 183 var factNames []string 184 for fact := range node { 185 if fact == facts.Code { 186 continue 187 } 188 factNames = append(factNames, fact) 189 } 190 sort.Strings(factNames) 191 for _, fact := range factNames { 192 if _, err := fmt.Printf("<tr><td>%s</td><td>%s</td></tr>", html.EscapeString(fact), html.EscapeString(string(node[fact]))); err != nil { 193 return err 194 } 195 } 196 if _, err := fmt.Println("</table>> shape=plaintext];"); err != nil { 197 return err 198 } 199 } 200 if _, err := fmt.Println(); err != nil { 201 return err 202 } 203 204 for src, groups := range esets { 205 for kind, targets := range groups { 206 for tgt := range targets { 207 if _, err := fmt.Printf("\t%q -> %q [label=%q];\n", src, tgt, kind); err != nil { 208 return err 209 } 210 } 211 } 212 } 213 if _, err := fmt.Println("}"); err != nil { 214 return err 215 } 216 return nil 217 } 218 219 func (c edgesCommand) displayEdgeCounts(edges *gpb.EdgesReply) error { 220 counts := make(map[string]int) 221 for _, es := range edges.EdgeSets { 222 for kind, g := range es.Groups { 223 counts[kind] += len(g.Edge) 224 } 225 } 226 227 if DisplayJSON { 228 return PrintJSON(counts) 229 } 230 231 for kind, cnt := range counts { 232 if _, err := fmt.Fprintf(out, "%s\t%d\n", kind, cnt); err != nil { 233 return err 234 } 235 } 236 return nil 237 } 238 239 // expandEdgeKind prefixes unrooted (not starting with "/") edge kinds with the 240 // standard Kythe edge prefix ("/kythe/edge/"). 241 func (c edgesCommand) expandEdgeKind(kind string) string { 242 ck := edges.Canonical(kind) 243 if strings.HasPrefix(ck, "/") { 244 return kind 245 } 246 247 expansion := edges.Prefix + ck 248 if edges.IsReverse(kind) { 249 return edges.Mirror(expansion) 250 } 251 return expansion 252 }