func displayDirectory(d *ftpb.DirectoryReply) error { if *displayJSON { return jsonMarshaler.Marshal(out, d) } for _, d := range d.Subdirectory { if !lsURIs { uri, err := kytheuri.Parse(d) if err != nil { return fmt.Errorf("received invalid directory uri %q: %v", d, err) } d = filepath.Base(uri.Path) + "/" } if _, err := fmt.Fprintln(out, d); err != nil { return err } } for _, f := range d.File { if !lsURIs { uri, err := kytheuri.Parse(f) if err != nil { return fmt.Errorf("received invalid file ticket %q: %v", f, err) } f = filepath.Base(uri.Path) } if _, err := fmt.Fprintln(out, f); err != nil { return err } } return nil }
func init() { testVNames = make([]*spb.VName, len(testNodes)) for i, n := range testNodes { uri, _ := kytheuri.Parse(n.Ticket) testVNames[i] = uri.VName() } }
func displayRelatedAnchors(kind string, anchors []*xpb.CrossReferencesReply_RelatedAnchor) error { if len(anchors) > 0 { if _, err := fmt.Fprintf(out, " %s:\n", kind); err != nil { return err } for _, a := range anchors { pURI, err := kytheuri.Parse(a.Anchor.Parent) if err != nil { return err } if _, err := fmt.Fprintf(out, " %s\t%s\t[%d:%d-%d:%d)\n %q\n", pURI.Path, showPrintable(a.DisplayName), a.Anchor.Start.LineNumber, a.Anchor.Start.ColumnOffset, a.Anchor.End.LineNumber, a.Anchor.End.ColumnOffset, string(a.Anchor.Snippet)); err != nil { return err } for _, site := range a.Site { if _, err := fmt.Fprintf(out, " [%d:%d-%d-%d)\n %q\n", site.Start.LineNumber, site.Start.ColumnOffset, site.End.LineNumber, site.End.ColumnOffset, string(site.Snippet)); err != nil { return err } } } } return nil }
func (d *DB) createFileTree() error { if _, err := d.Exec(createFilesTable); err != nil { return fmt.Errorf("error creating files table: %v", err) } rs, err := d.Query("SELECT ticket FROM Nodes WHERE node_kind = 'file';") if err != nil { return fmt.Errorf("error creating files query: %v", err) } insert, err := d.Prepare(`INSERT INTO Files (corpus, root, path, ticket, file) VALUES ($1, $2, $3, $4, $5);`) if err != nil { return fmt.Errorf("error preparing statement: %v", err) } for rs.Next() { var ticket string if err := rs.Scan(&ticket); err != nil { return fmt.Errorf("scan error: %v", err) } uri, err := kytheuri.Parse(ticket) if err != nil { return fmt.Errorf("error parsing node ticket %q: %v", ticket, err) } path, _ := filepath.Split(filepath.Join("/", uri.Path)) if _, err := insert.Exec(uri.Corpus, uri.Root, path, ticket, true); err != nil { return fmt.Errorf("error inserting file: %v", err) } uri.Signature, uri.Language = "", "" for { uri.Path = path path, _ = filepath.Split(strings.TrimSuffix(path, "/")) if path == "" { break } if _, err := insert.Exec(uri.Corpus, uri.Root, path, uri.String(), false); err != nil { if err, ok := err.(*pq.Error); ok && err.Code == pqUniqueViolationErrorCode { // Since we've found the current directory, we can stop recursively // adding parent directories now break } return fmt.Errorf("error inserting directory: %v", err) } } } return nil }
// IndexNode writes each of n's VName components and facts to t as search index // entries. MaxIndexedFactValueSize limits fact values written to the index. func IndexNode(ctx context.Context, t table.Inverted, n *srvpb.Node) error { uri, err := kytheuri.Parse(n.Ticket) if err != nil { return err } key := []byte(n.Ticket) if uri.Signature != "" { if err := t.Put(ctx, key, vNameVal("signature", uri.Signature)); err != nil { return err } } if uri.Corpus != "" { if err := t.Put(ctx, key, vNameVal("corpus", uri.Corpus)); err != nil { return err } } if uri.Root != "" { if err := t.Put(ctx, key, vNameVal("root", uri.Root)); err != nil { return err } } if uri.Path != "" { if err := t.Put(ctx, key, vNameVal("path", uri.Path)); err != nil { return err } } if uri.Language != "" { if err := t.Put(ctx, key, vNameVal("language", uri.Language)); err != nil { return err } } for _, f := range n.Fact { if len(f.Value) <= MaxIndexedFactValueSize { if err := t.Put(ctx, key, factVal(f.Name, f.Value)); err != nil { return err } } } return nil }
func displayAnchors(kind string, anchors []*xpb.Anchor) error { if len(anchors) > 0 { if _, err := fmt.Fprintf(out, " %s:\n", kind); err != nil { return err } for _, a := range anchors { pURI, err := kytheuri.Parse(a.Parent) if err != nil { return err } if _, err := fmt.Fprintf(out, " %s\t[%d:%d-%d:%d)\n %q\n", pURI.Path, a.Start.LineNumber, a.Start.ColumnOffset, a.End.LineNumber, a.End.ColumnOffset, string(a.Snippet)); err != nil { return err } } } return nil }
// Nodes implements part of the xrefs.Service interface. func (db *DB) Nodes(req *xpb.NodesRequest) (*xpb.NodesReply, error) { reply := &xpb.NodesReply{} patterns := xrefs.ConvertFilters(req.Filter) for _, t := range req.Ticket { uri, err := kytheuri.Parse(t) if err != nil { return nil, fmt.Errorf("invalid ticket %q: %v", t, err) } rows, err := db.nodeFactsStmt.Query(uri.Signature, uri.Corpus, uri.Root, uri.Language, uri.Path) if err != nil { return nil, fmt.Errorf("sql select error for node %q: %v", t, err) } var facts []*xpb.Fact for rows.Next() { var fact xpb.Fact err = rows.Scan(&fact.Name, &fact.Value) if err != nil { rows.Close() return nil, fmt.Errorf("sql scan error for node %q: %v", t, err) } if len(patterns) == 0 || xrefs.MatchesAny(fact.Name, patterns) { facts = append(facts, &fact) } } if err := rows.Close(); err != nil { return nil, fmt.Errorf("error closing rows for node %q: %v", t, err) } if len(facts) != 0 { reply.Node = append(reply.Node, &xpb.NodeInfo{ Ticket: t, Fact: facts, }) } } return reply, nil }
func completeDefinition(definesAnchor string) (*definition, error) { parentReply, err := xs.Edges(ctx, &xpb.EdgesRequest{ Ticket: []string{definesAnchor}, Kind: []string{schema.ChildOfEdge}, Filter: []string{schema.NodeKindFact, schema.AnchorLocFilter}, }) if err != nil { return nil, err } parentNodes := xrefs.NodesMap(parentReply.Node) var files []string for _, parent := range xrefs.EdgesMap(parentReply.EdgeSet)[definesAnchor][schema.ChildOfEdge] { if string(parentNodes[parent][schema.NodeKindFact]) == schema.FileKind { files = append(files, parent) } } if len(files) == 0 { return nil, nil } else if len(files) > 1 { return nil, fmt.Errorf("anchor has multiple file parents %q: %v", definesAnchor, files) } vName, err := kytheuri.Parse(files[0]) if err != nil { return nil, err } start, end := parseAnchorSpan(parentNodes[definesAnchor]) return &definition{ File: vName.VName(), Start: start, End: end, }, nil }
func main() { flag.Parse() if *offset < 0 { flagutil.UsageError("non-negative --offset required") } else if *signature == "" && *path == "" { flagutil.UsageError("must provide at least --path or --signature") } if strings.HasPrefix(*remoteAPI, "http://") || strings.HasPrefix(*remoteAPI, "https://") { xs = xrefs.WebClient(*remoteAPI) idx = search.WebClient(*remoteAPI) } else { conn, err := grpc.Dial(*remoteAPI) if err != nil { log.Fatalf("Error connecting to remote API %q: %v", *remoteAPI, err) } defer conn.Close() xs = xrefs.GRPC(xpb.NewXRefServiceClient(conn)) idx = search.GRPC(spb.NewSearchServiceClient(conn)) } relPath := *path if !*ignoreLocalRepo { if _, err := os.Stat(relPath); err == nil { absPath, err := filepath.Abs(relPath) if err != nil { log.Fatal(err) } kytheRoot := findKytheRoot(filepath.Dir(absPath)) if kytheRoot != "" { relPath, err = filepath.Rel(filepath.Join(kytheRoot, *root), absPath) if err != nil { log.Fatal(err) } } } } partialFile := &spb.VName{ Signature: *signature, Corpus: *corpus, Root: *root, Path: relPath, Language: *language, } reply, err := idx.Search(ctx, &spb.SearchRequest{ Partial: partialFile, Fact: fileFacts, }) if err != nil { log.Fatalf("Error locating file {%v}: %v", partialFile, err) } if len(reply.Ticket) == 0 { log.Fatalf("Could not locate file {%v}", partialFile) } else if len(reply.Ticket) > 1 { log.Fatalf("Ambiguous file {%v}; multiple results: %v", partialFile, reply.Ticket) } fileTicket := reply.Ticket[0] decor, err := xs.Decorations(ctx, &xpb.DecorationsRequest{ // TODO(schroederc): limit Location to a SPAN around *offset Location: &xpb.Location{Ticket: fileTicket}, References: true, SourceText: true, DirtyBuffer: readDirtyBuffer(ctx), }) if err != nil { log.Fatal(err) } nodes := xrefs.NodesMap(decor.Node) en := json.NewEncoder(os.Stdout) for _, ref := range decor.Reference { start, end := parseAnchorSpan(nodes[ref.SourceTicket]) if start <= *offset && *offset < end { var r reference r.Span.Start = start r.Span.End = end r.Span.Text = string(decor.SourceText[start:end]) r.Kind = strings.TrimPrefix(ref.Kind, schema.EdgePrefix) r.Node.Ticket = ref.TargetTicket node := nodes[ref.TargetTicket] r.Node.Kind = string(node[schema.NodeKindFact]) r.Node.Subkind = string(node[schema.SubkindFact]) if eReply, err := xs.Edges(ctx, &xpb.EdgesRequest{ Ticket: []string{ref.TargetTicket}, Kind: []string{schema.NamedEdge, definedAtEdge}, }); err != nil { log.Printf("WARNING: error getting edges for %q: %v", ref.TargetTicket, err) } else { edges := xrefs.EdgesMap(eReply.EdgeSet)[ref.TargetTicket] for _, name := range edges[schema.NamedEdge] { if uri, err := kytheuri.Parse(name); err != nil { log.Printf("WARNING: named node ticket (%q) could not be parsed: %v", name, err) } else { r.Node.Names = append(r.Node.Names, uri.Signature) } } if !*skipDefinitions { for _, defAnchor := range edges[definedAtEdge] { def, err := completeDefinition(defAnchor) if err != nil { log.Printf("WARNING: failed to complete definition for %q: %v", defAnchor, err) } else { r.Node.Definitions = append(r.Node.Definitions, def) } } } } if err := en.Encode(r); err != nil { log.Fatal(err) } } } }
func main() { flag.Parse() if flag.NArg() > 0 { flagutil.UsageErrorf("unknown non-flag argument(s): %v", flag.Args()) } else if *offset < 0 && (*lineNumber < 0 || *columnOffset < 0) { flagutil.UsageError("non-negative --offset (or --line and --column) required") } else if *signature == "" && *path == "" { flagutil.UsageError("must provide at least --path or --signature") } defer (*apiFlag).Close() xs, idx = *apiFlag, *apiFlag relPath := *path if *localRepoRoot != "NONE" { if _, err := os.Stat(relPath); err == nil { absPath, err := filepath.Abs(relPath) if err != nil { log.Fatal(err) } if *dirtyBuffer == "" { *dirtyBuffer = absPath } kytheRoot := *localRepoRoot if kytheRoot == "" { kytheRoot = findKytheRoot(filepath.Dir(absPath)) } if kytheRoot != "" { relPath, err = filepath.Rel(filepath.Join(kytheRoot, *root), absPath) if err != nil { log.Fatal(err) } } } } partialFile := &spb.VName{ Signature: *signature, Corpus: *corpus, Root: *root, Path: relPath, Language: *language, } reply, err := idx.Search(ctx, &spb.SearchRequest{ Partial: partialFile, Fact: fileFacts, }) if err != nil { log.Fatalf("Error locating file {%v}: %v", partialFile, err) } if len(reply.Ticket) == 0 { log.Fatalf("Could not locate file {%v}", partialFile) } else if len(reply.Ticket) > 1 { log.Fatalf("Ambiguous file {%v}; multiple results: %v", partialFile, reply.Ticket) } fileTicket := reply.Ticket[0] text := readDirtyBuffer(ctx) decor, err := xs.Decorations(ctx, &xpb.DecorationsRequest{ // TODO(schroederc): limit Location to a SPAN around *offset Location: &xpb.Location{Ticket: fileTicket}, References: true, SourceText: true, DirtyBuffer: text, Filter: []string{ schema.NodeKindFact, schema.SubkindFact, schema.AnchorLocFilter, // TODO(schroederc): remove once backwards-compatibility fix below is removed }, }) if err != nil { log.Fatal(err) } if text == nil { text = decor.SourceText } nodes := xrefs.NodesMap(decor.Node) // Normalize point within source text point := normalizedPoint(text) en := json.NewEncoder(os.Stdout) for _, ref := range decor.Reference { var start, end int if ref.AnchorStart == nil || ref.AnchorEnd == nil { // TODO(schroederc): remove this backwards-compatibility fix start, end = parseAnchorSpan(nodes[ref.SourceTicket]) } else { start, end = int(ref.AnchorStart.ByteOffset), int(ref.AnchorEnd.ByteOffset) } if start <= point && point < end { var r reference r.Span.Start = start r.Span.End = end r.Span.Text = string(text[start:end]) r.Kind = strings.TrimPrefix(ref.Kind, schema.EdgePrefix) r.Node.Ticket = ref.TargetTicket node := nodes[ref.TargetTicket] r.Node.Kind = string(node[schema.NodeKindFact]) r.Node.Subkind = string(node[schema.SubkindFact]) if eReply, err := xrefs.AllEdges(ctx, xs, &xpb.EdgesRequest{ Ticket: []string{ref.TargetTicket}, Kind: []string{schema.NamedEdge, schema.TypedEdge, definedAtEdge, definedBindingAtEdge}, }); err != nil { log.Printf("WARNING: error getting edges for %q: %v", ref.TargetTicket, err) } else { edges := xrefs.EdgesMap(eReply.EdgeSet)[ref.TargetTicket] for _, name := range edges[schema.NamedEdge] { if uri, err := kytheuri.Parse(name); err != nil { log.Printf("WARNING: named node ticket (%q) could not be parsed: %v", name, err) } else { r.Node.Names = append(r.Node.Names, uri.Signature) } } if typed := edges[schema.TypedEdge]; len(typed) > 0 { r.Node.Typed = typed[0] } if !*skipDefinitions { defs := edges[definedAtEdge] if len(defs) == 0 { defs = edges[definedBindingAtEdge] } for _, defAnchor := range defs { def, err := completeDefinition(defAnchor) if err != nil { log.Printf("WARNING: failed to complete definition for %q: %v", defAnchor, err) } else { r.Node.Definitions = append(r.Node.Definitions, def) } } } } if err := en.Encode(r); err != nil { log.Fatal(err) } } } }
func main() { flag.Parse() if flag.NArg() > 0 { flagutil.UsageErrorf("unknown non-flag argument(s): %v", flag.Args()) } else if *offset < 0 && (*lineNumber < 0 || *columnOffset < 0) { flagutil.UsageError("non-negative --offset (or --line and --column) required") } else if *path == "" { flagutil.UsageError("must provide --path") } defer (*apiFlag).Close() xs = *apiFlag relPath := *path if *localRepoRoot != "NONE" { if _, err := os.Stat(relPath); err == nil { absPath, err := filepath.Abs(relPath) if err != nil { log.Fatal(err) } if *dirtyBuffer == "" { *dirtyBuffer = absPath } kytheRoot := *localRepoRoot if kytheRoot == "" { kytheRoot = findKytheRoot(filepath.Dir(absPath)) } if kytheRoot != "" { relPath, err = filepath.Rel(filepath.Join(kytheRoot, *root), absPath) if err != nil { log.Fatal(err) } } } } fileTicket := (&kytheuri.URI{Corpus: *corpus, Root: *root, Path: relPath}).String() point := &xpb.Location_Point{ ByteOffset: int32(*offset), LineNumber: int32(*lineNumber), ColumnOffset: int32(*columnOffset), } dirtyBuffer := readDirtyBuffer(ctx) decor, err := xs.Decorations(ctx, &xpb.DecorationsRequest{ Location: &xpb.Location{ Ticket: fileTicket, Kind: xpb.Location_SPAN, Start: point, End: point, }, SpanKind: xpb.DecorationsRequest_AROUND_SPAN, References: true, SourceText: true, DirtyBuffer: dirtyBuffer, Filter: []string{ facts.NodeKind, facts.Subkind, }, }) if err != nil { log.Fatal(err) } nodes := xrefs.NodesMap(decor.Nodes) en := json.NewEncoder(os.Stdout) for _, ref := range decor.Reference { start, end := int(ref.AnchorStart.ByteOffset), int(ref.AnchorEnd.ByteOffset) var r reference r.Span.Start = start r.Span.End = end if len(dirtyBuffer) > 0 { r.Span.Text = string(dirtyBuffer[start:end]) } // TODO(schroederc): add option to get anchor text from DecorationsReply r.Kind = strings.TrimPrefix(ref.Kind, edges.Prefix) r.Node.Ticket = ref.TargetTicket node := nodes[ref.TargetTicket] r.Node.Kind = string(node[facts.NodeKind]) r.Node.Subkind = string(node[facts.Subkind]) // TODO(schroederc): use CrossReferences method if eReply, err := xrefs.AllEdges(ctx, xs, &gpb.EdgesRequest{ Ticket: []string{ref.TargetTicket}, Kind: []string{edges.Named, edges.Typed, definedAtEdge, definedBindingAtEdge}, }); err != nil { log.Printf("WARNING: error getting edges for %q: %v", ref.TargetTicket, err) } else { matching := xrefs.EdgesMap(eReply.EdgeSets)[ref.TargetTicket] for name := range matching[edges.Named] { if uri, err := kytheuri.Parse(name); err != nil { log.Printf("WARNING: named node ticket (%q) could not be parsed: %v", name, err) } else { r.Node.Names = append(r.Node.Names, uri.Signature) } } for typed := range matching[edges.Typed] { r.Node.Typed = typed break } if !*skipDefinitions { defs := matching[definedAtEdge] if len(defs) == 0 { defs = matching[definedBindingAtEdge] } for defAnchor := range defs { def, err := completeDefinition(defAnchor) if err != nil { log.Printf("WARNING: failed to complete definition for %q: %v", defAnchor, err) } else { r.Node.Definitions = append(r.Node.Definitions, def) } } } } if err := en.Encode(r); err != nil { log.Fatal(err) } } }
return errors.New("--files and --dirs are mutually exclusive") } if len(flag.Args()) == 0 { req := &ftpb.CorpusRootsRequest{} logRequest(req) cr, err := ft.CorpusRoots(ctx, req) if err != nil { return err } return displayCorpusRoots(cr) } var corpus, root, path string switch len(flag.Args()) { case 1: uri, err := kytheuri.Parse(flag.Arg(0)) if err != nil { log.Fatalf("invalid uri %q: %v", flag.Arg(0), err) } corpus = uri.Corpus root = uri.Root path = uri.Path default: flag.Usage() os.Exit(1) } path = filepath.Join("/", path) req := &ftpb.DirectoryRequest{ Corpus: corpus, Root: root, Path: path,
// Edges implements part of the xrefs.Service interface. func (db *DB) Edges(req *xpb.EdgesRequest) (*xpb.EdgesReply, error) { if req.PageSize != 0 || req.PageToken != "" { return nil, errors.New("edge pages unimplemented for SQL DB") } reply := &xpb.EdgesReply{} allowedKinds := stringset.New(req.Kind...) nodeTickets := stringset.New() for _, t := range req.Ticket { uri, err := kytheuri.Parse(t) if err != nil { return nil, err } edges := make(map[string]stringset.Set) var ( target kytheuri.URI kind string ) rows, err := db.edgesStmt.Query(uri.Signature, uri.Corpus, uri.Root, uri.Path, uri.Language) if err != nil { rows.Close() return nil, fmt.Errorf("forward edges query error: %v", err) } for rows.Next() { if err := rows.Scan(&target.Signature, &target.Corpus, &target.Root, &target.Path, &target.Language, &kind); err != nil { rows.Close() return nil, fmt.Errorf("forward edges scan error: %v", err) } else if len(allowedKinds) != 0 && !allowedKinds.Contains(kind) { continue } targets, ok := edges[kind] if !ok { targets = stringset.New() edges[kind] = targets } ticket := target.String() targets.Add(ticket) nodeTickets.Add(ticket) } if err := rows.Close(); err != nil { return nil, err } rows, err = db.revEdgesStmt.Query(uri.Signature, uri.Corpus, uri.Root, uri.Path, uri.Language) if err != nil { rows.Close() return nil, fmt.Errorf("reverse edges query error: %v", err) } for rows.Next() { if err := rows.Scan(&target.Signature, &target.Corpus, &target.Root, &target.Path, &target.Language, &kind); err != nil { rows.Close() return nil, fmt.Errorf("reverse edges scan error: %v", err) } kind = schema.MirrorEdge(kind) if len(allowedKinds) != 0 && !allowedKinds.Contains(kind) { continue } targets, ok := edges[kind] if !ok { targets = stringset.New() edges[kind] = targets } ticket := target.String() targets.Add(ticket) nodeTickets.Add(ticket) } if err := rows.Close(); err != nil { return nil, err } var g []*xpb.EdgeSet_Group for kind, targets := range edges { g = append(g, &xpb.EdgeSet_Group{ Kind: kind, TargetTicket: targets.Slice(), }) } if len(g) != 0 { reply.EdgeSet = append(reply.EdgeSet, &xpb.EdgeSet{ SourceTicket: t, Group: g, }) } } nodesReply, err := db.Nodes(&xpb.NodesRequest{ Ticket: nodeTickets.Slice(), Filter: req.Filter, }) if err != nil { return nil, fmt.Errorf("nodes request error: %v", err) } reply.Node = nodesReply.Node return reply, nil }
// Decorations implements part of the xrefs.Service interface. func (db *DB) Decorations(req *xpb.DecorationsRequest) (*xpb.DecorationsReply, error) { if req.GetLocation() == nil { return nil, errors.New("missing location") } else if req.Location.Kind != xpb.Location_FILE { return nil, fmt.Errorf("%s location kind unimplemented", req.Location.Kind) } else if len(req.DirtyBuffer) != 0 { return nil, errors.New("dirty buffers unimplemented") } fileTicket := req.Location.Ticket edgesReply, err := db.Edges(&xpb.EdgesRequest{ Ticket: []string{fileTicket}, Kind: []string{revChildOfEdge}, Filter: []string{schema.NodeKindFact, schema.TextFact, schema.TextEncodingFact}, }) if err != nil { return nil, err } reply := &xpb.DecorationsReply{ Location: &xpb.Location{ Ticket: fileTicket, }, } nodes := xrefs.NodesMap(edgesReply.Node) if req.SourceText { if nodes[fileTicket] == nil { nodesReply, err := db.Nodes(&xpb.NodesRequest{Ticket: []string{fileTicket}}) if err != nil { return nil, err } nodes = xrefs.NodesMap(nodesReply.Node) } reply.SourceText = nodes[fileTicket][schema.TextFact] reply.Encoding = string(nodes[fileTicket][schema.TextEncodingFact]) } nodeTickets := stringset.New() if req.References { // Traverse the following chain of edges: // file --%/kythe/edge/childof-> []anchor --forwardEdgeKind-> []target edges := xrefs.EdgesMap(edgesReply.EdgeSet) for _, anchor := range edges[fileTicket][revChildOfEdge] { if string(nodes[anchor][schema.NodeKindFact]) != schema.AnchorKind { continue } uri, err := kytheuri.Parse(anchor) if err != nil { return nil, fmt.Errorf("invalid anchor ticket found %q: %v", anchor, err) } rows, err := db.anchorEdgesStmt.Query(schema.ChildOfEdge, uri.Signature, uri.Corpus, uri.Root, uri.Path, uri.Language) if err != nil { return nil, fmt.Errorf("anchor %q edge query error: %v", anchor, err) } for rows.Next() { var target kytheuri.URI var kind string if err := rows.Scan(&target.Signature, &target.Corpus, &target.Root, &target.Path, &target.Language, &kind); err != nil { return nil, fmt.Errorf("anchor %q edge scan error: %v", anchor, err) } ticket := target.String() reply.Reference = append(reply.Reference, &xpb.DecorationsReply_Reference{ SourceTicket: anchor, Kind: kind, TargetTicket: ticket, }) nodeTickets.Add(anchor, ticket) } } } nodesReply, err := db.Nodes(&xpb.NodesRequest{Ticket: nodeTickets.Slice()}) if err != nil { return nil, err } reply.Node = nodesReply.Node return reply, nil }