root.go 11 KB

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