main.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /*
  2. Package main provides the command line tool to gather the line burndown
  3. statistics from Git repositories. Usage:
  4. hercules <URL or FS path>
  5. Output is always written to stdout, progress is written to stderr.
  6. Output formats:
  7. - YAML (default)
  8. - Protocol Buffers (-pb)
  9. Extensions:
  10. -files include line burndown stats for each file alive in HEAD
  11. -people include line burndown stats for each developer
  12. -couples include coupling betwwen files and developers
  13. -granularity sets the number of days in each band of the burndown charts.
  14. -sampling set the frequency of measuring the state of burndown in days.
  15. -people-dict allows to specify a hand-crafted identity matching list. The format is text,
  16. each identity on separate line, matching names and emails separated with |
  17. -debug activates the debugging mode - hardly ever needed, used internally during the development.
  18. -profile activates the profile collection and runs the server on localhost:6060
  19. */
  20. package main
  21. import (
  22. "bytes"
  23. "flag"
  24. "fmt"
  25. "io"
  26. "io/ioutil"
  27. "net/http"
  28. _ "net/http/pprof"
  29. "os"
  30. "plugin"
  31. "runtime/pprof"
  32. "strconv"
  33. "strings"
  34. "github.com/gogo/protobuf/proto"
  35. "github.com/vbauerster/mpb"
  36. "github.com/vbauerster/mpb/decor"
  37. "golang.org/x/crypto/ssh/terminal"
  38. "gopkg.in/src-d/go-billy.v4/osfs"
  39. "gopkg.in/src-d/go-git.v4"
  40. "gopkg.in/src-d/go-git.v4/plumbing/object"
  41. "gopkg.in/src-d/go-git.v4/storage"
  42. "gopkg.in/src-d/go-git.v4/storage/filesystem"
  43. "gopkg.in/src-d/go-git.v4/storage/memory"
  44. "gopkg.in/src-d/hercules.v3"
  45. "gopkg.in/src-d/hercules.v3/pb"
  46. )
  47. type OneLineWriter struct {
  48. Writer io.Writer
  49. }
  50. func (writer OneLineWriter) Write(p []byte) (n int, err error) {
  51. if p[len(p)-1] == '\n' {
  52. p = p[:len(p)-1]
  53. if len(p) > 5 && bytes.Compare(p[len(p)-5:], []byte("done.")) == 0 {
  54. p = []byte("cloning...")
  55. }
  56. p = append(p, '\r')
  57. writer.Writer.Write([]byte("\r" + strings.Repeat(" ", 80) + "\r"))
  58. }
  59. n, err = writer.Writer.Write(p)
  60. return
  61. }
  62. func loadRepository(uri string, disableStatus bool) *git.Repository {
  63. var repository *git.Repository
  64. var backend storage.Storer
  65. var err error
  66. if strings.Contains(uri, "://") {
  67. if len(flag.Args()) == 2 {
  68. backend, err = filesystem.NewStorage(osfs.New(flag.Arg(1)))
  69. if err != nil {
  70. panic(err)
  71. }
  72. _, err = os.Stat(flag.Arg(1))
  73. if !os.IsNotExist(err) {
  74. fmt.Fprintf(os.Stderr, "warning: deleted %s\n", flag.Arg(1))
  75. os.RemoveAll(flag.Arg(1))
  76. }
  77. } else {
  78. backend = memory.NewStorage()
  79. }
  80. cloneOptions := &git.CloneOptions{URL: uri}
  81. if !disableStatus {
  82. fmt.Fprint(os.Stderr, "connecting...\r")
  83. cloneOptions.Progress = OneLineWriter{Writer: os.Stderr}
  84. }
  85. repository, err = git.Clone(backend, nil, cloneOptions)
  86. if !disableStatus {
  87. fmt.Fprint(os.Stderr, strings.Repeat(" ", 80)+"\r")
  88. }
  89. } else {
  90. if uri[len(uri)-1] == os.PathSeparator {
  91. uri = uri[:len(uri)-1]
  92. }
  93. repository, err = git.PlainOpen(uri)
  94. }
  95. if err != nil {
  96. panic(err)
  97. }
  98. return repository
  99. }
  100. type arrayPluginFlags map[string]bool
  101. func (apf *arrayPluginFlags) String() string {
  102. list := []string{}
  103. for key := range *apf {
  104. list = append(list, key)
  105. }
  106. return strings.Join(list, ", ")
  107. }
  108. func (apf *arrayPluginFlags) Set(value string) error {
  109. (*apf)[value] = true
  110. return nil
  111. }
  112. func loadPlugins() {
  113. pluginFlags := arrayPluginFlags{}
  114. fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
  115. fs.SetOutput(ioutil.Discard)
  116. pluginFlagName := "plugin"
  117. pluginDesc := "Load the specified plugin by the full or relative path. " +
  118. "Can be specified multiple times."
  119. fs.Var(&pluginFlags, pluginFlagName, pluginDesc)
  120. flag.Var(&pluginFlags, pluginFlagName, pluginDesc)
  121. fs.Parse(os.Args[1:])
  122. for path := range pluginFlags {
  123. _, err := plugin.Open(path)
  124. if err != nil {
  125. fmt.Fprintf(os.Stderr, "Failed to load plugin from %s %s\n", path, err)
  126. }
  127. }
  128. }
  129. func main() {
  130. loadPlugins()
  131. var printVersion, protobuf, profile, disableStatus bool
  132. var commitsFile string
  133. flag.BoolVar(&profile, "profile", false, "Collect the profile to hercules.pprof.")
  134. flag.StringVar(&commitsFile, "commits", "", "Path to the text file with the "+
  135. "commit history to follow instead of the default rev-list "+
  136. "--first-parent. The format is the list of hashes, each hash on a "+
  137. "separate line. The first hash is the root.")
  138. flag.BoolVar(&protobuf, "pb", false, "The output format will be Protocol Buffers instead of YAML.")
  139. flag.BoolVar(&printVersion, "version", false, "Print version information and exit.")
  140. flag.BoolVar(&disableStatus, "quiet", !terminal.IsTerminal(int(os.Stdin.Fd())),
  141. "Do not print status updates to stderr.")
  142. facts, deployChoices := hercules.Registry.AddFlags()
  143. flag.Parse()
  144. if printVersion {
  145. fmt.Printf("Version: 3\nGit: %s\n", hercules.GIT_HASH)
  146. return
  147. }
  148. if profile {
  149. go http.ListenAndServe("localhost:6060", nil)
  150. prof, _ := os.Create("hercules.pprof")
  151. pprof.StartCPUProfile(prof)
  152. defer pprof.StopCPUProfile()
  153. }
  154. if len(flag.Args()) == 0 || len(flag.Args()) > 3 {
  155. fmt.Fprint(os.Stderr,
  156. "Usage: hercules [options] <path to repo or URL> [<disk cache path>]\n")
  157. os.Exit(1)
  158. }
  159. uri := flag.Arg(0)
  160. repository := loadRepository(uri, disableStatus)
  161. // core logic
  162. pipeline := hercules.NewPipeline(repository)
  163. pipeline.SetFeaturesFromFlags()
  164. var progress *mpb.Progress
  165. var progressRendered bool
  166. if !disableStatus {
  167. beforeRender := func([]*mpb.Bar) {
  168. progressRendered = true
  169. }
  170. progress = mpb.New(mpb.Output(os.Stderr), mpb.WithBeforeRenderFunc(beforeRender))
  171. var bar *mpb.Bar
  172. pipeline.OnProgress = func(commit, length int) {
  173. if bar == nil {
  174. width := len(strconv.Itoa(length))*2 + 3
  175. bar = progress.AddBar(int64(length+1),
  176. mpb.PrependDecorators(decor.DynamicName(
  177. func(stats *decor.Statistics) string {
  178. if stats.Current < stats.Total {
  179. return fmt.Sprintf("%d / %d", stats.Current, length)
  180. }
  181. return "finalizing"
  182. }, width, 0)),
  183. mpb.AppendDecorators(decor.ETA(4, 0)),
  184. )
  185. }
  186. bar.Incr(commit - int(bar.Current()))
  187. }
  188. }
  189. var commits []*object.Commit
  190. if commitsFile == "" {
  191. // list of commits belonging to the default branch, from oldest to newest
  192. // rev-list --first-parent
  193. commits = pipeline.Commits()
  194. } else {
  195. var err error
  196. commits, err = hercules.LoadCommitsFromFile(commitsFile, repository)
  197. if err != nil {
  198. panic(err)
  199. }
  200. }
  201. facts["commits"] = commits
  202. deployed := []hercules.LeafPipelineItem{}
  203. for name, valPtr := range deployChoices {
  204. if *valPtr {
  205. item := pipeline.DeployItem(hercules.Registry.Summon(name)[0])
  206. deployed = append(deployed, item.(hercules.LeafPipelineItem))
  207. }
  208. }
  209. pipeline.Initialize(facts)
  210. if dryRun, _ := facts[hercules.ConfigPipelineDryRun].(bool); dryRun {
  211. return
  212. }
  213. results, err := pipeline.Run(commits)
  214. if err != nil {
  215. panic(err)
  216. }
  217. if !disableStatus {
  218. progress.Stop()
  219. if progressRendered {
  220. // this is the only way to reliably clear the progress bar
  221. fmt.Fprint(os.Stderr, "\033[F\033[K")
  222. }
  223. fmt.Fprint(os.Stderr, "writing...\r")
  224. }
  225. if !protobuf {
  226. printResults(uri, deployed, results)
  227. } else {
  228. protobufResults(uri, deployed, results)
  229. }
  230. if !disableStatus {
  231. fmt.Fprint(os.Stderr, "\033[K")
  232. }
  233. }
  234. func printResults(
  235. uri string, deployed []hercules.LeafPipelineItem,
  236. results map[hercules.LeafPipelineItem]interface{}) {
  237. commonResult := results[nil].(*hercules.CommonAnalysisResult)
  238. fmt.Println("hercules:")
  239. fmt.Println(" version: 3")
  240. fmt.Println(" hash:", hercules.GIT_HASH)
  241. fmt.Println(" repository:", uri)
  242. fmt.Println(" begin_unix_time:", commonResult.BeginTime)
  243. fmt.Println(" end_unix_time:", commonResult.EndTime)
  244. fmt.Println(" commits:", commonResult.CommitsNumber)
  245. fmt.Println(" run_time:", commonResult.RunTime.Nanoseconds()/1e6)
  246. for _, item := range deployed {
  247. result := results[item]
  248. fmt.Printf("%s:\n", item.Name())
  249. if err := item.Serialize(result, false, os.Stdout); err != nil {
  250. panic(err)
  251. }
  252. }
  253. }
  254. func protobufResults(
  255. uri string, deployed []hercules.LeafPipelineItem,
  256. results map[hercules.LeafPipelineItem]interface{}) {
  257. header := pb.Metadata{
  258. Version: 2,
  259. Hash: hercules.GIT_HASH,
  260. Repository: uri,
  261. }
  262. results[nil].(*hercules.CommonAnalysisResult).FillMetadata(&header)
  263. message := pb.AnalysisResults{
  264. Header: &header,
  265. Contents: map[string][]byte{},
  266. }
  267. for _, item := range deployed {
  268. result := results[item]
  269. buffer := &bytes.Buffer{}
  270. if err := item.Serialize(result, true, buffer); err != nil {
  271. panic(err)
  272. }
  273. message.Contents[item.Name()] = buffer.Bytes()
  274. }
  275. serialized, err := proto.Marshal(&message)
  276. if err != nil {
  277. panic(err)
  278. }
  279. os.Stdout.Write(serialized)
  280. }