Skip to content

Commit

Permalink
feat(tooling): import analysis (grafana#10)
Browse files Browse the repository at this point in the history
Adds a tool (tk tool imports) to get a list of all files, a file transitively imports.

Optionally allows to check whether a git commit changed one of those imports.

BREAKING: tk debug jpath is now tk tool jpath
  • Loading branch information
sh0rez authored Aug 7, 2019
1 parent 283b34a commit ce2b0d3
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 49 deletions.
45 changes: 0 additions & 45 deletions cmd/tk/debug.go

This file was deleted.

2 changes: 1 addition & 1 deletion cmd/tk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func main() {
rootCmd.AddCommand(
evalCmd(),
initCmd(),
debugCmd(),
toolCmd(),
)

// completion
Expand Down
137 changes: 137 additions & 0 deletions cmd/tk/tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/spf13/cobra"

"github.com/sh0rez/tanka/pkg/jpath"
"github.com/sh0rez/tanka/pkg/jsonnet"
)

func toolCmd() *cobra.Command {
cmd := &cobra.Command{
Short: "handy utilities for working with jsonnet",
Use: "tool",
}
cmd.AddCommand(jpathCmd())
cmd.AddCommand(importsCmd())
return cmd
}

func jpathCmd() *cobra.Command {
cmd := &cobra.Command{
Short: "print information about the jpath",
Use: "jpath",
RunE: func(cmd *cobra.Command, args []string) error {
pwd, err := os.Getwd()
if err != nil {
return err
}
path, base, root, err := jpath.Resolve(pwd)
if err != nil {
log.Fatalln("Resolving JPATH:", err)
}
fmt.Println("main:", filepath.Join(base, "main.jsonnet"))
fmt.Println("rootDir:", root)
fmt.Println("baseDir:", base)
fmt.Println("jpath:", path)
return nil
},
}
return cmd
}

func importsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "imports [file]",
Short: "list all transitive imports of a file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var modFiles []string
if cmd.Flag("check").Changed {
var err error
modFiles, err = gitChangedFiles(cmd.Flag("check").Value.String())
if err != nil {
log.Fatalln("invoking git:", err)
}
}

f, err := filepath.Abs(args[0])
if err != nil {
log.Fatalln("Opening file:", err)
}

deps, err := jsonnet.TransitiveImports(f)
if err != nil {
log.Fatalln("resolving imports:", err)
}

// include main.jsonnet as well
deps = append(deps, f)

root, err := gitRoot()
if err != nil {
log.Fatalln("invoking git:", err)
}
if modFiles != nil {
for _, m := range modFiles {
mod := filepath.Join(root, m)
if err != nil {
log.Fatalln(err)
}

for _, dep := range deps {
if mod == dep {
fmt.Printf("Rebuild required. File `%s` imports `%s`, which has been changed in `%s`.\n", args[0], dep, cmd.Flag("check").Value.String())
os.Exit(16)
}
}
}
fmt.Printf("Rebuild not required, because no imported files have been changed in `%s`.\n", cmd.Flag("check").Value.String())
os.Exit(0)
}

s, err := json.Marshal(deps)
if err != nil {
log.Fatalln("Formatting:", err)
}
fmt.Println(string(s))
},
}

cmd.Flags().StringP("check", "c", "", "git commit hash to check against")

return cmd
}

func gitRoot() (string, error) {
s, err := git("rev-parse", "--show-toplevel")
return strings.TrimRight(s, "\n"), err
}

func gitChangedFiles(sha string) ([]string, error) {
f, err := git("diff-tree", "--no-commit-id", "--name-only", "-r", sha)
if err != nil {
return nil, err
}
return strings.Split(f, "\n"), nil
}

func git(argv ...string) (string, error) {
cmd := exec.Command("git", argv...)
cmd.Stderr = os.Stderr
var buf bytes.Buffer
cmd.Stdout = &buf
if err := cmd.Run(); err != nil {
return "", err
}
return buf.String(), nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ require (
github.com/fatih/color v1.7.0
github.com/google/go-jsonnet v0.13.0
github.com/kr/pretty v0.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2
github.com/pkg/errors v0.8.1
github.com/posener/complete v1.2.1
github.com/sh0rez/go-jsonnet v0.14.2
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.2
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sh0rez/go-jsonnet v0.14.2 h1:xt6UJNVUR9blFgVrOrRqYwv5Qncp3xgM18fz1t2WjPQ=
github.com/sh0rez/go-jsonnet v0.14.2/go.mod h1:OC3U+HWq8EVB5nvJDJAWvgVb43HFEXc2VqCUipW9/BE=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
Expand Down Expand Up @@ -113,6 +115,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
Expand Down
58 changes: 57 additions & 1 deletion pkg/jsonnet/jsonnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"io/ioutil"
"path/filepath"

jsonnet "github.com/google/go-jsonnet"
"github.com/pkg/errors"
"github.com/sh0rez/tanka/pkg/jpath"
"github.com/sh0rez/tanka/pkg/native"

jsonnet "github.com/sh0rez/go-jsonnet"
)

// EvaluateFile opens the file, reads it into memory and evaluates it afterwards (`Evaluate()`)
Expand Down Expand Up @@ -38,3 +39,58 @@ func Evaluate(sonnet string, jpath []string) (string, error) {

return vm.EvaluateSnippet("main.jsonnet", sonnet)
}

type ImportVisitor func(who, what string) error

func VisitImportsFile(jsonnetFile string, v ImportVisitor) error {
bytes, err := ioutil.ReadFile(jsonnetFile)
if err != nil {
return err
}

jpath, _, _, err := jpath.Resolve(filepath.Dir(jsonnetFile))
if err != nil {
return err
}
return VisitImports(string(bytes), jpath, v)
}

func VisitImports(sonnet string, jpath []string, v ImportVisitor) error {
importer := TraceImporter{
JPaths: jpath,
Visitor: v,
}

vm := jsonnet.MakeVM()
vm.Importer(&importer)
for _, nf := range native.Funcs() {
vm.NativeFunction(nf)
}

// This method does not exist in google/go-jsonnet. It has been patched in sh0rez/go-jsonnet.
// Basically it aborts the evaluation after the imports are done. This is much faster (7s vs 0.5s)
if err := vm.EvaluateSnippetWithoutManifestation("main.jsonnet", sonnet); err != nil {
return err
}
return nil
}

type TraceImporter struct {
JPaths []string
Visitor ImportVisitor
importer *jsonnet.FileImporter
}

func (t *TraceImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
if t.importer == nil {
t.importer = &jsonnet.FileImporter{
JPaths: t.JPaths,
}
}

contents, foundAt, err = t.importer.Import(importedFrom, importedPath)
if err := t.Visitor(importedFrom, foundAt); err != nil {
return jsonnet.Contents{}, "", err
}
return
}
79 changes: 79 additions & 0 deletions pkg/jsonnet/transitive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package jsonnet

import (
"path/filepath"
)

// TransitiveImports returns a slice with all files this file imports plus downstream imports
func TransitiveImports(filename string) ([]string, error) {
imports := map[string][]string{}
if err := VisitImportsFile(filename, func(who, what string) error {
if imports[who] == nil {
imports[who] = []string{}
}
imports[who] = append(imports[who], what)
return nil
}); err != nil {
return nil, err
}

deps := map[string]*File{}
for k, v := range imports {
deps[k] = &File{Imports: v}
}

for _, d := range deps {
resolveTransitives(d, deps)
}

for _, d := range deps {
d.Dependencies = uniqueStringSlice(d.Dependencies)
}

return deps[filepath.Base(filename)].Dependencies, nil
}

// File represents a jsonnet file that may import other files
type File struct {
// List of files this file imports
Imports []string
// Full list of transitive imports
Dependencies []string
}

func resolveTransitives(f *File, deps map[string]*File) {
// already resolved
if len(f.Dependencies) != 0 {
return
}

for _, i := range f.Imports {
f.Dependencies = append(f.Dependencies, i)

// import has no dependencies
if deps[i] == nil {
continue
}

// import dependencies have not yet been resolved
if len(deps[i].Dependencies) == 0 {
resolveTransitives(deps[i], deps)
}

f.Dependencies = append(f.Dependencies, deps[i].Dependencies...)
}
}

func uniqueStringSlice(s []string) []string {
seen := make(map[string]struct{}, len(s))
j := 0
for _, v := range s {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
s[j] = v
j++
}
return s[:j]
}
3 changes: 2 additions & 1 deletion pkg/native/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
"regexp"
"strings"

jsonnet "github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
yaml "gopkg.in/yaml.v2"

jsonnet "github.com/sh0rez/go-jsonnet"
)

// Funcs returns a slice of native Go functions that shall be available
Expand Down

0 comments on commit ce2b0d3

Please sign in to comment.