root.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. package main
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "log"
  8. "net/http"
  9. _ "net/http/pprof"
  10. "os"
  11. "plugin"
  12. "runtime/pprof"
  13. "strings"
  14. _ "unsafe" // for go:linkname
  15. "github.com/gogo/protobuf/proto"
  16. "github.com/spf13/cobra"
  17. "github.com/spf13/pflag"
  18. "golang.org/x/crypto/ssh/terminal"
  19. progress "gopkg.in/cheggaaa/pb.v1"
  20. "gopkg.in/src-d/go-billy.v4/osfs"
  21. "gopkg.in/src-d/go-git.v4"
  22. "gopkg.in/src-d/go-git.v4/plumbing/object"
  23. "gopkg.in/src-d/go-git.v4/storage"
  24. "gopkg.in/src-d/go-git.v4/storage/filesystem"
  25. "gopkg.in/src-d/go-git.v4/storage/memory"
  26. "gopkg.in/src-d/hercules.v3"
  27. "gopkg.in/src-d/hercules.v3/pb"
  28. )
  29. // oneLineWriter splits the output data by lines and outputs one on top of another using '\r'.
  30. // It also does some dark magic to handle Git statuses.
  31. type oneLineWriter struct {
  32. Writer io.Writer
  33. }
  34. func (writer oneLineWriter) Write(p []byte) (n int, err error) {
  35. if p[len(p)-1] == '\n' {
  36. p = p[:len(p)-1]
  37. if len(p) > 5 && bytes.Compare(p[len(p)-5:], []byte("done.")) == 0 {
  38. p = []byte("cloning...")
  39. }
  40. p = append(p, '\r')
  41. writer.Writer.Write([]byte("\r" + strings.Repeat(" ", 80) + "\r"))
  42. }
  43. n, err = writer.Writer.Write(p)
  44. return
  45. }
  46. func loadRepository(uri string, cachePath string, disableStatus bool) *git.Repository {
  47. var repository *git.Repository
  48. var backend storage.Storer
  49. var err error
  50. if strings.Contains(uri, "://") {
  51. if cachePath != "" {
  52. backend, err = filesystem.NewStorage(osfs.New(cachePath))
  53. if err != nil {
  54. panic(err)
  55. }
  56. _, err = os.Stat(cachePath)
  57. if !os.IsNotExist(err) {
  58. log.Printf("warning: deleted %s\n", cachePath)
  59. os.RemoveAll(cachePath)
  60. }
  61. } else {
  62. backend = memory.NewStorage()
  63. }
  64. cloneOptions := &git.CloneOptions{URL: uri}
  65. if !disableStatus {
  66. fmt.Fprint(os.Stderr, "connecting...\r")
  67. cloneOptions.Progress = oneLineWriter{Writer: os.Stderr}
  68. }
  69. repository, err = git.Clone(backend, nil, cloneOptions)
  70. if !disableStatus {
  71. fmt.Fprint(os.Stderr, strings.Repeat(" ", 80)+"\r")
  72. }
  73. } else {
  74. if uri[len(uri)-1] == os.PathSeparator {
  75. uri = uri[:len(uri)-1]
  76. }
  77. repository, err = git.PlainOpen(uri)
  78. }
  79. if err != nil {
  80. panic(err)
  81. }
  82. return repository
  83. }
  84. type arrayPluginFlags map[string]bool
  85. func (apf *arrayPluginFlags) String() string {
  86. list := []string{}
  87. for key := range *apf {
  88. list = append(list, key)
  89. }
  90. return strings.Join(list, ", ")
  91. }
  92. func (apf *arrayPluginFlags) Set(value string) error {
  93. (*apf)[value] = true
  94. return nil
  95. }
  96. func (apf *arrayPluginFlags) Type() string {
  97. return "path"
  98. }
  99. func loadPlugins() {
  100. pluginFlags := arrayPluginFlags{}
  101. fs := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
  102. fs.SetOutput(ioutil.Discard)
  103. pluginFlagName := "plugin"
  104. const pluginDesc = "Load the specified plugin by the full or relative path. " +
  105. "Can be specified multiple times."
  106. fs.Var(&pluginFlags, pluginFlagName, pluginDesc)
  107. pflag.Var(&pluginFlags, pluginFlagName, pluginDesc)
  108. fs.Parse(os.Args[1:])
  109. for path := range pluginFlags {
  110. _, err := plugin.Open(path)
  111. if err != nil {
  112. log.Printf("Failed to load plugin from %s %s\n", path, err)
  113. }
  114. }
  115. }
  116. // rootCmd represents the base command when called without any subcommands
  117. var rootCmd = &cobra.Command{
  118. Use: "hercules",
  119. Short: "Analyse a Git repository.",
  120. Long: `Hercules is a flexible and fast Git repository analysis engine. The base command executes
  121. the commit processing pipeline which is automatically generated from the dependencies of one
  122. or several analysis targets. The list of the available targets is printed in --help. External
  123. targets can be added using the --plugin system.`,
  124. Args: cobra.RangeArgs(1, 2),
  125. Run: func(cmd *cobra.Command, args []string) {
  126. flags := cmd.Flags()
  127. commitsFile, _ := flags.GetString("commits")
  128. protobuf, _ := flags.GetBool("pb")
  129. profile, _ := flags.GetBool("profile")
  130. disableStatus, _ := flags.GetBool("quiet")
  131. if profile {
  132. go http.ListenAndServe("localhost:6060", nil)
  133. prof, _ := os.Create("hercules.pprof")
  134. pprof.StartCPUProfile(prof)
  135. defer pprof.StopCPUProfile()
  136. }
  137. uri := args[0]
  138. cachePath := ""
  139. if len(args) == 2 {
  140. cachePath = args[1]
  141. }
  142. repository := loadRepository(uri, cachePath, disableStatus)
  143. // core logic
  144. pipeline := hercules.NewPipeline(repository)
  145. pipeline.SetFeaturesFromFlags()
  146. var bar *progress.ProgressBar
  147. if !disableStatus {
  148. pipeline.OnProgress = func(commit, length int) {
  149. if bar == nil {
  150. bar = progress.New(length + 1)
  151. bar.Callback = func(msg string) {
  152. os.Stderr.WriteString("\r" + msg)
  153. }
  154. bar.NotPrint = true
  155. bar.ShowPercent = false
  156. bar.ShowSpeed = false
  157. bar.SetMaxWidth(80)
  158. bar.Start()
  159. }
  160. bar.Set(commit)
  161. }
  162. }
  163. var commits []*object.Commit
  164. if commitsFile == "" {
  165. // list of commits belonging to the default branch, from oldest to newest
  166. // rev-list --first-parent
  167. commits = pipeline.Commits()
  168. } else {
  169. var err error
  170. commits, err = hercules.LoadCommitsFromFile(commitsFile, repository)
  171. if err != nil {
  172. panic(err)
  173. }
  174. }
  175. cmdlineFacts["commits"] = commits
  176. deployed := []hercules.LeafPipelineItem{}
  177. for name, valPtr := range cmdlineDeployed {
  178. if *valPtr {
  179. item := pipeline.DeployItem(hercules.Registry.Summon(name)[0])
  180. deployed = append(deployed, item.(hercules.LeafPipelineItem))
  181. }
  182. }
  183. pipeline.Initialize(cmdlineFacts)
  184. if dryRun, _ := cmdlineFacts[hercules.ConfigPipelineDryRun].(bool); dryRun {
  185. return
  186. }
  187. results, err := pipeline.Run(commits)
  188. if err != nil {
  189. panic(err)
  190. }
  191. if !disableStatus {
  192. bar.Finish()
  193. fmt.Fprint(os.Stderr, "\r"+strings.Repeat(" ", 80)+"\r")
  194. if !terminal.IsTerminal(int(os.Stdout.Fd())) {
  195. fmt.Fprint(os.Stderr, "writing...\r")
  196. }
  197. }
  198. if !protobuf {
  199. printResults(uri, deployed, results)
  200. } else {
  201. protobufResults(uri, deployed, results)
  202. }
  203. },
  204. }
  205. func printResults(
  206. uri string, deployed []hercules.LeafPipelineItem,
  207. results map[hercules.LeafPipelineItem]interface{}) {
  208. commonResult := results[nil].(*hercules.CommonAnalysisResult)
  209. fmt.Println("hercules:")
  210. fmt.Println(" version: 3")
  211. fmt.Println(" hash:", hercules.BinaryGitHash)
  212. fmt.Println(" repository:", uri)
  213. fmt.Println(" begin_unix_time:", commonResult.BeginTime)
  214. fmt.Println(" end_unix_time:", commonResult.EndTime)
  215. fmt.Println(" commits:", commonResult.CommitsNumber)
  216. fmt.Println(" run_time:", commonResult.RunTime.Nanoseconds()/1e6)
  217. for _, item := range deployed {
  218. result := results[item]
  219. fmt.Printf("%s:\n", item.Name())
  220. if err := item.Serialize(result, false, os.Stdout); err != nil {
  221. panic(err)
  222. }
  223. }
  224. }
  225. func protobufResults(
  226. uri string, deployed []hercules.LeafPipelineItem,
  227. results map[hercules.LeafPipelineItem]interface{}) {
  228. header := pb.Metadata{
  229. Version: 2,
  230. Hash: hercules.BinaryGitHash,
  231. Repository: uri,
  232. }
  233. results[nil].(*hercules.CommonAnalysisResult).FillMetadata(&header)
  234. message := pb.AnalysisResults{
  235. Header: &header,
  236. Contents: map[string][]byte{},
  237. }
  238. for _, item := range deployed {
  239. result := results[item]
  240. buffer := &bytes.Buffer{}
  241. if err := item.Serialize(result, true, buffer); err != nil {
  242. panic(err)
  243. }
  244. message.Contents[item.Name()] = buffer.Bytes()
  245. }
  246. serialized, err := proto.Marshal(&message)
  247. if err != nil {
  248. panic(err)
  249. }
  250. os.Stdout.Write(serialized)
  251. }
  252. // animate the private function defined in Cobra
  253. //go:linkname tmpl github.com/spf13/cobra.tmpl
  254. func tmpl(w io.Writer, text string, data interface{}) error
  255. func formatUsage(c *cobra.Command) error {
  256. // the default UsageFunc() does some private magic c.mergePersistentFlags()
  257. // this should stay on top
  258. localFlags := c.LocalFlags()
  259. leaves := hercules.Registry.GetLeaves()
  260. plumbing := hercules.Registry.GetPlumbingItems()
  261. features := hercules.Registry.GetFeaturedItems()
  262. filter := map[string]bool{}
  263. for _, l := range leaves {
  264. filter[l.Flag()] = true
  265. for _, cfg := range l.ListConfigurationOptions() {
  266. filter[cfg.Flag] = true
  267. }
  268. }
  269. for _, i := range plumbing {
  270. for _, cfg := range i.ListConfigurationOptions() {
  271. filter[cfg.Flag] = true
  272. }
  273. }
  274. for key := range filter {
  275. localFlags.Lookup(key).Hidden = true
  276. }
  277. args := map[string]interface{}{
  278. "c": c,
  279. "leaves": leaves,
  280. "plumbing": plumbing,
  281. "features": features,
  282. }
  283. template := `Usage:{{if .c.Runnable}}
  284. {{.c.UseLine}}{{end}}{{if .c.HasAvailableSubCommands}}
  285. {{.c.CommandPath}} [command]{{end}}{{if gt (len .c.Aliases) 0}}
  286. Aliases:
  287. {{.c.NameAndAliases}}{{end}}{{if .c.HasExample}}
  288. Examples:
  289. {{.c.Example}}{{end}}{{if .c.HasAvailableSubCommands}}
  290. Available Commands:{{range .c.Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
  291. {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .c.HasAvailableLocalFlags}}
  292. Flags:
  293. {{.c.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
  294. Analysis Targets:{{range .leaves}}
  295. --{{rpad .Flag 40 }}Runs {{.Name}} analysis.{{range .ListConfigurationOptions}}
  296. --{{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}}
  297. Plumbing Options:{{range .plumbing}}{{$name := .Name}}{{range .ListConfigurationOptions}}
  298. --{{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}}
  299. --feature:{{range $key, $value := .features}}
  300. {{rpad $key 40}}Enables {{range $index, $item := $value}}{{if $index}}, {{end}}{{$item.Name}}{{end}}.{{end}}{{if .c.HasAvailableInheritedFlags}}
  301. Global Flags:
  302. {{.c.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .c.HasHelpSubCommands}}
  303. Additional help topics:{{range .c.Commands}}{{if .IsAdditionalHelpTopicCommand}}
  304. {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .c.HasAvailableSubCommands}}
  305. Use "{{.c.CommandPath}} [command] --help" for more information about a command.{{end}}
  306. `
  307. err := tmpl(c.OutOrStderr(), template, args)
  308. for key := range filter {
  309. localFlags.Lookup(key).Hidden = false
  310. }
  311. if err != nil {
  312. c.Println(err)
  313. }
  314. return err
  315. }
  316. // versionCmd prints the API version and the Git commit hash
  317. var versionCmd = &cobra.Command{
  318. Use: "version",
  319. Short: "Print version information and exit.",
  320. Long: ``,
  321. Args: cobra.MaximumNArgs(0),
  322. Run: func(cmd *cobra.Command, args []string) {
  323. fmt.Printf("Version: %d\nGit: %s\n", hercules.BinaryVersion, hercules.BinaryGitHash)
  324. },
  325. }
  326. var cmdlineFacts map[string]interface{}
  327. var cmdlineDeployed map[string]*bool
  328. func init() {
  329. loadPlugins()
  330. rootCmd.MarkFlagFilename("plugin")
  331. rootFlags := rootCmd.Flags()
  332. rootFlags.String("commits", "", "Path to the text file with the "+
  333. "commit history to follow instead of the default rev-list "+
  334. "--first-parent. The format is the list of hashes, each hash on a "+
  335. "separate line. The first hash is the root.")
  336. rootCmd.MarkFlagFilename("commits")
  337. rootFlags.Bool("pb", false, "The output format will be Protocol Buffers instead of YAML.")
  338. rootFlags.Bool("quiet", !terminal.IsTerminal(int(os.Stdin.Fd())),
  339. "Do not print status updates to stderr.")
  340. rootFlags.Bool("profile", false, "Collect the profile to hercules.pprof.")
  341. cmdlineFacts, cmdlineDeployed = hercules.Registry.AddFlags(rootFlags)
  342. rootCmd.SetUsageFunc(formatUsage)
  343. rootCmd.AddCommand(versionCmd)
  344. versionCmd.SetUsageFunc(versionCmd.UsageFunc())
  345. }
  346. func main() {
  347. if err := rootCmd.Execute(); err != nil {
  348. fmt.Fprintln(os.Stderr, err)
  349. os.Exit(1)
  350. }
  351. }