diff --git a/chi_util.go b/chi_util.go new file mode 100644 index 000000000..96e1f9d59 --- /dev/null +++ b/chi_util.go @@ -0,0 +1,54 @@ +package main + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi" + "github.com/go-chi/render" +) + +// taken from +// - https://github.com/go-chi/chi/blob/cca4135d8dddff765463feaf1118047a9e506b4a/_examples/fileserver/main.go +// - https://github.com/go-chi/chi/blob/cca4135d8dddff765463feaf1118047a9e506b4a/_examples/rest/main.go + +func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { + render.Status(r, e.HTTPStatusCode) + return nil +} + +type ErrResponse struct { + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code + + StatusText string `json:"status"` // user-level status message + AppCode int64 `json:"code,omitempty"` // application-specific error code + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging +} + +func ErrRender(err error) render.Renderer { + return &ErrResponse{ + Err: err, + HTTPStatusCode: 422, + StatusText: "Error rendering response.", + ErrorText: err.Error(), + } +} + +func FileServer(r chi.Router, path string, root http.FileSystem) { + if strings.ContainsAny(path, "{}*") { + panic("FileServer does not permit URL parameters.") + } + + fs := http.StripPrefix(path, http.FileServer(root)) + + if path != "/" && path[len(path)-1] != '/' { + r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) + path += "/" + } + path += "*" + + r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + })) +} diff --git a/cmd_db.go b/cmd_db.go index be129a963..0b72c76ff 100644 --- a/cmd_db.go +++ b/cmd_db.go @@ -12,8 +12,6 @@ import ( ) type dbOptions struct { - Path string `mapstructure:"dbpath"` - Verbose bool } func (opts dbOptions) String() string { @@ -22,7 +20,6 @@ func (opts dbOptions) String() string { } func dbSetupFlags(flags *pflag.FlagSet, opts *dbOptions) { - viper.BindPFlags(flags) } diff --git a/cmd_run.go b/cmd_run.go index cde429fc2..525d904fb 100644 --- a/cmd_run.go +++ b/cmd_run.go @@ -20,19 +20,13 @@ type runOptions struct { ReposToFetch []string // db - DBOpts dbOptions + DBOpts dbOptions + GraphOpts graphOptions // run - ShowClosed bool `mapstructure:"show-closed"` - ShowOrphans bool AdditionalPulls []string - EpicLabel string Destination string - DebugGraph bool - NoCompress bool - DarkTheme bool - Targets []string //Preview bool } @@ -43,12 +37,6 @@ func (opts runOptions) String() string { func runSetupFlags(flags *pflag.FlagSet, opts *runOptions) { flags.BoolVarP(&opts.NoPull, "no-pull", "f", false, "do not pull new issues before runing") - flags.BoolVarP(&opts.ShowClosed, "show-closed", "", false, "show closed issues") - flags.BoolVarP(&opts.DebugGraph, "debug-graph", "", false, "debug graph") - flags.BoolVarP(&opts.ShowOrphans, "show-orphans", "", false, "show issues not linked to an epic") - flags.BoolVarP(&opts.NoCompress, "no-compress", "", false, "do not compress graph (no overlap)") - flags.BoolVarP(&opts.DarkTheme, "dark-theme", "", false, "dark theme") - flags.StringVarP(&opts.EpicLabel, "epic-label", "", "epic", "label used for epics (empty means issues with dependencies but without dependants)") flags.StringVarP(&opts.Destination, "destination", "", "-", "destination ('-' for stdout)") flags.StringSliceVarP(&opts.AdditionalPulls, "additional-pull", "", []string{}, "additional pull that won't necessarily be displayed on the graph") //flags.BoolVarP(&opts.Preview, "preview", "p", false, "preview result") @@ -69,9 +57,12 @@ func newRunCommand() *cobra.Command { if err := viper.Unmarshal(&opts.DBOpts); err != nil { return err } + if err := viper.Unmarshal(&opts.GraphOpts); err != nil { + return err + } opts.PullOpts.DBOpts = opts.DBOpts opts.PullOpts.Targets = append(args, opts.AdditionalPulls...) - opts.Targets = args + opts.GraphOpts.Targets = args return run(opts) }, } @@ -81,21 +72,17 @@ func newRunCommand() *cobra.Command { return cmd } -func run(opts *runOptions) error { - logger().Debug("run", zap.Stringer("opts", *opts)) - if !opts.NoPull { - if err := pull(&opts.PullOpts); err != nil { - return err - } +func graphviz(opts *graphOptions) (string, error) { + if opts.Targets == nil || len(opts.Targets) < 1 || opts.Targets[0] == "" { + return "", fmt.Errorf("you need to specify at least one target") } - issues, err := loadIssues(db, nil) if err != nil { - return errors.Wrap(err, "failed to load issues") + return "", errors.Wrap(err, "failed to load issues") } if err := issues.prepare(); err != nil { - return errors.Wrap(err, "failed to prepare issues") + return "", errors.Wrap(err, "failed to prepare issues") } if !opts.ShowClosed { @@ -106,7 +93,18 @@ func run(opts *runOptions) error { logger().Warn("--show-orphans is deprecated and will be removed") } - out, err := graphviz(issues, opts) + return graphvizRender(issues, opts) +} + +func run(opts *runOptions) error { + logger().Debug("run", zap.Stringer("opts", *opts)) + if !opts.NoPull { + if err := pull(&opts.PullOpts); err != nil { + return err + } + } + + out, err := graphviz(&opts.GraphOpts) if err != nil { return err } diff --git a/cmd_web.go b/cmd_web.go new file mode 100644 index 000000000..e9755dacc --- /dev/null +++ b/cmd_web.go @@ -0,0 +1,153 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/docgen" + "github.com/go-chi/render" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +type webOptions struct { + // web specific + Bind string + ShowRoutes bool + + // db + DBOpts dbOptions +} + +func (opts webOptions) String() string { + out, _ := json.Marshal(opts) + return string(out) +} + +func webSetupFlags(flags *pflag.FlagSet, opts *webOptions) { + flags.StringVarP(&opts.Bind, "bind", "b", ":2020", "web server bind address") + flags.BoolVarP(&opts.ShowRoutes, "show-routes", "", false, "display available routes and quit") + viper.BindPFlags(flags) +} + +func newWebCommand() *cobra.Command { + opts := &webOptions{} + cmd := &cobra.Command{ + Use: "web", + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.Unmarshal(opts); err != nil { + return err + } + if err := viper.Unmarshal(&opts.DBOpts); err != nil { + return err + } + return web(opts) + }, + } + webSetupFlags(cmd.Flags(), opts) + dbSetupFlags(cmd.Flags(), &opts.DBOpts) + return cmd +} + +func (i *Issue) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +func webListIssues(w http.ResponseWriter, r *http.Request) { + issues, err := loadIssues(db, nil) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + list := []render.Renderer{} + for _, issue := range issues { + list = append(list, issue) + } + + if err := render.RenderList(w, r, list); err != nil { + render.Render(w, r, ErrRender(err)) + return + } +} + +func webGraphviz(r *http.Request) (string, error) { + opts := &graphOptions{ + Targets: strings.Split(r.URL.Query().Get("targets"), ","), + ShowClosed: r.URL.Query().Get("show-closed") == "1", + } + return graphviz(opts) +} + +func webDotIssues(w http.ResponseWriter, r *http.Request) { + out, err := webGraphviz(r) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + w.Write([]byte(out)) +} + +func webImageIssues(w http.ResponseWriter, r *http.Request) { + out, err := webGraphviz(r) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + cmd := exec.Command("dot", "-Tsvg") + cmd.Stdin = bytes.NewBuffer([]byte(out)) + cmd.Stdout = w + + if err := cmd.Run(); err != nil { + render.Render(w, r, ErrRender(err)) + return + } +} + +func web(opts *webOptions) error { + r := chi.NewRouter() + + //r.Use(middleware.RequestID) + //r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + //r.Use(middleware.URLFormat) + r.Use(middleware.Timeout(5 * time.Second)) + + r.Route("/api", func(r chi.Router) { + r.Route("/", func(r chi.Router) { + r.Use(render.SetContentType(render.ContentTypeJSON)) + r.Get("/issues.json", webListIssues) + }) + r.Get("/graph/dot", webDotIssues) + r.Get("/graph/image", webImageIssues) + }) + + workDir, _ := os.Getwd() + filesDir := filepath.Join(workDir, "web") + FileServer(r, "/", http.Dir(filesDir)) + + if opts.ShowRoutes { + fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{ + ProjectPath: "moul.io/depviz", + Intro: "Welcome to depviz generated docs.", + })) + return nil + } + + log.Printf("Listening on %s", opts.Bind) + return http.ListenAndServe(opts.Bind, r) +} diff --git a/go.mod b/go.mod index 67ea29a35..77b2a72bd 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,23 @@ module moul.io/depviz require ( + cloud.google.com/go v0.29.0 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect github.com/awalterschulze/gographviz v0.0.0-20180927133620-e69668a01397 + github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect + github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect + github.com/go-chi/chi v3.3.3+incompatible + github.com/go-chi/docgen v1.0.2 // indirect + github.com/go-chi/render v1.0.1 + github.com/go-sql-driver/mysql v1.4.0 // indirect + github.com/google/go-cmp v0.2.0 // indirect github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jinzhu/gorm v1.9.1 github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect + github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae // indirect + github.com/lib/pq v1.0.0 // indirect github.com/mattn/go-sqlite3 v1.9.0 github.com/pkg/errors v0.8.0 github.com/spf13/cobra v0.0.3 @@ -13,7 +25,10 @@ require ( github.com/spf13/viper v1.2.1 github.com/xanzy/go-gitlab v0.11.1 go.uber.org/zap v1.9.1 + golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824 // indirect golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect + google.golang.org/appengine v1.2.0 // indirect moul.io/zapgorm v0.0.0-20181003053625-c808c1c4adc6 ) diff --git a/go.sum b/go.sum index e3e4fc263..32e6b5045 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,45 @@ +cloud.google.com/go v0.29.0 h1:gv/9Wwq5WPVIGaROMQg8tw4jLFFiyacODxEIrlz0wTw= +cloud.google.com/go v0.29.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/awalterschulze/gographviz v0.0.0-20180927133620-e69668a01397 h1:SzTF/aqwdSBijZUpXvw1ditdkwfU+ONMyocC2CyRtrM= github.com/awalterschulze/gographviz v0.0.0-20180927133620-e69668a01397/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 h1:BZGp1dbKFjqlGmxEpwkDpCWNxVwEYnUPoncIzLiHlPo= +github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8= +github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/docgen v1.0.2 h1:BL7Y/SQlZMlhEI8dgukaEvF0AqdqG7axNdJsUVAzbRE= +github.com/go-chi/docgen v1.0.2/go.mod h1:n7Wqcp0XCeIb/IHrd6hxqtFJzCklt0pKeo7uVUXkrdY= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA= github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae h1:8bBMcboXYVuo0WYH+rPe5mB8obO89a993hdTZ3phTjc= +github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= @@ -23,6 +50,7 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -37,6 +65,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M= github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/xanzy/go-gitlab v0.11.1 h1:kgVxG9YFerbzMnLuwcFio5nwdv/5bCDHgm7WviT+EEE= github.com/xanzy/go-gitlab v0.11.1/go.mod h1:CRKHkvFWNU6C3AEfqLWjnCNnAs4nj8Zk95rX2S3X6Mw= @@ -46,14 +75,22 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc= +golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824 h1:MkjFNbaZJyH98M67Q3umtwZ+EdVdrNJLqSwZp5vcv60= golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced h1:4oqSq7eft7MdPKBGQK11X9WYUxmj6ZLgGTqYIbY1kyw= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/graphviz.go b/graphviz.go index e6e36eb28..afbba1056 100644 --- a/graphviz.go +++ b/graphviz.go @@ -6,10 +6,32 @@ import ( "sort" "github.com/awalterschulze/gographviz" + "github.com/spf13/pflag" + "github.com/spf13/viper" "go.uber.org/zap" ) -func graphviz(issues Issues, opts *runOptions) (string, error) { +type graphOptions struct { + DebugGraph bool `mapstructure:"debug-graph"` + NoCompress bool `mapstructure:"no-compress"` + DarkTheme bool `mapstructure:"dark-theme"` + ShowClosed bool `mapstructure:"show-closed"` + ShowOrphans bool `mapstructure:"show-orphans"` + EpicLabel string `mapstructure:"epic-label"` + Targets []string +} + +func graphSetupFlags(flags *pflag.FlagSet, opts *graphOptions) { + flags.BoolVarP(&opts.ShowClosed, "show-closed", "", false, "show closed issues") + flags.BoolVarP(&opts.DebugGraph, "debug-graph", "", false, "debug graph") + flags.BoolVarP(&opts.ShowOrphans, "show-orphans", "", false, "show issues not linked to an epic") + flags.BoolVarP(&opts.NoCompress, "no-compress", "", false, "do not compress graph (no overlap)") + flags.BoolVarP(&opts.DarkTheme, "dark-theme", "", false, "dark theme") + flags.StringVarP(&opts.EpicLabel, "epic-label", "", "epic", "label used for epics (empty means issues with dependencies but without dependants)") + viper.BindPFlags(flags) +} + +func graphvizRender(issues Issues, opts *graphOptions) (string, error) { var ( stats = map[string]int{ "nodes": 0, diff --git a/issue.go b/issue.go index 9fb04342f..f58bd154d 100644 --- a/issue.go +++ b/issue.go @@ -306,8 +306,16 @@ func (i Issue) blocksAnEpic(depth int) bool { } func (i Issue) DependsOnAnEpic() bool { + return i.dependsOnAnEpic(0) +} + +func (i Issue) dependsOnAnEpic(depth int) bool { + if depth > 100 { + log.Printf("very high blocking depth (>100), do not continue. (issue=%s)", i) + return false + } for _, dep := range i.DependsOn { - if dep.IsEpic() || dep.DependsOnAnEpic() { + if dep.IsEpic() || dep.dependsOnAnEpic(depth+1) { return true } } diff --git a/main.go b/main.go index 208e87e10..3f1d90563 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,7 @@ func newRootCommand() *cobra.Command { newPullCommand(), newRunCommand(), newDBCommand(), + newWebCommand(), ) viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) diff --git a/web/index.html b/web/index.html new file mode 100644 index 000000000..17cf20914 --- /dev/null +++ b/web/index.html @@ -0,0 +1 @@ +berty/berty