Explorar o código

Switch from vanilla "flag" to Cobra

Vadim Markovtsev %!s(int64=7) %!d(string=hai) anos
pai
achega
6458136f35

+ 1 - 1
.gitignore

@@ -1,4 +1,4 @@
-cmd/hercules-generate-plugin/plugin_template_source.go
+cmd/hercules/plugin_template_source.go
 contrib/_plugin_example/churn_analysis.pb.go
 pb/pb.pb.go
 

+ 8 - 6
.travis.yml

@@ -42,12 +42,14 @@ install:
 script:
   - go vet ./...
   - go test -v -cpu=1,2 -coverprofile=coverage.txt -covermode=count gopkg.in/src-d/hercules.v3
-  - $GOPATH/bin/hercules -version
-  - $GOPATH/bin/hercules -burndown -burndown-files -burndown-people -couples -quiet https://github.com/src-d/hercules | python3 labours.py -m all -o out --backend Agg --disable-projector
-  - $GOPATH/bin/hercules -burndown -burndown-files -burndown-people -couples -quiet -pb https://github.com/src-d/hercules | python3 labours.py -f pb -m all -o out --backend Agg --disable-projector
-  - $GOPATH/bin/hercules-generate-plugin -version
-  - $GOPATH/bin/hercules-generate-plugin -n MyPlug -o myplug && cd myplug && make && cd -
-  - cd contrib/_plugin_example && make
+  - $GOPATH/bin/hercules version
+  - $GOPATH/bin/hercules --burndown --couples --quiet --pb https://github.com/src-d/hercules > 1.pb
+  - cp 1.pb 2.pb
+  - $GOPATH/bin/hercules combine 1.pb 2.pb > 12.pb
+  - ($GOPATH/bin/hercules generate-plugin -n MyPlug -o myplug && cd myplug && make)
+  - (cd contrib/_plugin_example && make)
+  - $GOPATH/bin/hercules --burndown --burndown-files --burndown-people --couples --quiet https://github.com/src-d/hercules | python3 labours.py -m all -o out --backend Agg --disable-projector
+  - $GOPATH/bin/hercules --burndown --burndown-files --burndown-people --couples --quiet --pb https://github.com/src-d/hercules | python3 labours.py -f pb -m all -o out --backend Agg --disable-projector
 
 after_success:
   - bash <(curl -s https://codecov.io/bash)

+ 4 - 10
Makefile

@@ -2,18 +2,18 @@ ifneq (oneshell, $(findstring oneshell, $(.FEATURES)))
   $(error GNU make 3.82 or later is required)
 endif
 
-all: ${GOPATH}/bin/hercules ${GOPATH}/bin/hercules-generate-plugin ${GOPATH}/bin/hercules-combine
+all: ${GOPATH}/bin/hercules
 
 test: all
 	go test gopkg.in/src-d/hercules.v3
 
-dependencies: ${GOPATH}/src/gopkg.in/bblfsh/client-go.v2 ${GOPATH}/src/gopkg.in/src-d/hercules.v3 ${GOPATH}/src/gopkg.in/src-d/hercules.v3/pb/pb.pb.go ${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules-generate-plugin/plugin_template_source.go
+dependencies: ${GOPATH}/src/gopkg.in/bblfsh/client-go.v2 ${GOPATH}/src/gopkg.in/src-d/hercules.v3 ${GOPATH}/src/gopkg.in/src-d/hercules.v3/pb/pb.pb.go ${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules/plugin_template_source.go
 
 ${GOPATH}/src/gopkg.in/src-d/hercules.v3/pb/pb.pb.go:
 	PATH=$$PATH:$$GOPATH/bin protoc --gogo_out=pb --proto_path=pb pb/pb.proto
 
-${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules-generate-plugin/plugin_template_source.go:
-	cd ${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules-generate-plugin && go generate
+${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules/plugin_template_source.go: ${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules/plugin.template
+	cd ${GOPATH}/src/gopkg.in/src-d/hercules.v3/cmd/hercules && go generate
 
 ${GOPATH}/src/gopkg.in/src-d/hercules.v3:
 	go get -d gopkg.in/src-d/hercules.v3/...
@@ -28,9 +28,3 @@ ${GOPATH}/src/gopkg.in/bblfsh/client-go.v2:
 ${GOPATH}/bin/hercules: dependencies *.go cmd/hercules/*.go rbtree/*.go yaml/*.go toposort/*.go pb/*.go
 	cd ${GOPATH}/src/gopkg.in/src-d/hercules.v3
 	go get -ldflags "-X gopkg.in/src-d/hercules.v3.GIT_HASH=$$(git rev-parse HEAD)" gopkg.in/src-d/hercules.v3/cmd/hercules
-
-${GOPATH}/bin/hercules-generate-plugin: cmd/hercules-generate-plugin/*.go ${GOPATH}/bin/hercules
-	go get -ldflags "-X gopkg.in/src-d/hercules.v3.GIT_HASH=$$(git rev-parse HEAD)" gopkg.in/src-d/hercules.v3/cmd/hercules-generate-plugin
-
-${GOPATH}/bin/hercules-combine: cmd/hercules-combine/*.go ${GOPATH}/bin/hercules
-	go get -ldflags "-X gopkg.in/src-d/hercules.v3.GIT_HASH=$$(git rev-parse HEAD)" gopkg.in/src-d/hercules.v3/cmd/hercules-combine

+ 4 - 4
PLUGINS.md

@@ -17,7 +17,7 @@ There must be the `protoc` tool available in `$PATH`, version 3. Grab it from th
 Generation of a new plugin skeleton:
 
 ```
-hercules-generate-plugin -n MyPluginName -o my_plugin
+hercules generate-plugin -n MyPluginName -o my_plugin
 ```
 
 This command creates:
@@ -36,14 +36,14 @@ make
 ### Using a plugin
 
 ```
-hercules -plugin my_plugin_name.so -my-plugin-name https://github.com/user/repo
+hercules --plugin my_plugin_name.so --my-plugin-name https://github.com/user/repo
 ```
 
 ### Example
 
-See [contrib/plugin_example](contrib/_plugin_example). It was generated by `hercules-generate-plugin`
+See [contrib/plugin_example](contrib/_plugin_example). It was generated by `hercules generate-plugin`
 and implements [code churn](https://blog.gitprime.com/why-code-churn-matters/) analysis through time.
 It uses many Hercules features and supports YAML and protobuf output formats.
 
 ![go-git global churn](doc/churn_global.png)
-<p align="center">Generated with <code>hercules -plugin churn_analysis.so -churn https://github.com/src-d/go-git | python3 plot_churn.py --tick-days 14 -</code></p>
+<p align="center">Generated with <code>hercules --plugin churn_analysis.so --churn https://github.com/src-d/go-git | python3 plot_churn.py --tick-days 14 -</code></p>

+ 19 - 19
README.md

@@ -11,7 +11,7 @@ a pipe. It is possible to write custom analyses using the plugin system. It is a
 to merge several analysis results together.
 
 ![Hercules DAG of Burndown analysis](doc/dag.png)
-<p align="center">The DAG of burndown and couples analyses with UAST diff refining. Generated with <code>hercules -burndown -burndown-people -couples -feature=uast -dry-run -dump-dag doc/dag.dot https://github.com/src-d/hercules</code></p>
+<p align="center">The DAG of burndown and couples analyses with UAST diff refining. Generated with <code>hercules --burndown --burndown-people --couples --feature=uast --dry-run --dump-dag doc/dag.dot https://github.com/src-d/hercules</code></p>
 
 ![git/git image](doc/linux.png)
 <p align="center">torvalds/linux line burndown (granularity 30, sampling 30, resampled by year)</p>
@@ -38,18 +38,18 @@ Couples analysis also needs Tensorflow.
 ### Usage
 ```
 # Use "memory" go-git backend and display the burndown plot. "memory" is the fastest but the repository's git data must fit into RAM.
-hercules -burndown https://github.com/src-d/go-git | python3 labours.py -m project --resample month
+hercules --burndown https://github.com/src-d/go-git | python3 labours.py -m project --resample month
 # Use "file system" go-git backend and print some basic information about the repository.
 hercules /path/to/cloned/go-git
 # Use "file system" go-git backend, cache the cloned repository to /tmp/repo-cache, use Protocol Buffers and display the burndown plot without resampling.
-hercules -burndown -pb https://github.com/git/git /tmp/repo-cache | python3 labours.py -m project -f pb --resample raw
+hercules --burndown --pb https://github.com/git/git /tmp/repo-cache | python3 labours.py -m project -f pb --resample raw
 
 # Now something fun
 # Get the linear history from git rev-list, reverse it
 # Pipe to hercules, produce burndown snapshots for every 30 days grouped by 30 days
 # Save the raw data to cache.yaml, so that later is possible to python3 labours.py -i cache.yaml
 # Pipe the raw data to labours.py, set text font size to 16pt, use Agg matplotlib backend and save the plot to output.png
-git rev-list HEAD | tac | hercules -commits - -burndown https://github.com/git/git | tee cache.yaml | python3 labours.py -m project --font-size 16 --backend Agg --output git.png
+git rev-list HEAD | tac | hercules --commits - --burndown https://github.com/git/git | tee cache.yaml | python3 labours.py -m project --font-size 16 --backend Agg --output git.png
 ```
 
 `labours.py -i /path/to/yaml` allows to read the output from `hercules` which was saved on disk.
@@ -64,13 +64,13 @@ corresponding directory instead of cloning from scratch:
 hercules https://github.com/git/git /tmp/repo-cache
 
 # Second time - use the cache
-hercules -some-analysis /tmp/repo-cache
+hercules --some-analysis /tmp/repo-cache
 ```
 
 #### Docker image
 
 ```
-docker run --rm srcd/hercules hercules -burndown -pb https://github.com/git/git | docker run --rm -i -v $(pwd):/io srcd/hercules labours.py -f pb -m project -o /io/git_git.png
+docker run --rm srcd/hercules hercules --burndown --pb https://github.com/git/git | docker run --rm -i -v $(pwd):/io srcd/hercules labours.py -f pb -m project -o /io/git_git.png
 ```
 
 ### Built-in analyses
@@ -78,7 +78,7 @@ docker run --rm srcd/hercules hercules -burndown -pb https://github.com/git/git
 #### Project burndown
 
 ```
-hercules -burndown
+hercules --burndown
 python3 labours.py -m project
 ```
 
@@ -95,7 +95,7 @@ value, the more smooth is the plot but the more work is done.
 #### Files
 
 ```
-hercules -burndown -burndown-files
+hercules --burndown --burndown-files
 python3 labours.py -m files
 ```
 
@@ -104,7 +104,7 @@ Burndown statistics for every file in the repository which is alive in the lates
 #### People
 
 ```
-hercules -burndown -burndown-people [-people-dict=/path/to/identities]
+hercules --burndown --burndown-people [-people-dict=/path/to/identities]
 python3 labours.py -m person
 ```
 
@@ -128,7 +128,7 @@ by `|`. The case is ignored.
 <p align="center">Wireshark top 20 devs - churn matrix</p>
 
 ```
-hercules -burndown -burndown-people [-people-dict=/path/to/identities]
+hercules --burndown --burndown-people [-people-dict=/path/to/identities]
 python3 labours.py -m churn_matrix
 ```
 
@@ -150,7 +150,7 @@ The sequence of developers is stored in `people_sequence` YAML node.
 <p align="center">Ember.js top 20 devs - code ownership</p>
 
 ```
-hercules -burndown -burndown-people [-people-dict=/path/to/identities]
+hercules --burndown --burndown-people [-people-dict=/path/to/identities]
 python3 labours.py -m ownership
 ```
 
@@ -163,7 +163,7 @@ how many lines are alive at the sampled moments in time for each identified deve
 <p align="center">torvalds/linux files' coupling in Tensorflow Projector</p>
 
 ```
-hercules -couples [-people-dict=/path/to/identities]
+hercules --couples [-people-dict=/path/to/identities]
 python3 labours.py -m couples -o <name> [--couples-tmp-dir=/tmp]
 ```
 
@@ -183,7 +183,7 @@ can be visualized with t-SNE implemented in TF Projector.
 #### Everything in a single pass
 
 ```
-hercules -burndown -burndown-files -burndown-people -couples [-people-dict=/path/to/identities]
+hercules --burndown --burndown-files --burndown-people --couples [-people-dict=/path/to/identities]
 python3 labours.py -m all
 ```
 
@@ -193,12 +193,12 @@ Hercules has a plugin system and allows to run custom analyses. See [PLUGINS.md]
 
 ### Merging
 
-`hercules-combine` is the tool which joins several analysis results in Protocol Buffers format together. 
+`hercules combine` is the command which joins several analysis results in Protocol Buffers format together. 
 
 ```
-hercules -burndown -pb https://github.com/src-d/go-git > go-git.pb
-hercules -burndown -pb https://github.com/src-d/hercules > hercules.pb
-hercules-combine go-git.pb hercules.pb | python3 labours.py -f pb -m project --resample M
+hercules --burndown --pb https://github.com/src-d/go-git > go-git.pb
+hercules --burndown --pb https://github.com/src-d/hercules > hercules.pb
+hercules combine go-git.pb hercules.pb | python3 labours.py -f pb -m project --resample M
 ```
 
 ### Bad unicode errors
@@ -208,7 +208,7 @@ may raise exceptions. Filter the output from `hercules` through `fix_yaml_unicod
 such offending characters.
 
 ```
-hercules -burndown -burndown-people https://github.com/... | python3 fix_yaml_unicode.py | python3 labours.py -m people
+hercules --burndown --burndown-people https://github.com/... | python3 fix_yaml_unicode.py | python3 labours.py -m people
 ```
 
 ### Plotting
@@ -252,7 +252,7 @@ in-memory storage may require much RAM, for example, the Linux kernel takes over
 1. Parsing YAML in Python is slow when the number of internal objects is big. `hercules`' output
 for the Linux kernel in "couples" mode is 1.5 GB and takes more than an hour / 180GB RAM to be
 parsed. However, most of the repositories are parsed within a minute. Try using Protocol Buffers
-instead (`hercules -pb` and `labours.py -f pb`).
+instead (`hercules --pb` and `labours.py -f pb`).
 1. To speed-up yaml parsing
    ```
    # Debian, Ubuntu

+ 0 - 129
cmd/hercules-generate-plugin/main.go

@@ -1,129 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"flag"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"path"
-	"runtime"
-	"strings"
-	"text/template"
-
-	"github.com/fatih/camelcase"
-	"gopkg.in/src-d/hercules.v3"
-)
-
-//go:generate go run embed.go
-
-var SHLIB_EXT = map[string]string{
-	"window":  "dll",
-	"linux":   "so",
-	"darwin":  "dylib",
-	"freebsd": "dylib",
-}
-
-func main() {
-	var outputDir, name, varname, _flag, pkg string
-	var printVersion, disableMakefile bool
-	flag.StringVar(&name, "n", "", "Name of the plugin, CamelCase. Required.")
-	flag.StringVar(&outputDir, "o", ".", "Output directory for the generated plugin files.")
-	flag.StringVar(&varname, "varname", "", "Name of the plugin instance variable, If not "+
-		"specified, inferred from -n.")
-	flag.StringVar(&_flag, "flag", "", "Name of the plugin activation cmdline flag, If not "+
-		"specified, inferred from -varname.")
-	flag.BoolVar(&printVersion, "version", false, "Print version information and exit.")
-	flag.BoolVar(&disableMakefile, "no-makefile", false, "Do not generate the Makefile.")
-	flag.StringVar(&pkg, "package", "main", "Name of the package.")
-	flag.Parse()
-	if printVersion {
-		fmt.Printf("Version: 3\nGit:     %s\n", hercules.GIT_HASH)
-		return
-	}
-	if name == "" {
-		fmt.Fprintln(os.Stderr, "-n must be specified")
-		flag.PrintDefaults()
-		os.Exit(1)
-	}
-	splitted := camelcase.Split(name)
-	err := os.MkdirAll(outputDir, os.ModePerm)
-	if err != nil {
-		panic(err)
-	}
-	outputPath := path.Join(outputDir, strings.ToLower(strings.Join(splitted, "_"))+".go")
-	gen := template.Must(template.New("plugin").Parse(PLUGIN_TEMPLATE_SOURCE))
-	outFile, err := os.Create(outputPath)
-	if err != nil {
-		panic(err)
-	}
-	defer outFile.Close()
-	if varname == "" {
-		varname = strings.ToLower(splitted[0])
-	}
-	if _flag == "" {
-		_flag = strings.ToLower(strings.Join(splitted, "-"))
-	}
-	outputBase := path.Base(outputPath)
-	shlib := outputBase[:len(outputBase)-2] + SHLIB_EXT[runtime.GOOS]
-	protoBuf := outputPath[:len(outputPath)-3] + ".proto"
-	pbGo := outputPath[:len(outputPath)-3] + ".pb.go"
-	dict := map[string]string{
-		"name": name, "varname": varname, "flag": _flag, "package": pkg,
-		"output": outputPath, "shlib": shlib, "proto": protoBuf, "protogo": pbGo,
-		"outdir": outputDir}
-	err = gen.Execute(outFile, dict)
-	if err != nil {
-		panic(err)
-	}
-	// write pb file
-	ioutil.WriteFile(protoBuf, []byte(fmt.Sprintf(`syntax = "proto3";
-option go_package = "%s";
-
-message %sResultMessage {
-  // add fields here
-  // reference: https://developers.google.com/protocol-buffers/docs/proto3
-  // example: pb/pb.proto https://github.com/src-d/hercules/blob/master/pb/pb.proto
-}
-`, pkg, name)), 0666)
-	// generate the pb Go file
-	protoc, err := exec.LookPath("protoc")
-	args := [...]string{
-		protoc,
-		"--gogo_out=" + outputDir,
-		"--proto_path=" + outputDir,
-		protoBuf,
-	}
-	env := os.Environ()
-	env = append(env, fmt.Sprintf(
-		"PATH=%s:%s", os.Getenv("PATH"), path.Join(os.Getenv("GOPATH"), "bin")))
-	if err != nil {
-		panic("protoc was not found at " + env[len(env)-1])
-	}
-	cmd := exec.Cmd{Path: protoc, Args: args[:], Env: env, Stdout: os.Stdout, Stderr: os.Stderr}
-	err = cmd.Run()
-	if err != nil {
-		panic(err)
-	}
-	if !disableMakefile {
-		makefile := path.Join(outputDir, "Makefile")
-		gen = template.Must(template.New("plugin").Parse(`all: {{.shlib}}
-
-{{.shlib}}: {{.output}} {{.protogo}}
-` + "\t" + `go build -buildmode=plugin {{.output}} {{.protogo}}
-
-{{.protogo}}: {{.proto}}
-` + "\t" + `PATH=$$PATH:$$GOPATH/bin protoc --gogo_out=. --proto_path=. {{.proto}}
-`))
-		buffer := new(bytes.Buffer)
-		mkrelative := func(name string) {
-			dict[name] = path.Base(dict[name])
-		}
-		mkrelative("output")
-		mkrelative("protogo")
-		mkrelative("proto")
-		gen.Execute(buffer, dict)
-		ioutil.WriteFile(makefile, buffer.Bytes(), 0666)
-	}
-}

+ 57 - 49
cmd/hercules-combine/main.go

@@ -7,65 +7,68 @@ import (
 	"io"
 	"io/ioutil"
 	"os"
+	"sort"
 	"strings"
 
 	"github.com/gogo/protobuf/proto"
+	"github.com/spf13/cobra"
 	"gopkg.in/src-d/hercules.v3"
 	"gopkg.in/src-d/hercules.v3/pb"
-	"sort"
 )
 
-func main() {
-	files := os.Args[1:]
-	if len(files) == 0 {
-		fmt.Fprintln(os.Stderr, "Usage: hercules-combine file [file...]")
-		os.Exit(1)
-	}
-	if len(files) == 1 {
-		file, err := os.Open(files[0])
+// combineCmd represents the combine command
+var combineCmd = &cobra.Command{
+	Use:   "combine",
+	Short: "Merge several binary analysis results together.",
+	Long:  ``,
+	Args:  cobra.MinimumNArgs(1),
+	Run: func(cmd *cobra.Command, files []string) {
+		if len(files) == 1 {
+			file, err := os.Open(files[0])
+			if err != nil {
+				panic(err)
+			}
+			defer file.Close()
+			io.Copy(os.Stdout, bufio.NewReader(file))
+			return
+		}
+		repos := []string{}
+		allErrors := map[string][]string{}
+		mergedResults := map[string]interface{}{}
+		mergedMetadata := &hercules.CommonAnalysisResult{}
+		for _, fileName := range files {
+			anotherResults, anotherMetadata, errs := loadMessage(fileName, &repos)
+			if anotherMetadata != nil {
+				mergeResults(mergedResults, mergedMetadata, anotherResults, anotherMetadata)
+			}
+			allErrors[fileName] = errs
+		}
+		printErrors(allErrors)
+		sort.Strings(repos)
+		if mergedMetadata == nil {
+			return
+		}
+		mergedMessage := pb.AnalysisResults{
+			Header: &pb.Metadata{
+				Version:    2,
+				Hash:       hercules.GIT_HASH,
+				Repository: strings.Join(repos, " & "),
+			},
+			Contents: map[string][]byte{},
+		}
+		mergedMetadata.FillMetadata(mergedMessage.Header)
+		for key, val := range mergedResults {
+			buffer := bytes.Buffer{}
+			hercules.Registry.Summon(key)[0].(hercules.LeafPipelineItem).Serialize(
+				val, true, &buffer)
+			mergedMessage.Contents[key] = buffer.Bytes()
+		}
+		serialized, err := proto.Marshal(&mergedMessage)
 		if err != nil {
 			panic(err)
 		}
-		defer file.Close()
-		io.Copy(os.Stdout, bufio.NewReader(file))
-		return
-	}
-	repos := []string{}
-	allErrors := map[string][]string{}
-	mergedResults := map[string]interface{}{}
-	mergedMetadata := &hercules.CommonAnalysisResult{}
-	for _, fileName := range files {
-		anotherResults, anotherMetadata, errs := loadMessage(fileName, &repos)
-		if anotherMetadata != nil {
-			mergeResults(mergedResults, mergedMetadata, anotherResults, anotherMetadata)
-		}
-		allErrors[fileName] = errs
-	}
-	printErrors(allErrors)
-	sort.Strings(repos)
-	if mergedMetadata == nil {
-		return
-	}
-	mergedMessage := pb.AnalysisResults{
-		Header: &pb.Metadata{
-			Version:    2,
-			Hash:       hercules.GIT_HASH,
-			Repository: strings.Join(repos, " & "),
-		},
-		Contents: map[string][]byte{},
-	}
-	mergedMetadata.FillMetadata(mergedMessage.Header)
-	for key, val := range mergedResults {
-		buffer := bytes.Buffer{}
-		hercules.Registry.Summon(key)[0].(hercules.LeafPipelineItem).Serialize(
-			val, true, &buffer)
-		mergedMessage.Contents[key] = buffer.Bytes()
-	}
-	serialized, err := proto.Marshal(&mergedMessage)
-	if err != nil {
-		panic(err)
-	}
-	os.Stdout.Write(serialized)
+		os.Stdout.Write(serialized)
+	},
 }
 
 func loadMessage(fileName string, repos *[]string) (
@@ -147,3 +150,8 @@ func mergeResults(mergedResults map[string]interface{},
 		mergedCommons.Merge(anotherCommons)
 	}
 }
+
+func init() {
+	rootCmd.AddCommand(combineCmd)
+	combineCmd.SetUsageFunc(combineCmd.UsageFunc())
+}

cmd/hercules-generate-plugin/embed.go → cmd/hercules/embed.go


+ 137 - 0
cmd/hercules/generate_plugin.go

@@ -0,0 +1,137 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"runtime"
+	"strings"
+	"text/template"
+
+	"github.com/fatih/camelcase"
+	"github.com/spf13/cobra"
+)
+
+//go:generate go run embed.go
+
+var SHLIB_EXT = map[string]string{
+	"window":  "dll",
+	"linux":   "so",
+	"darwin":  "dylib",
+	"freebsd": "dylib",
+}
+
+// generatePluginCmd represents the generatePlugin command
+var generatePluginCmd = &cobra.Command{
+	Use:   "generate-plugin",
+	Short: "Write the plugin source skeleton.",
+	Long:  ``,
+	Run: func(cmd *cobra.Command, args []string) {
+		flags := cmd.Flags()
+		name, _ := flags.GetString("name")
+		outputDir, _ := flags.GetString("output")
+		varname, _ := flags.GetString("varname")
+		flag, _ := flags.GetString("flag")
+		disableMakefile, _ := flags.GetBool("no-makefile")
+		pkg, _ := flags.GetString("package")
+
+		splitted := camelcase.Split(name)
+		err := os.MkdirAll(outputDir, os.ModePerm)
+		if err != nil {
+			panic(err)
+		}
+		outputPath := path.Join(outputDir, strings.ToLower(strings.Join(splitted, "_"))+".go")
+		gen := template.Must(template.New("plugin").Parse(PLUGIN_TEMPLATE_SOURCE))
+		outFile, err := os.Create(outputPath)
+		if err != nil {
+			panic(err)
+		}
+		defer outFile.Close()
+		if varname == "" {
+			varname = strings.ToLower(splitted[0])
+		}
+		if flag == "" {
+			flag = strings.ToLower(strings.Join(splitted, "-"))
+		}
+		outputBase := path.Base(outputPath)
+		shlib := outputBase[:len(outputBase)-2] + SHLIB_EXT[runtime.GOOS]
+		protoBuf := outputPath[:len(outputPath)-3] + ".proto"
+		pbGo := outputPath[:len(outputPath)-3] + ".pb.go"
+		dict := map[string]string{
+			"name": name, "varname": varname, "flag": flag, "package": pkg,
+			"output": outputPath, "shlib": shlib, "proto": protoBuf, "protogo": pbGo,
+			"outdir": outputDir}
+		err = gen.Execute(outFile, dict)
+		if err != nil {
+			panic(err)
+		}
+		// write pb file
+		ioutil.WriteFile(protoBuf, []byte(fmt.Sprintf(`syntax = "proto3";
+	option go_package = "%s";
+	
+	message %sResultMessage {
+	  // add fields here
+	  // reference: https://developers.google.com/protocol-buffers/docs/proto3
+	  // example: pb/pb.proto https://github.com/src-d/hercules/blob/master/pb/pb.proto
+	}
+	`, pkg, name)), 0666)
+		// generate the pb Go file
+		protoc, err := exec.LookPath("protoc")
+		cmdargs := [...]string{
+			protoc,
+			"--gogo_out=" + outputDir,
+			"--proto_path=" + outputDir,
+			protoBuf,
+		}
+		env := os.Environ()
+		env = append(env, fmt.Sprintf(
+			"PATH=%s:%s", os.Getenv("PATH"), path.Join(os.Getenv("GOPATH"), "bin")))
+		if err != nil {
+			panic("protoc was not found at " + env[len(env)-1])
+		}
+		protocmd := exec.Cmd{
+			Path: protoc, Args: cmdargs[:], Env: env, Stdout: os.Stdout, Stderr: os.Stderr}
+		err = protocmd.Run()
+		if err != nil {
+			panic(err)
+		}
+		if !disableMakefile {
+			makefile := path.Join(outputDir, "Makefile")
+			gen = template.Must(template.New("plugin").Parse(`all: {{.shlib}}
+
+{{.shlib}}: {{.output}} {{.protogo}}
+` + "\t" + `go build -buildmode=plugin {{.output}} {{.protogo}}
+
+{{.protogo}}: {{.proto}}
+` + "\t" + `PATH=$$PATH:$$GOPATH/bin protoc --gogo_out=. --proto_path=. {{.proto}}
+`))
+			buffer := new(bytes.Buffer)
+			mkrelative := func(name string) {
+				dict[name] = path.Base(dict[name])
+			}
+			mkrelative("output")
+			mkrelative("protogo")
+			mkrelative("proto")
+			gen.Execute(buffer, dict)
+			ioutil.WriteFile(makefile, buffer.Bytes(), 0666)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(generatePluginCmd)
+	generatePluginCmd.SetUsageFunc(generatePluginCmd.UsageFunc())
+	gpFlags := generatePluginCmd.Flags()
+	gpFlags.StringP("name", "n", "", "Name of the plugin, CamelCase. Required.")
+	generatePluginCmd.MarkFlagRequired("name")
+	gpFlags.StringP("output", "o", ".", "Output directory for the generated plugin files.")
+	gpFlags.String("varname", "", "Name of the plugin instance variable, If not "+
+		"specified, inferred from -n.")
+	gpFlags.String("flag", "", "Name of the plugin activation cmdline flag, If not "+
+		"specified, inferred from -varname.")
+	gpFlags.Bool("no-makefile", false, "Do not generate the Makefile.")
+	gpFlags.String("package", "main", "Name of the package.")
+}

+ 3 - 0
cmd/hercules/hack.go

@@ -0,0 +1,3 @@
+package main
+
+import "C" // needed for go:linkname in root.go

+ 0 - 310
cmd/hercules/main.go

@@ -1,310 +0,0 @@
-/*
-Package main provides the command line tool to gather the line burndown
-statistics from Git repositories. Usage:
-
-	hercules <URL or FS path>
-
-Output is always written to stdout, progress is written to stderr.
-Output formats:
-
-- YAML (default)
-- Protocol Buffers (-pb)
-
-Extensions:
-
--files include line burndown stats for each file alive in HEAD
--people include line burndown stats for each developer
--couples include coupling betwwen files and developers
-
--granularity sets the number of days in each band of the burndown charts.
--sampling set the frequency of measuring the state of burndown in days.
-
--people-dict allows to specify a hand-crafted identity matching list. The format is text,
-each identity on separate line, matching names and emails separated with |
-
--debug activates the debugging mode - hardly ever needed, used internally during the development.
--profile activates the profile collection and runs the server on localhost:6060
-*/
-package main
-
-import (
-	"bytes"
-	"flag"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net/http"
-	_ "net/http/pprof"
-	"os"
-	"plugin"
-	"runtime/pprof"
-	"strconv"
-	"strings"
-
-	"github.com/gogo/protobuf/proto"
-	"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, disableStatus bool) *git.Repository {
-	var repository *git.Repository
-	var backend storage.Storer
-	var err error
-	if strings.Contains(uri, "://") {
-		if len(flag.Args()) == 2 {
-			backend, err = filesystem.NewStorage(osfs.New(flag.Arg(1)))
-			if err != nil {
-				panic(err)
-			}
-			_, err = os.Stat(flag.Arg(1))
-			if !os.IsNotExist(err) {
-				fmt.Fprintf(os.Stderr, "warning: deleted %s\n", flag.Arg(1))
-				os.RemoveAll(flag.Arg(1))
-			}
-		} 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 loadPlugins() {
-	pluginFlags := arrayPluginFlags{}
-	fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
-	fs.SetOutput(ioutil.Discard)
-	pluginFlagName := "plugin"
-	pluginDesc := "Load the specified plugin by the full or relative path. " +
-		"Can be specified multiple times."
-	fs.Var(&pluginFlags, pluginFlagName, pluginDesc)
-	flag.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)
-		}
-	}
-}
-
-func main() {
-	loadPlugins()
-	var printVersion, protobuf, profile, disableStatus bool
-	var commitsFile string
-	flag.BoolVar(&profile, "profile", false, "Collect the profile to hercules.pprof.")
-	flag.StringVar(&commitsFile, "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.")
-	flag.BoolVar(&protobuf, "pb", false, "The output format will be Protocol Buffers instead of YAML.")
-	flag.BoolVar(&printVersion, "version", false, "Print version information and exit.")
-	flag.BoolVar(&disableStatus, "quiet", !terminal.IsTerminal(int(os.Stdin.Fd())),
-		"Do not print status updates to stderr.")
-	facts, deployChoices := hercules.Registry.AddFlags()
-	flag.Parse()
-
-	if printVersion {
-		fmt.Printf("Version: 3\nGit:     %s\n", hercules.GIT_HASH)
-		return
-	}
-
-	if profile {
-		go http.ListenAndServe("localhost:6060", nil)
-		prof, _ := os.Create("hercules.pprof")
-		pprof.StartCPUProfile(prof)
-		defer pprof.StopCPUProfile()
-	}
-	if len(flag.Args()) == 0 || len(flag.Args()) > 3 {
-		fmt.Fprint(os.Stderr,
-			"Usage: hercules [options] <path to repo or URL> [<disk cache path>]\n")
-		os.Exit(1)
-	}
-	uri := flag.Arg(0)
-	repository := loadRepository(uri, disableStatus)
-
-	// core logic
-	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 == "" {
-		// list of commits belonging to the default branch, from oldest to newest
-		// rev-list --first-parent
-		commits = pipeline.Commits()
-	} else {
-		var err error
-		commits, err = hercules.LoadCommitsFromFile(commitsFile, repository)
-		if err != nil {
-			panic(err)
-		}
-	}
-	facts["commits"] = commits
-	deployed := []hercules.LeafPipelineItem{}
-	for name, valPtr := range deployChoices {
-		if *valPtr {
-			item := pipeline.DeployItem(hercules.Registry.Summon(name)[0])
-			deployed = append(deployed, item.(hercules.LeafPipelineItem))
-		}
-	}
-	pipeline.Initialize(facts)
-	if dryRun, _ := facts[hercules.ConfigPipelineDryRun].(bool); dryRun {
-		return
-	}
-	results, err := pipeline.Run(commits)
-	if err != nil {
-		panic(err)
-	}
-	if !disableStatus {
-		progress.Stop()
-		if progressRendered {
-			// this is the only way to reliably clear the progress bar
-			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)
-}

+ 2 - 2
cmd/hercules-generate-plugin/plugin.template

@@ -14,8 +14,8 @@
 //
 // Usage:
 //
-//    hercules -plugin {{.shlib}} -{{.flag}}
-//    hercules -plugin {{.shlib}} -help
+//    hercules --plugin {{.shlib}} --{{.flag}}
+//    hercules --plugin {{.shlib}} -help
 
 package {{.package}}
 

+ 402 - 0
cmd/hercules/root.go

@@ -0,0 +1,402 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	_ "net/http/pprof"
+	"os"
+	"plugin"
+	"runtime/pprof"
+	"strconv"
+	"strings"
+	_ "unsafe" // for go:linkname
+
+	"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)
+		}
+	}
+}
+
+// rootCmd represents the base command when called without any subcommands
+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)
+
+		// core logic
+		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 == "" {
+			// list of commits belonging to the default branch, from oldest to newest
+			// rev-list --first-parent
+			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 {
+				// this is the only way to reliably clear the progress bar
+				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)
+}
+
+// animate the private function defined in Cobra
+//go:linkname tmpl github.com/spf13/cobra.tmpl
+func tmpl(w io.Writer, text string, data interface{}) error
+
+func formatUsage(c *cobra.Command) error {
+	// the default UsageFunc() does some private magic c.mergePersistentFlags()
+	// this should stay on top
+	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
+}
+
+// versionCmd prints the API version and the Git commit hash
+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)
+	}
+}

+ 23 - 140
pipeline.go

@@ -3,17 +3,13 @@ package hercules
 import (
 	"bufio"
 	"errors"
-	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"reflect"
 	"sort"
-	"strings"
 	"time"
-	"unsafe"
 
 	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing"
@@ -33,10 +29,17 @@ const (
 	StringConfigurationOption
 )
 
-const (
-	ConfigPipelineDumpPath = "Pipeline.DumpPath"
-	ConfigPipelineDryRun   = "Pipeline.DryRun"
-)
+func (opt ConfigurationOptionType) String() string {
+	switch opt {
+	case BoolConfigurationOption:
+		return ""
+	case IntConfigurationOption:
+		return "int"
+	case StringConfigurationOption:
+		return "string"
+	}
+	panic(fmt.Sprintf("Invalid ConfigurationOptionType value %d", opt))
+}
 
 // ConfigurationOption allows for the unified, retrospective way to setup PipelineItem-s.
 type ConfigurationOption struct {
@@ -52,6 +55,13 @@ type ConfigurationOption struct {
 	Default interface{}
 }
 
+func (opt ConfigurationOption) FormatDefault() string {
+	if opt.Type != StringConfigurationOption {
+		return fmt.Sprint(opt.Default)
+	}
+	return fmt.Sprintf("\"%s\"", opt.Default)
+}
+
 // PipelineItem is the interface for all the units of the Git commit analysis pipeline.
 type PipelineItem interface {
 	// Name returns the name of the analysis.
@@ -155,137 +165,6 @@ func MetadataToCommonAnalysisResult(meta *pb.Metadata) *CommonAnalysisResult {
 	}
 }
 
-// PipelineItemRegistry contains all the known PipelineItem-s.
-type PipelineItemRegistry struct {
-	provided   map[string][]reflect.Type
-	registered map[string]reflect.Type
-	flags      map[string]reflect.Type
-}
-
-// Register adds another PipelineItem to the registry.
-func (registry *PipelineItemRegistry) Register(example PipelineItem) {
-	t := reflect.TypeOf(example)
-	registry.registered[example.Name()] = t
-	if fpi, ok := example.(LeafPipelineItem); ok {
-		registry.flags[fpi.Flag()] = t
-	}
-	for _, dep := range example.Provides() {
-		ts := registry.provided[dep]
-		if ts == nil {
-			ts = []reflect.Type{}
-		}
-		ts = append(ts, t)
-		registry.provided[dep] = ts
-	}
-}
-
-func (registry *PipelineItemRegistry) Summon(providesOrName string) []PipelineItem {
-	if registry.provided == nil {
-		return []PipelineItem{}
-	}
-	ts := registry.provided[providesOrName]
-	items := []PipelineItem{}
-	for _, t := range ts {
-		items = append(items, reflect.New(t.Elem()).Interface().(PipelineItem))
-	}
-	if t, exists := registry.registered[providesOrName]; exists {
-		items = append(items, reflect.New(t.Elem()).Interface().(PipelineItem))
-	}
-	return items
-}
-
-type arrayFeatureFlags struct {
-	// Flags containts the features activated through the command line.
-	Flags []string
-	// Choices contains all registered features.
-	Choices map[string]bool
-}
-
-func (acf *arrayFeatureFlags) String() string {
-	return strings.Join([]string(acf.Flags), ", ")
-}
-
-func (acf *arrayFeatureFlags) Set(value string) error {
-	if _, exists := acf.Choices[value]; !exists {
-		return errors.New(fmt.Sprintf("Feature \"%s\" is not registered.", value))
-	}
-	acf.Flags = append(acf.Flags, value)
-	return nil
-}
-
-var featureFlags = arrayFeatureFlags{Flags: []string{}, Choices: map[string]bool{}}
-
-// AddFlags inserts the cmdline options from PipelineItem.ListConfigurationOptions(),
-// FeaturedPipelineItem().Features() and LeafPipelineItem.Flag() into the global "flag" parser
-// built into the Go runtime.
-// Returns the "facts" which can be fed into PipelineItem.Configure() and the dictionary of
-// runnable analysis (LeafPipelineItem) choices. E.g. if "BurndownAnalysis" was activated
-// through "-burndown" cmdline argument, this mapping would contain ["BurndownAnalysis"] = *true.
-func (registry *PipelineItemRegistry) AddFlags() (map[string]interface{}, map[string]*bool) {
-	flags := map[string]interface{}{}
-	deployed := map[string]*bool{}
-	for name, it := range registry.registered {
-		formatHelp := func(desc string) string {
-			return fmt.Sprintf("%s [%s]", desc, name)
-		}
-		itemIface := reflect.New(it.Elem()).Interface()
-		for _, opt := range itemIface.(PipelineItem).ListConfigurationOptions() {
-			var iface interface{}
-			switch opt.Type {
-			case BoolConfigurationOption:
-				iface = interface{}(true)
-				ptr := (**bool)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
-				*ptr = flag.Bool(opt.Flag, opt.Default.(bool), formatHelp(opt.Description))
-			case IntConfigurationOption:
-				iface = interface{}(0)
-				ptr := (**int)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
-				*ptr = flag.Int(opt.Flag, opt.Default.(int), formatHelp(opt.Description))
-			case StringConfigurationOption:
-				iface = interface{}("")
-				ptr := (**string)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
-				*ptr = flag.String(opt.Flag, opt.Default.(string), formatHelp(opt.Description))
-			}
-			flags[opt.Name] = iface
-		}
-		if fpi, ok := itemIface.(FeaturedPipelineItem); ok {
-			for _, f := range fpi.Features() {
-				featureFlags.Choices[f] = true
-			}
-		}
-		if fpi, ok := itemIface.(LeafPipelineItem); ok {
-			deployed[fpi.Name()] = flag.Bool(
-				fpi.Flag(), false, fmt.Sprintf("Runs %s analysis.", fpi.Name()))
-		}
-	}
-	{
-		// Pipeline flags
-		iface := interface{}("")
-		ptr1 := (**string)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
-		*ptr1 = flag.String("dump-dag", "", "Write the pipeline DAG to a Graphviz file.")
-		flags[ConfigPipelineDumpPath] = iface
-		iface = interface{}(true)
-		ptr2 := (**bool)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
-		*ptr2 = flag.Bool("dry-run", false, "Do not run any analyses - only resolve the DAG. "+
-			"Useful for -dump-dag.")
-		flags[ConfigPipelineDryRun] = iface
-	}
-	features := []string{}
-	for f := range featureFlags.Choices {
-		features = append(features, f)
-	}
-	flag.Var(&featureFlags, "feature",
-		fmt.Sprintf("Enables specific analysis features, can be specified "+
-			"multiple times. Available features: [%s].", strings.Join(features, ", ")))
-	return flags, deployed
-}
-
-// Registry contains all known pipeline item types.
-var Registry = &PipelineItemRegistry{
-	provided:   map[string][]reflect.Type{},
-	registered: map[string]reflect.Type{},
-	flags:      map[string]reflect.Type{},
-}
-
 type Pipeline struct {
 	// OnProgress is the callback which is invoked in Analyse() to output it's
 	// progress. The first argument is the number of processed commits and the
@@ -306,7 +185,11 @@ type Pipeline struct {
 	features map[string]bool
 }
 
-const FactPipelineCommits = "commits"
+const (
+	ConfigPipelineDumpPath = "Pipeline.DumpPath"
+	ConfigPipelineDryRun   = "Pipeline.DryRun"
+	FactPipelineCommits    = "commits"
+)
 
 func NewPipeline(repository *git.Repository) *Pipeline {
 	return &Pipeline{

+ 27 - 42
pipeline_test.go

@@ -2,12 +2,10 @@ package hercules
 
 import (
 	"errors"
-	"flag"
 	"io"
 	"io/ioutil"
 	"os"
 	"path"
-	"reflect"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -92,39 +90,6 @@ func (item *testPipelineItem) Serialize(result interface{}, binary bool, writer
 	return nil
 }
 
-func getRegistry() *PipelineItemRegistry {
-	return &PipelineItemRegistry{
-		provided:   map[string][]reflect.Type{},
-		registered: map[string]reflect.Type{},
-		flags:      map[string]reflect.Type{},
-	}
-}
-
-func TestPipelineItemRegistrySummon(t *testing.T) {
-	reg := getRegistry()
-	reg.Register(&testPipelineItem{})
-	summoned := reg.Summon((&testPipelineItem{}).Provides()[0])
-	assert.Len(t, summoned, 1)
-	assert.Equal(t, summoned[0].Name(), (&testPipelineItem{}).Name())
-	summoned = reg.Summon((&testPipelineItem{}).Name())
-	assert.Len(t, summoned, 1)
-	assert.Equal(t, summoned[0].Name(), (&testPipelineItem{}).Name())
-}
-
-func TestPipelineItemRegistryAddFlags(t *testing.T) {
-	reg := getRegistry()
-	reg.Register(&testPipelineItem{})
-	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
-	facts, deployed := reg.AddFlags()
-	assert.Len(t, facts, 3)
-	assert.IsType(t, 0, facts[(&testPipelineItem{}).ListConfigurationOptions()[0].Name])
-	assert.Contains(t, facts, ConfigPipelineDryRun)
-	assert.Contains(t, facts, ConfigPipelineDumpPath)
-	assert.Len(t, deployed, 1)
-	assert.Contains(t, deployed, (&testPipelineItem{}).Name())
-	assert.NotNil(t, flag.Lookup((&testPipelineItem{}).Flag()))
-}
-
 type dependingTestPipelineItem struct {
 	DependencySatisfied  bool
 	TestNilConsumeReturn bool
@@ -438,6 +403,33 @@ func TestCommonAnalysisResultMetadata(t *testing.T) {
 	assert.Equal(t, c1.RunTime.Nanoseconds(), int64(100*1e6))
 }
 
+func TestPipelineResolveIntegration(t *testing.T) {
+	pipeline := NewPipeline(testRepository)
+	pipeline.DeployItem(&BurndownAnalysis{})
+	pipeline.DeployItem(&CouplesAnalysis{})
+	pipeline.Initialize(nil)
+}
+
+func TestConfigurationOptionTypeString(t *testing.T) {
+	opt := ConfigurationOptionType(0)
+	assert.Equal(t, opt.String(), "")
+	opt = ConfigurationOptionType(1)
+	assert.Equal(t, opt.String(), "int")
+	opt = ConfigurationOptionType(2)
+	assert.Equal(t, opt.String(), "string")
+	opt = ConfigurationOptionType(3)
+	assert.Panics(t, func() { _ = opt.String() })
+}
+
+func TestConfigurationOptionFormatDefault(t *testing.T) {
+	opt := ConfigurationOption{Type: StringConfigurationOption, Default: "ololo"}
+	assert.Equal(t, opt.FormatDefault(), "\"ololo\"")
+	opt = ConfigurationOption{Type: IntConfigurationOption, Default: 7}
+	assert.Equal(t, opt.FormatDefault(), "7")
+	opt = ConfigurationOption{Type: BoolConfigurationOption, Default: false}
+	assert.Equal(t, opt.FormatDefault(), "false")
+}
+
 func init() {
 	cwd, err := os.Getwd()
 	if err == nil {
@@ -462,10 +454,3 @@ func init() {
 		URL: "https://github.com/src-d/hercules",
 	})
 }
-
-func TestPipelineResolveIntegration(t *testing.T) {
-	pipeline := NewPipeline(testRepository)
-	pipeline.DeployItem(&BurndownAnalysis{})
-	pipeline.DeployItem(&CouplesAnalysis{})
-	pipeline.Initialize(nil)
-}

+ 214 - 0
registry.go

@@ -0,0 +1,214 @@
+package hercules
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"sort"
+	"strings"
+	"unsafe"
+
+	"github.com/spf13/pflag"
+)
+
+// PipelineItemRegistry contains all the known PipelineItem-s.
+type PipelineItemRegistry struct {
+	provided   map[string][]reflect.Type
+	registered map[string]reflect.Type
+	flags      map[string]reflect.Type
+}
+
+// Register adds another PipelineItem to the registry.
+func (registry *PipelineItemRegistry) Register(example PipelineItem) {
+	t := reflect.TypeOf(example)
+	registry.registered[example.Name()] = t
+	if fpi, ok := example.(LeafPipelineItem); ok {
+		registry.flags[fpi.Flag()] = t
+	}
+	for _, dep := range example.Provides() {
+		ts := registry.provided[dep]
+		if ts == nil {
+			ts = []reflect.Type{}
+		}
+		ts = append(ts, t)
+		registry.provided[dep] = ts
+	}
+}
+
+func (registry *PipelineItemRegistry) Summon(providesOrName string) []PipelineItem {
+	if registry.provided == nil {
+		return []PipelineItem{}
+	}
+	ts := registry.provided[providesOrName]
+	items := []PipelineItem{}
+	for _, t := range ts {
+		items = append(items, reflect.New(t.Elem()).Interface().(PipelineItem))
+	}
+	if t, exists := registry.registered[providesOrName]; exists {
+		items = append(items, reflect.New(t.Elem()).Interface().(PipelineItem))
+	}
+	return items
+}
+
+func (registry *PipelineItemRegistry) GetLeaves() []LeafPipelineItem {
+	keys := []string{}
+	for key := range registry.flags {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	items := []LeafPipelineItem{}
+	for _, key := range keys {
+		items = append(items, reflect.New(registry.flags[key].Elem()).Interface().(LeafPipelineItem))
+	}
+	return items
+}
+
+func (registry *PipelineItemRegistry) GetPlumbingItems() []PipelineItem {
+	keys := []string{}
+	for key := range registry.registered {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	items := []PipelineItem{}
+	for _, key := range keys {
+		iface := reflect.New(registry.registered[key].Elem()).Interface()
+		if _, ok := iface.(LeafPipelineItem); !ok {
+			items = append(items, iface.(PipelineItem))
+		}
+	}
+	return items
+}
+
+type orderedFeaturedItems []FeaturedPipelineItem
+
+func (ofi orderedFeaturedItems) Len() int {
+	return len([]FeaturedPipelineItem(ofi))
+}
+
+func (ofi orderedFeaturedItems) Less(i, j int) bool {
+	cofi := []FeaturedPipelineItem(ofi)
+	return cofi[i].Name() < cofi[j].Name()
+}
+
+func (ofi orderedFeaturedItems) Swap(i, j int) {
+	cofi := []FeaturedPipelineItem(ofi)
+	cofi[i], cofi[j] = cofi[j], cofi[i]
+}
+
+func (registry *PipelineItemRegistry) GetFeaturedItems() map[string][]FeaturedPipelineItem {
+	features := map[string][]FeaturedPipelineItem{}
+	for _, t := range registry.registered {
+		if fiface, ok := reflect.New(t.Elem()).Interface().(FeaturedPipelineItem); ok {
+			for _, f := range fiface.Features() {
+				list := features[f]
+				if list == nil {
+					list = []FeaturedPipelineItem{}
+				}
+				list = append(list, fiface)
+				features[f] = list
+			}
+		}
+	}
+	for _, vals := range features {
+		sort.Sort(orderedFeaturedItems(vals))
+	}
+	return features
+}
+
+type arrayFeatureFlags struct {
+	// Flags containts the features activated through the command line.
+	Flags []string
+	// Choices contains all registered features.
+	Choices map[string]bool
+}
+
+func (acf *arrayFeatureFlags) String() string {
+	return strings.Join([]string(acf.Flags), ", ")
+}
+
+func (acf *arrayFeatureFlags) Set(value string) error {
+	if _, exists := acf.Choices[value]; !exists {
+		return errors.New(fmt.Sprintf("Feature \"%s\" is not registered.", value))
+	}
+	acf.Flags = append(acf.Flags, value)
+	return nil
+}
+
+func (acf *arrayFeatureFlags) Type() string {
+	return "string"
+}
+
+var featureFlags = arrayFeatureFlags{Flags: []string{}, Choices: map[string]bool{}}
+
+// AddFlags inserts the cmdline options from PipelineItem.ListConfigurationOptions(),
+// FeaturedPipelineItem().Features() and LeafPipelineItem.Flag() into the global "flag" parser
+// built into the Go runtime.
+// Returns the "facts" which can be fed into PipelineItem.Configure() and the dictionary of
+// runnable analysis (LeafPipelineItem) choices. E.g. if "BurndownAnalysis" was activated
+// through "-burndown" cmdline argument, this mapping would contain ["BurndownAnalysis"] = *true.
+func (registry *PipelineItemRegistry) AddFlags(flagSet *pflag.FlagSet) (
+	map[string]interface{}, map[string]*bool) {
+	flags := map[string]interface{}{}
+	deployed := map[string]*bool{}
+	for name, it := range registry.registered {
+		formatHelp := func(desc string) string {
+			return fmt.Sprintf("%s [%s]", desc, name)
+		}
+		itemIface := reflect.New(it.Elem()).Interface()
+		for _, opt := range itemIface.(PipelineItem).ListConfigurationOptions() {
+			var iface interface{}
+			switch opt.Type {
+			case BoolConfigurationOption:
+				iface = interface{}(true)
+				ptr := (**bool)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
+				*ptr = flagSet.Bool(opt.Flag, opt.Default.(bool), formatHelp(opt.Description))
+			case IntConfigurationOption:
+				iface = interface{}(0)
+				ptr := (**int)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
+				*ptr = flagSet.Int(opt.Flag, opt.Default.(int), formatHelp(opt.Description))
+			case StringConfigurationOption:
+				iface = interface{}("")
+				ptr := (**string)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
+				*ptr = flagSet.String(opt.Flag, opt.Default.(string), formatHelp(opt.Description))
+			}
+			flags[opt.Name] = iface
+		}
+		if fpi, ok := itemIface.(FeaturedPipelineItem); ok {
+			for _, f := range fpi.Features() {
+				featureFlags.Choices[f] = true
+			}
+		}
+		if fpi, ok := itemIface.(LeafPipelineItem); ok {
+			deployed[fpi.Name()] = flagSet.Bool(
+				fpi.Flag(), false, fmt.Sprintf("Runs %s analysis.", fpi.Name()))
+		}
+	}
+	{
+		// Pipeline flags
+		iface := interface{}("")
+		ptr1 := (**string)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
+		*ptr1 = flagSet.String("dump-dag", "", "Write the pipeline DAG to a Graphviz file.")
+		flags[ConfigPipelineDumpPath] = iface
+		iface = interface{}(true)
+		ptr2 := (**bool)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface)) + unsafe.Sizeof(&iface)))
+		*ptr2 = flagSet.Bool("dry-run", false, "Do not run any analyses - only resolve the DAG. "+
+			"Useful for -dump-dag.")
+		flags[ConfigPipelineDryRun] = iface
+	}
+	features := []string{}
+	for f := range featureFlags.Choices {
+		features = append(features, f)
+	}
+	flagSet.Var(&featureFlags, "feature",
+		fmt.Sprintf("Enables the items which depend on the specified features. Can be specified "+
+			"multiple times. Available features: [%s] (see --feature below).",
+			strings.Join(features, ", ")))
+	return flags, deployed
+}
+
+// Registry contains all known pipeline item types.
+var Registry = &PipelineItemRegistry{
+	provided:   map[string][]reflect.Type{},
+	registered: map[string]reflect.Type{},
+	flags:      map[string]reflect.Type{},
+}

+ 191 - 0
registry_test.go

@@ -0,0 +1,191 @@
+package hercules
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/src-d/go-git.v4"
+)
+
+func getRegistry() *PipelineItemRegistry {
+	return &PipelineItemRegistry{
+		provided:   map[string][]reflect.Type{},
+		registered: map[string]reflect.Type{},
+		flags:      map[string]reflect.Type{},
+	}
+}
+
+type dummyPipelineItem struct{}
+
+func (item *dummyPipelineItem) Name() string {
+	return "dummy"
+}
+
+func (item *dummyPipelineItem) Provides() []string {
+	arr := [...]string{"dummy"}
+	return arr[:]
+}
+
+func (item *dummyPipelineItem) Requires() []string {
+	return []string{}
+}
+
+func (item *dummyPipelineItem) Features() []string {
+	arr := [...]string{"power"}
+	return arr[:]
+}
+
+func (item *dummyPipelineItem) Configure(facts map[string]interface{}) {
+}
+
+func (item *dummyPipelineItem) ListConfigurationOptions() []ConfigurationOption {
+	options := [...]ConfigurationOption{{
+		Name:        "DummyOption",
+		Description: "The option description.",
+		Flag:        "dummy-option",
+		Type:        BoolConfigurationOption,
+		Default:     false,
+	}}
+	return options[:]
+}
+
+func (item *dummyPipelineItem) Initialize(repository *git.Repository) {}
+
+func (item *dummyPipelineItem) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	return map[string]interface{}{"dummy": nil}, nil
+}
+
+type dummyPipelineItem2 struct{}
+
+func (item *dummyPipelineItem2) Name() string {
+	return "dummy2"
+}
+
+func (item *dummyPipelineItem2) Provides() []string {
+	arr := [...]string{"dummy2"}
+	return arr[:]
+}
+
+func (item *dummyPipelineItem2) Requires() []string {
+	return []string{}
+}
+
+func (item *dummyPipelineItem2) Features() []string {
+	arr := [...]string{"other"}
+	return arr[:]
+}
+
+func (item *dummyPipelineItem2) Configure(facts map[string]interface{}) {
+}
+
+func (item *dummyPipelineItem2) ListConfigurationOptions() []ConfigurationOption {
+	return []ConfigurationOption{}
+}
+
+func (item *dummyPipelineItem2) Initialize(repository *git.Repository) {}
+
+func (item *dummyPipelineItem2) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+	return map[string]interface{}{"dummy2": nil}, nil
+}
+
+func TestRegistrySummon(t *testing.T) {
+	reg := getRegistry()
+	reg.Register(&testPipelineItem{})
+	summoned := reg.Summon((&testPipelineItem{}).Provides()[0])
+	assert.Len(t, summoned, 1)
+	assert.Equal(t, summoned[0].Name(), (&testPipelineItem{}).Name())
+	summoned = reg.Summon((&testPipelineItem{}).Name())
+	assert.Len(t, summoned, 1)
+	assert.Equal(t, summoned[0].Name(), (&testPipelineItem{}).Name())
+}
+
+func TestRegistryAddFlags(t *testing.T) {
+	reg := getRegistry()
+	reg.Register(&testPipelineItem{})
+	reg.Register(&dummyPipelineItem{})
+	testCmd := &cobra.Command{
+		Use:   "test",
+		Short: "Temporary command to test the stuff.",
+		Long:  ``,
+		Args:  cobra.MaximumNArgs(0),
+		Run:   func(cmd *cobra.Command, args []string) {},
+	}
+	facts, deployed := reg.AddFlags(testCmd.Flags())
+	assert.Len(t, facts, 4)
+	assert.IsType(t, 0, facts[(&testPipelineItem{}).ListConfigurationOptions()[0].Name])
+	assert.IsType(t, true, facts[(&dummyPipelineItem{}).ListConfigurationOptions()[0].Name])
+	assert.Contains(t, facts, ConfigPipelineDryRun)
+	assert.Contains(t, facts, ConfigPipelineDumpPath)
+	assert.Len(t, deployed, 1)
+	assert.Contains(t, deployed, (&testPipelineItem{}).Name())
+	assert.NotNil(t, testCmd.Flags().Lookup((&testPipelineItem{}).Flag()))
+	assert.NotNil(t, testCmd.Flags().Lookup("feature"))
+	assert.NotNil(t, testCmd.Flags().Lookup("dump-dag"))
+	assert.NotNil(t, testCmd.Flags().Lookup("dry-run"))
+	assert.NotNil(t, testCmd.Flags().Lookup(
+		(&testPipelineItem{}).ListConfigurationOptions()[0].Flag))
+	assert.NotNil(t, testCmd.Flags().Lookup(
+		(&dummyPipelineItem{}).ListConfigurationOptions()[0].Flag))
+	testCmd.UsageString() // to test that nothing is broken
+}
+
+func TestRegistryFeatures(t *testing.T) {
+	reg := getRegistry()
+	reg.Register(&dummyPipelineItem{})
+	reg.Register(&dummyPipelineItem2{})
+	testCmd := &cobra.Command{
+		Use:   "test",
+		Short: "Temporary command to test the stuff.",
+		Long:  ``,
+		Args:  cobra.MaximumNArgs(0),
+		Run:   func(cmd *cobra.Command, args []string) {},
+	}
+	reg.AddFlags(testCmd.Flags())
+	args := [...]string{"--feature", "other", "--feature", "power"}
+	testCmd.ParseFlags(args[:])
+	pipeline := NewPipeline(testRepository)
+	val, _ := pipeline.GetFeature("power")
+	assert.False(t, val)
+	val, _ = pipeline.GetFeature("other")
+	assert.False(t, val)
+	pipeline.SetFeaturesFromFlags()
+	val, _ = pipeline.GetFeature("power")
+	assert.True(t, val)
+	val, _ = pipeline.GetFeature("other")
+	assert.True(t, val)
+}
+
+func TestRegistryLeaves(t *testing.T) {
+	reg := getRegistry()
+	reg.Register(&testPipelineItem{})
+	reg.Register(&dependingTestPipelineItem{})
+	reg.Register(&dummyPipelineItem{})
+	leaves := reg.GetLeaves()
+	assert.Len(t, leaves, 2)
+	assert.Equal(t, leaves[0].Name(), (&dependingTestPipelineItem{}).Name())
+	assert.Equal(t, leaves[1].Name(), (&testPipelineItem{}).Name())
+}
+
+func TestRegistryPlumbingItems(t *testing.T) {
+	reg := getRegistry()
+	reg.Register(&testPipelineItem{})
+	reg.Register(&dependingTestPipelineItem{})
+	reg.Register(&dummyPipelineItem{})
+	plumbing := reg.GetPlumbingItems()
+	assert.Len(t, plumbing, 1)
+	assert.Equal(t, plumbing[0].Name(), (&dummyPipelineItem{}).Name())
+}
+
+func TestRegistryFeaturedItems(t *testing.T) {
+	reg := getRegistry()
+	reg.Register(&testPipelineItem{})
+	reg.Register(&dependingTestPipelineItem{})
+	reg.Register(&dummyPipelineItem{})
+	featured := reg.GetFeaturedItems()
+	assert.Len(t, featured, 1)
+	assert.Len(t, featured["power"], 2)
+	assert.Equal(t, featured["power"][0].Name(), (&testPipelineItem{}).Name())
+	assert.Equal(t, featured["power"][1].Name(), (&dummyPipelineItem{}).Name())
+}

+ 15 - 0
version.go

@@ -1,3 +1,18 @@
 package hercules
 
+import (
+	"reflect"
+	"strconv"
+	"strings"
+)
+
 var GIT_HASH = "<unknown>"
+
+var VERSION = 0
+
+type versionProbe struct{}
+
+func init() {
+	parts := strings.Split(reflect.TypeOf(versionProbe{}).PkgPath(), ".")
+	VERSION, _ = strconv.Atoi(parts[len(parts)-1][1:])
+}