|
@@ -0,0 +1,402 @@
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "io/ioutil"
|
|
|
+ "net/http"
|
|
|
+ _ "net/http/pprof"
|
|
|
+ "os"
|
|
|
+ "plugin"
|
|
|
+ "runtime/pprof"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ _ "unsafe"
|
|
|
+
|
|
|
+ "github.com/gogo/protobuf/proto"
|
|
|
+ "github.com/spf13/cobra"
|
|
|
+ "github.com/spf13/pflag"
|
|
|
+ "github.com/vbauerster/mpb"
|
|
|
+ "github.com/vbauerster/mpb/decor"
|
|
|
+ "golang.org/x/crypto/ssh/terminal"
|
|
|
+ "gopkg.in/src-d/go-billy.v4/osfs"
|
|
|
+ "gopkg.in/src-d/go-git.v4"
|
|
|
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
|
+ "gopkg.in/src-d/go-git.v4/storage"
|
|
|
+ "gopkg.in/src-d/go-git.v4/storage/filesystem"
|
|
|
+ "gopkg.in/src-d/go-git.v4/storage/memory"
|
|
|
+ "gopkg.in/src-d/hercules.v3"
|
|
|
+ "gopkg.in/src-d/hercules.v3/pb"
|
|
|
+)
|
|
|
+
|
|
|
+type OneLineWriter struct {
|
|
|
+ Writer io.Writer
|
|
|
+}
|
|
|
+
|
|
|
+func (writer OneLineWriter) Write(p []byte) (n int, err error) {
|
|
|
+ if p[len(p)-1] == '\n' {
|
|
|
+ p = p[:len(p)-1]
|
|
|
+ if len(p) > 5 && bytes.Compare(p[len(p)-5:], []byte("done.")) == 0 {
|
|
|
+ p = []byte("cloning...")
|
|
|
+ }
|
|
|
+ p = append(p, '\r')
|
|
|
+ writer.Writer.Write([]byte("\r" + strings.Repeat(" ", 80) + "\r"))
|
|
|
+ }
|
|
|
+ n, err = writer.Writer.Write(p)
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+func loadRepository(uri string, cachePath string, disableStatus bool) *git.Repository {
|
|
|
+ var repository *git.Repository
|
|
|
+ var backend storage.Storer
|
|
|
+ var err error
|
|
|
+ if strings.Contains(uri, "://") {
|
|
|
+ if cachePath != "" {
|
|
|
+ backend, err = filesystem.NewStorage(osfs.New(cachePath))
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ _, err = os.Stat(cachePath)
|
|
|
+ if !os.IsNotExist(err) {
|
|
|
+ fmt.Fprintf(os.Stderr, "warning: deleted %s\n", cachePath)
|
|
|
+ os.RemoveAll(cachePath)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ backend = memory.NewStorage()
|
|
|
+ }
|
|
|
+ cloneOptions := &git.CloneOptions{URL: uri}
|
|
|
+ if !disableStatus {
|
|
|
+ fmt.Fprint(os.Stderr, "connecting...\r")
|
|
|
+ cloneOptions.Progress = OneLineWriter{Writer: os.Stderr}
|
|
|
+ }
|
|
|
+ repository, err = git.Clone(backend, nil, cloneOptions)
|
|
|
+ if !disableStatus {
|
|
|
+ fmt.Fprint(os.Stderr, strings.Repeat(" ", 80)+"\r")
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if uri[len(uri)-1] == os.PathSeparator {
|
|
|
+ uri = uri[:len(uri)-1]
|
|
|
+ }
|
|
|
+ repository, err = git.PlainOpen(uri)
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ return repository
|
|
|
+}
|
|
|
+
|
|
|
+type arrayPluginFlags map[string]bool
|
|
|
+
|
|
|
+func (apf *arrayPluginFlags) String() string {
|
|
|
+ list := []string{}
|
|
|
+ for key := range *apf {
|
|
|
+ list = append(list, key)
|
|
|
+ }
|
|
|
+ return strings.Join(list, ", ")
|
|
|
+}
|
|
|
+
|
|
|
+func (apf *arrayPluginFlags) Set(value string) error {
|
|
|
+ (*apf)[value] = true
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (apf *arrayPluginFlags) Type() string {
|
|
|
+ return "path"
|
|
|
+}
|
|
|
+
|
|
|
+func loadPlugins() {
|
|
|
+ pluginFlags := arrayPluginFlags{}
|
|
|
+ fs := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
|
|
|
+ fs.SetOutput(ioutil.Discard)
|
|
|
+ pluginFlagName := "plugin"
|
|
|
+ const pluginDesc = "Load the specified plugin by the full or relative path. " +
|
|
|
+ "Can be specified multiple times."
|
|
|
+ fs.Var(&pluginFlags, pluginFlagName, pluginDesc)
|
|
|
+ pflag.Var(&pluginFlags, pluginFlagName, pluginDesc)
|
|
|
+ fs.Parse(os.Args[1:])
|
|
|
+ for path := range pluginFlags {
|
|
|
+ _, err := plugin.Open(path)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Fprintf(os.Stderr, "Failed to load plugin from %s %s\n", path, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+var rootCmd = &cobra.Command{
|
|
|
+ Use: "hercules",
|
|
|
+ Short: "Analyse a Git repository.",
|
|
|
+ Long: `Hercules is a flexible and fast Git repository analysis engine. The base command executes
|
|
|
+the commit processing pipeline which is automatically generated from the dependencies of one
|
|
|
+or several analysis targets. The list of the available targets is printed in --help. External
|
|
|
+targets can be added using the --plugin system.`,
|
|
|
+ Args: cobra.RangeArgs(1, 2),
|
|
|
+ Run: func(cmd *cobra.Command, args []string) {
|
|
|
+ flags := cmd.Flags()
|
|
|
+ commitsFile, _ := flags.GetString("commits")
|
|
|
+ protobuf, _ := flags.GetBool("pb")
|
|
|
+ profile, _ := flags.GetBool("profile")
|
|
|
+ disableStatus, _ := flags.GetBool("quiet")
|
|
|
+
|
|
|
+ if profile {
|
|
|
+ go http.ListenAndServe("localhost:6060", nil)
|
|
|
+ prof, _ := os.Create("hercules.pprof")
|
|
|
+ pprof.StartCPUProfile(prof)
|
|
|
+ defer pprof.StopCPUProfile()
|
|
|
+ }
|
|
|
+ uri := args[0]
|
|
|
+ cachePath := ""
|
|
|
+ if len(args) == 2 {
|
|
|
+ cachePath = args[1]
|
|
|
+ }
|
|
|
+ repository := loadRepository(uri, cachePath, disableStatus)
|
|
|
+
|
|
|
+
|
|
|
+ pipeline := hercules.NewPipeline(repository)
|
|
|
+ pipeline.SetFeaturesFromFlags()
|
|
|
+ var progress *mpb.Progress
|
|
|
+ var progressRendered bool
|
|
|
+ if !disableStatus {
|
|
|
+ beforeRender := func([]*mpb.Bar) {
|
|
|
+ progressRendered = true
|
|
|
+ }
|
|
|
+ progress = mpb.New(mpb.Output(os.Stderr), mpb.WithBeforeRenderFunc(beforeRender))
|
|
|
+ var bar *mpb.Bar
|
|
|
+ pipeline.OnProgress = func(commit, length int) {
|
|
|
+ if bar == nil {
|
|
|
+ width := len(strconv.Itoa(length))*2 + 3
|
|
|
+ bar = progress.AddBar(int64(length+1),
|
|
|
+ mpb.PrependDecorators(decor.DynamicName(
|
|
|
+ func(stats *decor.Statistics) string {
|
|
|
+ if stats.Current < stats.Total {
|
|
|
+ return fmt.Sprintf("%d / %d", stats.Current, length)
|
|
|
+ }
|
|
|
+ return "finalizing"
|
|
|
+ }, width, 0)),
|
|
|
+ mpb.AppendDecorators(decor.ETA(4, 0)),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ bar.Incr(commit - int(bar.Current()))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var commits []*object.Commit
|
|
|
+ if commitsFile == "" {
|
|
|
+
|
|
|
+
|
|
|
+ commits = pipeline.Commits()
|
|
|
+ } else {
|
|
|
+ var err error
|
|
|
+ commits, err = hercules.LoadCommitsFromFile(commitsFile, repository)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ cmdlineFacts["commits"] = commits
|
|
|
+ deployed := []hercules.LeafPipelineItem{}
|
|
|
+ for name, valPtr := range cmdlineDeployed {
|
|
|
+ if *valPtr {
|
|
|
+ item := pipeline.DeployItem(hercules.Registry.Summon(name)[0])
|
|
|
+ deployed = append(deployed, item.(hercules.LeafPipelineItem))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pipeline.Initialize(cmdlineFacts)
|
|
|
+ if dryRun, _ := cmdlineFacts[hercules.ConfigPipelineDryRun].(bool); dryRun {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ results, err := pipeline.Run(commits)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ if !disableStatus {
|
|
|
+ progress.Stop()
|
|
|
+ if progressRendered {
|
|
|
+
|
|
|
+ fmt.Fprint(os.Stderr, "\033[F\033[K")
|
|
|
+ }
|
|
|
+ fmt.Fprint(os.Stderr, "writing...\r")
|
|
|
+ }
|
|
|
+ if !protobuf {
|
|
|
+ printResults(uri, deployed, results)
|
|
|
+ } else {
|
|
|
+ protobufResults(uri, deployed, results)
|
|
|
+ }
|
|
|
+ if !disableStatus {
|
|
|
+ fmt.Fprint(os.Stderr, "\033[K")
|
|
|
+ }
|
|
|
+ },
|
|
|
+}
|
|
|
+
|
|
|
+func printResults(
|
|
|
+ uri string, deployed []hercules.LeafPipelineItem,
|
|
|
+ results map[hercules.LeafPipelineItem]interface{}) {
|
|
|
+ commonResult := results[nil].(*hercules.CommonAnalysisResult)
|
|
|
+
|
|
|
+ fmt.Println("hercules:")
|
|
|
+ fmt.Println(" version: 3")
|
|
|
+ fmt.Println(" hash:", hercules.GIT_HASH)
|
|
|
+ fmt.Println(" repository:", uri)
|
|
|
+ fmt.Println(" begin_unix_time:", commonResult.BeginTime)
|
|
|
+ fmt.Println(" end_unix_time:", commonResult.EndTime)
|
|
|
+ fmt.Println(" commits:", commonResult.CommitsNumber)
|
|
|
+ fmt.Println(" run_time:", commonResult.RunTime.Nanoseconds()/1e6)
|
|
|
+
|
|
|
+ for _, item := range deployed {
|
|
|
+ result := results[item]
|
|
|
+ fmt.Printf("%s:\n", item.Name())
|
|
|
+ if err := item.Serialize(result, false, os.Stdout); err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func protobufResults(
|
|
|
+ uri string, deployed []hercules.LeafPipelineItem,
|
|
|
+ results map[hercules.LeafPipelineItem]interface{}) {
|
|
|
+
|
|
|
+ header := pb.Metadata{
|
|
|
+ Version: 2,
|
|
|
+ Hash: hercules.GIT_HASH,
|
|
|
+ Repository: uri,
|
|
|
+ }
|
|
|
+ results[nil].(*hercules.CommonAnalysisResult).FillMetadata(&header)
|
|
|
+
|
|
|
+ message := pb.AnalysisResults{
|
|
|
+ Header: &header,
|
|
|
+ Contents: map[string][]byte{},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, item := range deployed {
|
|
|
+ result := results[item]
|
|
|
+ buffer := &bytes.Buffer{}
|
|
|
+ if err := item.Serialize(result, true, buffer); err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ message.Contents[item.Name()] = buffer.Bytes()
|
|
|
+ }
|
|
|
+
|
|
|
+ serialized, err := proto.Marshal(&message)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ os.Stdout.Write(serialized)
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+func tmpl(w io.Writer, text string, data interface{}) error
|
|
|
+
|
|
|
+func formatUsage(c *cobra.Command) error {
|
|
|
+
|
|
|
+
|
|
|
+ localFlags := c.LocalFlags()
|
|
|
+ leaves := hercules.Registry.GetLeaves()
|
|
|
+ plumbing := hercules.Registry.GetPlumbingItems()
|
|
|
+ features := hercules.Registry.GetFeaturedItems()
|
|
|
+ filter := map[string]bool{}
|
|
|
+ for _, l := range leaves {
|
|
|
+ filter[l.Flag()] = true
|
|
|
+ for _, cfg := range l.ListConfigurationOptions() {
|
|
|
+ filter[cfg.Flag] = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for _, i := range plumbing {
|
|
|
+ for _, cfg := range i.ListConfigurationOptions() {
|
|
|
+ filter[cfg.Flag] = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for key := range filter {
|
|
|
+ localFlags.Lookup(key).Hidden = true
|
|
|
+ }
|
|
|
+ args := map[string]interface{}{
|
|
|
+ "c": c,
|
|
|
+ "leaves": leaves,
|
|
|
+ "plumbing": plumbing,
|
|
|
+ "features": features,
|
|
|
+ }
|
|
|
+
|
|
|
+ template := `Usage:{{if .c.Runnable}}
|
|
|
+ {{.c.UseLine}}{{end}}{{if .c.HasAvailableSubCommands}}
|
|
|
+ {{.c.CommandPath}} [command]{{end}}{{if gt (len .c.Aliases) 0}}
|
|
|
+
|
|
|
+Aliases:
|
|
|
+ {{.c.NameAndAliases}}{{end}}{{if .c.HasExample}}
|
|
|
+
|
|
|
+Examples:
|
|
|
+{{.c.Example}}{{end}}{{if .c.HasAvailableSubCommands}}
|
|
|
+
|
|
|
+Available Commands:{{range .c.Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
|
|
+ {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .c.HasAvailableLocalFlags}}
|
|
|
+
|
|
|
+Flags:
|
|
|
+{{.c.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
|
|
+
|
|
|
+Analysis Targets:{{range .leaves}}
|
|
|
+ --{{rpad .Flag 40 }}Runs {{.Name}} analysis.{{range .ListConfigurationOptions}}
|
|
|
+ --{{if .Type.String}}{{rpad (print .Flag " " .Type.String) 40}}{{else}}{{rpad .Flag 40}}{{end}}{{.Description}}{{if .Default}} The default value is {{.FormatDefault}}.{{end}}{{end}}{{end}}
|
|
|
+
|
|
|
+Plumbing Options:{{range .plumbing}}{{$name := .Name}}{{range .ListConfigurationOptions}}
|
|
|
+ --{{if .Type.String}}{{rpad (print .Flag " " .Type.String " [" $name "]") 40}}{{else}}{{rpad (print .Flag " [" $name "]") 40}}{{end}}{{.Description}}{{if .Default}} The default value is {{.FormatDefault}}.{{end}}{{end}}{{end}}
|
|
|
+
|
|
|
+--feature:{{range $key, $value := .features}}
|
|
|
+ {{rpad $key 40}}Enables {{range $index, $item := $value}}{{if $index}}, {{end}}{{$item.Name}}{{end}}.{{end}}{{if .c.HasAvailableInheritedFlags}}
|
|
|
+
|
|
|
+Global Flags:
|
|
|
+{{.c.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .c.HasHelpSubCommands}}
|
|
|
+
|
|
|
+Additional help topics:{{range .c.Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
|
|
+ {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .c.HasAvailableSubCommands}}
|
|
|
+
|
|
|
+Use "{{.c.CommandPath}} [command] --help" for more information about a command.{{end}}
|
|
|
+`
|
|
|
+ err := tmpl(c.OutOrStderr(), template, args)
|
|
|
+ for key := range filter {
|
|
|
+ localFlags.Lookup(key).Hidden = false
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ c.Println(err)
|
|
|
+ }
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+var versionCmd = &cobra.Command{
|
|
|
+ Use: "version",
|
|
|
+ Short: "Print version information and exit.",
|
|
|
+ Long: ``,
|
|
|
+ Args: cobra.MaximumNArgs(0),
|
|
|
+ Run: func(cmd *cobra.Command, args []string) {
|
|
|
+ fmt.Printf("Version: %d\nGit: %s\n", hercules.VERSION, hercules.GIT_HASH)
|
|
|
+ },
|
|
|
+}
|
|
|
+
|
|
|
+var cmdlineFacts map[string]interface{}
|
|
|
+var cmdlineDeployed map[string]*bool
|
|
|
+
|
|
|
+func init() {
|
|
|
+ loadPlugins()
|
|
|
+ rootCmd.MarkFlagFilename("plugin")
|
|
|
+ rootFlags := rootCmd.Flags()
|
|
|
+ rootFlags.String("commits", "", "Path to the text file with the "+
|
|
|
+ "commit history to follow instead of the default rev-list "+
|
|
|
+ "--first-parent. The format is the list of hashes, each hash on a "+
|
|
|
+ "separate line. The first hash is the root.")
|
|
|
+ rootCmd.MarkFlagFilename("commits")
|
|
|
+ rootFlags.Bool("pb", false, "The output format will be Protocol Buffers instead of YAML.")
|
|
|
+ rootFlags.Bool("quiet", !terminal.IsTerminal(int(os.Stdin.Fd())),
|
|
|
+ "Do not print status updates to stderr.")
|
|
|
+ rootFlags.Bool("profile", false, "Collect the profile to hercules.pprof.")
|
|
|
+ cmdlineFacts, cmdlineDeployed = hercules.Registry.AddFlags(rootFlags)
|
|
|
+ rootCmd.SetUsageFunc(formatUsage)
|
|
|
+ rootCmd.AddCommand(versionCmd)
|
|
|
+ versionCmd.SetUsageFunc(versionCmd.UsageFunc())
|
|
|
+}
|
|
|
+
|
|
|
+func main() {
|
|
|
+ if err := rootCmd.Execute(); err != nil {
|
|
|
+ fmt.Fprintln(os.Stderr, err)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+}
|