root.go 13 KB

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