diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..9df718e
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0b1a7ab
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,56 @@
+_Store
+*.[56789ao]
+*.a[56789o]
+*.so
+*.pyc
+._*
+.nfs.*
+[56789a].out
+*~
+*.orig
+*.rej
+.*.swp
+core
+*.cgo*.go
+*.cgo*.c
+_cgo_*
+_obj
+_test
+_testmain.go
+
+/VERSION.cache
+/bin/
+/build.out
+/doc/articles/wiki/*.bin
+/goinstall.log
+/last-change
+/misc/cgo/life/run.out
+/misc/cgo/stdio/run.out
+/misc/cgo/testso/main
+/pkg/
+/src/*.*/
+/src/cmd/cgo/zdefaultcc.go
+/src/cmd/dist/dist
+/src/cmd/go/internal/cfg/zdefaultcc.go
+/src/cmd/go/internal/cfg/zosarch.go
+/src/cmd/internal/objabi/zbootstrap.go
+/src/go/build/zcgo.go
+/src/go/doc/headscan
+/src/runtime/internal/sys/zversion.go
+/src/unicode/maketables
+/test.out
+/test/garbage/*.out
+/test/pass.out
+/test/run.out
+/test/times.out
+/.idea/
+.idea
+/.idea
+
+# This file includes artifacts of Go build that should not be checked in.
+# For files created by specific development environment (e.g. editor),
+# use alternative ways to exclude files from git.
+# For example, set up .git/info/exclude or use a global .gitignore.
+
+/repos
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..39f6773
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,18 @@
+language: go
+
+go:
+ - master
+
+install:
+ - go mod download
+
+notifications:
+ email: true
+
+os: osx
+
+script:
+ - go test -race -coverprofile=../coverage.txt -covermode=atomic
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..99069da
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,7 @@
+Copyright (c) 2020
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..13d50f6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+ four-key Metrics
+
+
+
+ four-key is a measure tool written with golang for your repositories. This project inspired from Accelerate book.
+
+ Thoughtworks Technology Radar
+
+
+
+ Documentation on Gitbook 🔗
+
+
+
+
+
+
+
+This package provides cli-tool for your commits. You can measure lead time, deployment frequency, mean time to restore (MTTR) and change fail percentage of your repositories. If you want to contribute this package, please fork and [create](https://github.com/{user}/{repository-name}/pulls) a pull request.
+
+# Dependencies
+* git
+
+# Prerequisites
+
+
+ All releases must be tagged with a specific prefix and version number [prefix][versionNumber] like release-v15 or v-[commitSha] or rel12
+ Must be provided with -p yourTagPattern while adding repository OR add to config.json
+ All fixes must be specific like fix - hotfix - hot-fix
+ Must be provided with -c yourFixCommitPattern -c yourOtherCommitFixPattern -c yourOtherCommitFixPattern while adding repository OR add to config.json
+
+
+
+# Installation
+
+Executables
+
+* Mac 64-bit: https://github.com/trendyol/
+* Linux 64-bit: https://github.com/trendyol/
+* Windows 64-bit: https://github.com/trendyol/
+
+```
+Source Code Build
+$ git clone https://github.com/Trendyol/four-key.git && cd four-key
+$ go build
+```
+
+# Preparation
+````cli
+// Add repository with CLI
+// OR Add repository with configuration file - Sample config.json
+{
+ "repositories":[
+ {
+ "teamName":"cs",
+ "cloneAddress": "https://github.com/Trendyol/reponame",
+ "releaseTagPattern": "v-",
+ "fixCommitPatterns": ["fix","hot-fix","hotfix"]
+ }
+ ]
+}
+
+````
+[![asciicast](https://asciinema.org/a/MHuwLNKOT9mifuCKPfy2QIUD9.svg)](https://asciinema.org/a/MHuwLNKOT9mifuCKPfy2QIUD9)
+````
+add command usage
+
+$ four-key add [flags]
+
+Flags:
+ -c, --cloneAddress string Set your clone address
+ -f, --fixCommitPatterns stringArray Set your fix commit patterns of repository
+ -h, --help help for add
+ -r, --releaseTagPattern string Set your release tag pattern of repository
+ -t, --team string Set your team of repository
+
+````
+
+````
+list command usage
+
+$ four-key list [flags]
+
+Flags:
+ -h, --help help for remove
+
+````
+
+````
+remove command usage
+
+$ four-key remove [flags]
+
+Flags:
+ -h, --help help for remove
+ -r, --repository string Set your repository name to remove from config
+
+````
+
+[![asciicast](https://asciinema.org/a/dqIcYF1HXSIscgRdy2zkPOaUq.svg)](https://asciinema.org/a/dqIcYF1HXSIscgRdy2zkPOaUq)
+
+````
+set command usage
+
+Usage:
+ four-key set [flags]
+
+Flags:
+ -h, --help help for set
+ -o, --output string Set output source of 4Key metric results
+````
+
+# Generate
+````
+$ four-key run --startDate 2018-01-13 --endDate 2020-01-30
+
+Usage:
+ four-key run [flags]
+
+Flags:
+ -e, --endDate string Set a end date of range
+ -h, --help help for run
+ -r, --repository string Set a name of the specific repository
+ -s, --startDate string Set a start date of range
+
+````
+
+[![asciicast](https://asciinema.org/a/XSWFQHdyz8rnaSG7VUqpoaa8F.svg)](https://asciinema.org/a/XSWFQHdyz8rnaSG7VUqpoaa8F)
+
+# TODO
+* Add calculation descriptions for all metrics (In Progress)
+* Add brew & scoop as installation options (In Progress)
+* Complete tests (In Progress)
+* Add CI
+* Complete Charts
+* Add suggestions for metrics results
+
+# License
+The MIT License (MIT) - see [`LICENSE.md`](https://github.com/Trendyol/four-key/LICENSE.md) for more details
\ No newline at end of file
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..ca7fee6
Binary files /dev/null and b/assets/logo.png differ
diff --git a/cmd/repository_command.go b/cmd/repository_command.go
new file mode 100644
index 0000000..c6bcc66
--- /dev/null
+++ b/cmd/repository_command.go
@@ -0,0 +1,226 @@
+package cmd
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ Command "four-key/command"
+ "four-key/helpers"
+ "four-key/models"
+ "four-key/settings"
+ "github.com/spf13/cobra"
+ "io/ioutil"
+ "os"
+ "path"
+ "sort"
+ "text/tabwriter"
+)
+
+var addCommand = &cobra.Command{
+ Use: "add",
+ Short: "Add repository",
+ Long: "Add repository",
+ Run: onAddRepository,
+}
+
+var removeCommand = &cobra.Command{
+ Use: "remove",
+ Short: "Remove repository",
+ Long: "Remove repository",
+ Run: onRemoveRepository,
+}
+
+var listCommand = &cobra.Command{
+ Use: "list",
+ Short: "List repositories",
+ Long: "List repositories",
+ Run: onListRepositories,
+}
+
+func init() {
+ var empty []string
+ rootCmd.AddCommand(addCommand)
+ settings.Initialize(Command.ACommander())
+ addCommand.Flags().StringP("cloneAddress", "c", "", "Set your clone address")
+ addCommand.Flags().StringP("team", "t", "", "Set your team of repository")
+ addCommand.Flags().StringP("releaseTagPattern", "r", "", "Set your release tag pattern of repository")
+ addCommand.Flags().StringArrayP("fixCommitPatterns", "f", empty, "Set your fix commit patterns of repository")
+
+ rootCmd.AddCommand(removeCommand)
+ removeCommand.Flags().StringP("repository", "r", "", "Set your repository name to remove from config")
+
+ rootCmd.AddCommand(listCommand)
+}
+
+func writeFile(document models.Document) error {
+ doc, err := json.Marshal(document)
+ if err != nil {
+ return errors.New("json convert error")
+ }
+
+ err = ioutil.WriteFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName), doc, os.FileMode(0644))
+ if err != nil {
+ return errors.New("file write error")
+ }
+
+ return nil
+}
+
+func onAddRepository(cmd *cobra.Command, args []string) {
+ cloneAddress, err := cmd.Flags().GetString("cloneAddress")
+ team, err := cmd.Flags().GetString("team")
+ tagPattern, err := cmd.Flags().GetString("releaseTagPattern")
+ commitPatterns, err := cmd.Flags().GetStringArray("fixCommitPatterns")
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("an error occurred while adding repository, please check entered inputs."))
+ return
+ }
+
+ document := models.Document{}
+
+ fmt.Println(Command.ACommander().Good("Reading your configuration file..."))
+ existFile, err := ioutil.ReadFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("The file does not exist!"))
+
+ existFile, err = ioutil.ReadFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("The file does not exist!"), err)
+ return
+ }
+ }
+
+ err = json.Unmarshal(existFile, &document)
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("json parse error."))
+ return
+ }
+
+ document.Repositories = append(document.Repositories, &models.DocumentRepository{
+ TeamName: team,
+ CloneAddress: cloneAddress,
+ ReleaseTagPattern: tagPattern,
+ FixCommitPatterns: commitPatterns,
+ })
+
+ err = writeFile(document)
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal(err.Error()))
+ return
+ }
+
+ fmt.Println(Command.ACommander().Good("successfully added your repository to config file."))
+
+ s, err = settings.Get()
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal(err))
+ }
+
+ err = helpers.CloneRepository(cloneAddress, s.RepositoriesPath)
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal(err))
+ return
+ }
+}
+
+func onRemoveRepository(cmd *cobra.Command, args []string) {
+ repository, err := cmd.Flags().GetString("repository")
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("an error occurred while removing repository, please check entered inputs."))
+ return
+ }
+
+ document := models.Document{}
+
+ fmt.Println(Command.ACommander().Good("Reading your configuration file..."))
+ existFile, err := ioutil.ReadFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("The file does not exist!"))
+ existFile, err = ioutil.ReadFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("The file does not exist!"), err)
+ return
+ }
+ }
+
+ err = json.Unmarshal([]byte(string(existFile)), &document)
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("json parse error."))
+ return
+ }
+
+ isRemoved := false
+ var newRepositories []*models.DocumentRepository
+ for _, documentRepository := range document.Repositories {
+ if repository != helpers.GetNameByRepositoryCloneUrl(documentRepository.CloneAddress) {
+ newRepositories = append(newRepositories, documentRepository)
+ } else {
+ isRemoved = true
+ }
+ }
+
+ document.Repositories = newRepositories
+
+ err = writeFile(document)
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal(err.Error()))
+ return
+ }
+
+ if isRemoved {
+ fmt.Println(Command.ACommander().Good(fmt.Sprintf("successfully removed %s repository from the config file.", repository)))
+ } else {
+ fmt.Println(Command.ACommander().Fatal(fmt.Sprintf("The %s repository does not exist!", repository)))
+ }
+}
+
+func onListRepositories(cmd *cobra.Command, args []string) {
+ document := models.Document{}
+
+ fmt.Println(Command.ACommander().Good("Reading your configuration file..."))
+ existFile, err := ioutil.ReadFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("The file does not exist!"))
+ existFile, err = ioutil.ReadFile(path.Join(Command.ACommander().GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("The file does not exist!"), err)
+ return
+ }
+ }
+
+ err = json.Unmarshal(existFile, &document)
+ if err != nil {
+ fmt.Println(Command.ACommander().Fatal("json parse error."))
+ return
+ }
+
+ w := new(tabwriter.Writer)
+ w.Init(os.Stdout, 40, 10, 0, '\t', 0)
+
+ fmt.Println(fmt.Sprintf("\nTotal %d repository/repositories has been found.", len(document.Repositories)))
+
+ sort.Slice(document.Repositories, func(i, j int) bool {
+ return document.Repositories[i].TeamName < document.Repositories[j].TeamName
+ })
+
+ for i, repository := range document.Repositories {
+ if i == 0 {
+ _, _ = fmt.Fprintln(w, fmt.Sprintf("\nTeam: %s", repository.TeamName))
+ }
+
+ _, _ = fmt.Fprintln(w, fmt.Sprintf("%d. %s\t%s", i+1, helpers.GetNameByRepositoryCloneUrl(repository.CloneAddress), repository.CloneAddress))
+ if i < (len(document.Repositories)-1) && repository.TeamName != document.Repositories[i+1].TeamName {
+ _, _ = fmt.Fprintln(w, fmt.Sprintf("\nTeam: %s", document.Repositories[i+1].TeamName))
+ }
+ }
+ _ = w.Flush()
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..716a4f5
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,19 @@
+package cmd
+
+import (
+ "fmt"
+ "github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "four-key",
+ Short: "four-key Metrics Command",
+ Long: "four-key Metrics Command",
+}
+
+func Execute() {
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Println(err)
+ panic(err)
+ }
+}
diff --git a/cmd/run_command.go b/cmd/run_command.go
new file mode 100644
index 0000000..360ffce
--- /dev/null
+++ b/cmd/run_command.go
@@ -0,0 +1,384 @@
+package cmd
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ Command "four-key/command"
+ . "four-key/helpers"
+ . "four-key/models"
+ "four-key/settings"
+ "four-key/template"
+ "github.com/spf13/cobra"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var runCommand = &cobra.Command{
+ Use: "run",
+ Short: "run repository",
+ Long: "run repository",
+ Run: onRun,
+}
+
+type ChartItem struct {
+ Date string `json:"date"`
+ Value float64 `json:"value"`
+ Name string `json:"name"`
+}
+
+type ChartItems struct {
+ meanTimes []ChartItem
+ leadTimes []ChartItem
+ failPercentages []ChartItem
+ deploymentFrequencies []ChartItem
+ teamName string
+}
+
+type SeparatedTeamItems struct {
+ teams map[string][]ChartItems
+}
+
+var allChartItems []ChartItems
+var separatedTeamItems SeparatedTeamItems
+var commander Command.ICommand
+var s *settings.Settings
+
+const deploymentInitialValue = 1
+
+func init() {
+ rootCmd.AddCommand(runCommand)
+ runCommand.Flags().StringP("startDate", "s", "", "Set a start date of range")
+ runCommand.Flags().StringP("endDate", "e", "", "Set a end date of range")
+ runCommand.Flags().StringP("repository", "r", "", "Set a name of the specific repository")
+ commander = Command.ACommander()
+}
+
+func onRun(cmd *cobra.Command, args []string) {
+ repositoryName, err := cmd.Flags().GetString("repository")
+ startDateInput, err := cmd.Flags().GetString("startDate")
+ startDate, err := time.Parse(settings.DefaultDateFormat, startDateInput)
+
+ if err != nil {
+ fmt.Println(commander.Warn("invalid start date, start date will be like -s YYYY-MM-DD"))
+ return
+ }
+
+ endDateInput, err := cmd.Flags().GetString("endDate")
+ endDate, err := time.Parse(settings.DefaultDateFormat, endDateInput)
+
+ if err != nil {
+ fmt.Println(commander.Warn("invalid end date, end date will be like -s YYYY-MM-DD"))
+ return
+ }
+
+ err = settings.Initialize(commander)
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error()))
+ return
+ }
+
+ s, err = settings.Get()
+ if err != nil || s == nil {
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error()))
+ return
+ }
+
+ fmt.Println(commander.Fatal("configurations didn't loaded"))
+ return
+ }
+
+ var repositories []RepositoryWrapper
+ if repositoryName != "" {
+ repository, err := GetRepositoryByName(s, repositoryName)
+
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error()))
+ return
+ }
+
+ repositories = append(repositories, repository)
+
+ } else {
+ repositories, err = GetRepositories(s)
+ }
+
+ if err != nil {
+ fmt.Println(commander.Fatal("git clone returned an error - error -> ", err.Error()))
+ return
+ }
+
+ t := strings.Trim(startDate.Format(settings.DefaultDateFormat)+"--"+endDate.Format(settings.DefaultDateFormat), "")
+
+ var metricResultDtoList []FourKeyMetricResultDto
+
+ for _, repo := range repositories {
+ metricsRequest := MetricsRequest{
+ StartDate: startDate,
+ EndDate: endDate,
+ ReleaseTagPattern: repo.Configurations.ReleaseTagPattern,
+ FixPatterns: repo.Configurations.FixCommitPatterns,
+ }
+
+ metricsDto, err := CalculateMetrics(repo.Repository, metricsRequest)
+
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error(), " Project -> ", repo.Configurations.Name()))
+ continue
+ }
+
+ metricsDto.RepoName = repo.Configurations.Name()
+ metricsDto.TeamName = repo.Configurations.TeamName
+ metricResultDtoList = append(metricResultDtoList, metricsDto)
+ }
+
+ generateMetricFiles(metricResultDtoList, t)
+}
+
+func generateMetricFiles(metricResultDtoList []FourKeyMetricResultDto, reportTimeAsString string) {
+ outputSource := path.Join(s.Output, settings.DefaultGeneratedFileOutputDirName)
+ separatedTeamItems.teams = map[string][]ChartItems{}
+ err := CheckDirectory(outputSource)
+
+ if err != nil {
+ _ = CreateDirectory(s.Output, settings.DefaultGeneratedFileOutputDirName)
+ }
+
+ err = CheckDirectory(outputSource, settings.AllTeamsDefaultDirName)
+
+ if err != nil {
+ _ = CreateDirectory(outputSource, settings.AllTeamsDefaultDirName)
+ }
+
+ err = CheckDirectory(outputSource, settings.TeamBasedDefaultDirName)
+
+ if err != nil {
+ _ = CreateDirectory(outputSource, settings.TeamBasedDefaultDirName)
+ }
+
+ for i, metric := range metricResultDtoList {
+ dirName := metric.RepoName + "_" + reportTimeAsString
+
+ if metric.TeamName == "" {
+ metric.TeamName = settings.DefaultTeamName
+ }
+
+ err = CheckDirectory(outputSource, metric.TeamName)
+
+ if err != nil {
+ _ = CreateDirectory(outputSource, metric.TeamName)
+ }
+
+ dirName, err := generateDirectory(path.Join(outputSource, metric.TeamName), dirName)
+
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error()))
+ return
+ }
+
+ metrics := createChartItems(metric.MetricTags)
+ metrics.teamName = metric.TeamName
+ allChartItems = append(allChartItems, metrics)
+ separatedTeamItems.teams[metrics.teamName] = append(separatedTeamItems.teams[metrics.teamName], metrics)
+ err = generateOutput(dirName, metrics, metric, outputSource, false)
+
+ if i == len(metricResultDtoList)-1 {
+ var teamBasedMetrics []ChartItems
+ var allTeamsMetrics ChartItems
+ allTeamsMetrics.teamName = "AllTeamsResult"
+
+ for name, chartItems := range separatedTeamItems.teams {
+ teamChart := ChartItems{
+ teamName: name,
+ }
+ mergeChartItems(chartItems, &teamChart)
+ teamBasedMetrics = append(teamBasedMetrics, teamChart)
+ }
+
+ mergeChartItems(allChartItems, &allTeamsMetrics)
+
+ for _, team := range teamBasedMetrics {
+ metricDto := FourKeyMetricResultDto{
+ RepoName: settings.TeamBasedDefaultDirName,
+ TeamName: settings.TeamBasedDefaultDirName,
+ DateRangeStart: metric.DateRangeStart,
+ DateRangeEnd: metric.DateRangeEnd,
+ CreationDate: metric.CreationDate,
+ DeploymentFrequencyCount: 0,
+ }
+
+ dirName, err := generateDirectory(path.Join(outputSource, settings.TeamBasedDefaultDirName), team.teamName+"_"+reportTimeAsString)
+
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error()))
+ return
+ }
+
+ err = generateOutput(dirName, team, metricDto, outputSource, false)
+ }
+
+ metricDto := FourKeyMetricResultDto{
+ RepoName: settings.AllTeamsDefaultDirName,
+ TeamName: settings.AllTeamsDefaultDirName,
+ DateRangeStart: metric.DateRangeStart,
+ DateRangeEnd: metric.DateRangeEnd,
+ CreationDate: metric.CreationDate,
+ DeploymentFrequencyCount: 0,
+ }
+
+ dirName, err := generateDirectory(path.Join(outputSource, settings.AllTeamsDefaultDirName), allTeamsMetrics.teamName+"_"+reportTimeAsString)
+
+ if err != nil {
+ fmt.Println(commander.Fatal(err.Error()))
+ return
+ }
+
+ err = generateOutput(dirName, allTeamsMetrics, metricDto, outputSource, true)
+
+ if err != nil {
+ fmt.Println(commander.Warn("an error occurred while opening results folder", " you can see in -> ", path.Join(commander.GetFourKeyPath(), outputSource)))
+ }
+ } else {
+ if err != nil {
+ fmt.Println(commander.Warn("an error occurred while opening results folder", " you can see in -> ", path.Join(commander.GetFourKeyPath(), outputSource)))
+ }
+ }
+ }
+}
+
+func createChartItems(metrics []TagMetricDto) ChartItems {
+ var chartItems ChartItems
+ for _, t := range metrics {
+ chartItems.meanTimes = append(chartItems.meanTimes, ChartItem{
+ Date: t.TagDate.Format(settings.DefaultDateFormat),
+ Value: t.MeanTimeRestoreAverage,
+ })
+ chartItems.leadTimes = append(chartItems.leadTimes, ChartItem{
+ Date: t.TagDate.Format(settings.DefaultDateFormat),
+ Value: t.LeadTime,
+ })
+ chartItems.failPercentages = append(chartItems.failPercentages, ChartItem{
+ Date: t.TagDate.Format(settings.DefaultDateFormat),
+ Value: t.ChangeFailPercentage,
+ })
+ chartItems.deploymentFrequencies = append(chartItems.deploymentFrequencies, ChartItem{
+ Date: t.TagDate.Format(settings.DefaultDateFormat),
+ Value: deploymentInitialValue,
+ Name: t.TagName,
+ })
+ }
+
+ return chartItems
+}
+
+func generateOutput(dir string, items ChartItems, results FourKeyMetricResultDto, outputSource string, open bool) error {
+ var h *os.File
+ h, err := os.Create(path.Join(outputSource, results.TeamName, dir, "index.html"))
+
+ if err != nil {
+ return err
+ }
+
+ html, err := createHtml(results, items)
+
+ if err != nil {
+ return err
+ }
+
+ _, err = h.WriteString(html)
+
+ if err != nil {
+ err = h.Close()
+
+ if err != nil {
+ return err
+ }
+
+ return err
+ }
+
+ if open {
+ err := commander.Open(outputSource)
+
+ if err != nil {
+ fmt.Println(commander.Warn("an error occurred while opening results folder", " you can see in -> ", path.Join(commander.GetFourKeyPath(), outputSource)))
+ }
+ }
+
+ fmt.Println(commander.Good("metrics file generated", " for -> ", results.TeamName, ":", results.RepoName, "in -> ", path.Join(outputSource, results.TeamName)))
+
+ return nil
+}
+
+func generateDirectory(sourceDir, dir string) (string, error) {
+ err := CreateDirectory(sourceDir, dir)
+
+ if err != nil {
+ counter := 0
+ for {
+ counter++
+
+ if counter > 1000 {
+ return "", errors.New("while true")
+ }
+
+ d := dir + "_" + strconv.Itoa(counter)
+
+ err = CreateDirectory(sourceDir, d)
+
+ if err != nil {
+ continue
+ }
+
+ return d, nil
+ }
+ }
+
+ return dir, nil
+}
+
+func createHtml(dto FourKeyMetricResultDto, items ChartItems) (string, error) {
+
+ mtJson, err := json.Marshal(items.meanTimes)
+ ltJson, err := json.Marshal(items.leadTimes)
+ fpJson, err := json.Marshal(items.failPercentages)
+ dfJson, err := json.Marshal(items.deploymentFrequencies)
+
+ if err != nil {
+ fmt.Println(commander.Fatal("an error occurred while serializing"))
+ return "", err
+ }
+
+ htmlTemplate := template.GetHtml()
+ htmlTemplate = strings.Replace(htmlTemplate, "{repositoryName}", dto.RepoName, 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{teamName}", dto.TeamName, 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{startDate}", dto.DateRangeStart.Format(settings.DefaultDateFormat), 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{endDate}", dto.DateRangeEnd.Format(settings.DefaultDateFormat), 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{mtData}", string(mtJson), 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{ltData}", string(ltJson), 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{fpData}", string(fpJson), 1)
+ htmlTemplate = strings.Replace(htmlTemplate, "{dfData}", string(dfJson), 1)
+
+ return htmlTemplate, err
+}
+
+func mergeChartItems(source []ChartItems, target *ChartItems) {
+ for _, m := range source {
+ for _, item := range m.deploymentFrequencies {
+ target.deploymentFrequencies = append(target.deploymentFrequencies, item)
+ }
+ for _, item := range m.failPercentages {
+ target.failPercentages = append(target.failPercentages, item)
+ }
+ for _, item := range m.meanTimes {
+ target.meanTimes = append(target.meanTimes, item)
+ }
+ for _, item := range m.leadTimes {
+ target.leadTimes = append(target.leadTimes, item)
+ }
+ }
+}
diff --git a/cmd/set_command.go b/cmd/set_command.go
new file mode 100644
index 0000000..182c8ae
--- /dev/null
+++ b/cmd/set_command.go
@@ -0,0 +1,67 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ Command "four-key/command"
+ "four-key/models"
+ "four-key/settings"
+ "github.com/spf13/cobra"
+ "io/ioutil"
+ "path"
+)
+
+var setCommand = &cobra.Command{
+ Use: "set",
+ Short: "set config",
+ Long: "set config",
+ Run: onSet,
+}
+
+func init() {
+ rootCmd.AddCommand(setCommand)
+ setCommand.Flags().StringP("output", "o", "", "Set output source of 4Key metrics results")
+}
+
+func onSet(cmd *cobra.Command, args []string) {
+ output, err := cmd.Flags().GetString("output")
+ commander := Command.ACommander()
+
+ if output == "" || err != nil {
+ fmt.Println(commander.Fatal("output parameter error please check and re run"))
+ }
+
+ document := models.Document{}
+ existFile, err := ioutil.ReadFile(path.Join(commander.GetFourKeyPath(), settings.EnvironmentFileName))
+
+ if err != nil {
+ s, err := settings.Get()
+ if s == nil {
+ fmt.Println(commander.Fatal("The file does not exist!"))
+ return
+ }
+
+ existFile, err = ioutil.ReadFile(path.Join(commander.GetFourKeyPath(), settings.EnvironmentFileName))
+ if err != nil {
+ fmt.Println(commander.Fatal("The file does not exist!"))
+ return
+ }
+ }
+
+ err = json.Unmarshal([]byte(string(existFile)), &document)
+ if err != nil {
+ fmt.Println(commander.Fatal("json parse error"))
+ return
+ }
+
+ if output != "" {
+ document.Output = output
+ }
+
+ err = writeFile(document)
+
+ if err != nil {
+ fmt.Println(commander.Fatal("write error. err -> ", err))
+ return
+ }
+}
diff --git a/command/command.go b/command/command.go
new file mode 100644
index 0000000..5712903
--- /dev/null
+++ b/command/command.go
@@ -0,0 +1,116 @@
+package Command
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path"
+ "runtime"
+ "strings"
+)
+
+func (c *Commander) Info(args ...interface{}) string {
+ f := c.color("\033[1;36m%s\033[0m")
+ return f(args)
+}
+
+func (c *Commander) Good(args ...interface{}) string {
+ f := c.color("\033[1;32m%s\033[0m")
+ return f(args)
+
+}
+
+func (c *Commander) Fatal(args ...interface{}) string {
+ f := c.color("\033[1;31m%s\033[0m")
+ return f(args)
+
+}
+
+func (c *Commander) Warn(args ...interface{}) string {
+ f := c.color("\033[1;33m%s\033[0m")
+ return f(args)
+
+}
+
+var c Commander
+
+func ACommander() *Commander {
+ return &c
+}
+
+type Commander struct {
+}
+
+type ICommand interface {
+ Command(cmd string, p string) error
+ GetFourKeyPath() string
+ GetRepositoriesPath(cloneDir string) string
+ Info(...interface{}) string
+ Warn(...interface{}) string
+ Fatal(...interface{}) string
+ Good(...interface{}) string
+ Open(path string) error
+}
+
+func (c *Commander) Command(command string, p string) error {
+ // Prepare the command to execute.
+ cmd := exec.Command("sh", "-c", strings.TrimSuffix(command, "\n"))
+
+ // Set the correct output device.
+ cmd.Dir = p
+
+ // Execute the command and return the error.
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ err = errors.New(string(output))
+ return err
+ }
+
+ return nil
+}
+
+func (c *Commander) GetFourKeyPath() string {
+ r, err := os.UserHomeDir()
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ p := path.Join(r, "four-key")
+ err = os.Mkdir(p, os.ModePerm)
+
+ return p
+}
+
+func (c *Commander) GetRepositoriesPath(cloneDir string) string {
+ r, err := os.UserHomeDir()
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ p := path.Join(r, "four-key", cloneDir)
+ err = os.Mkdir(p, os.ModePerm)
+
+ log.Println(err)
+
+ return p
+}
+
+func (c *Commander) color(colorString string) func(...interface{}) string {
+ sprint := func(args ...interface{}) string {
+ return fmt.Sprintf(colorString,
+ fmt.Sprint(args...))
+ }
+ return sprint
+}
+
+func (c *Commander) Open(path string) error {
+ if runtime.GOOS == "windows" {
+ return c.Command("start .", path)
+ } else {
+ return c.Command("open .", path)
+ }
+}
diff --git a/command/mocks/Command.go b/command/mocks/Command.go
new file mode 100644
index 0000000..b52a82f
--- /dev/null
+++ b/command/mocks/Command.go
@@ -0,0 +1,130 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// Command is an autogenerated mock type for the Command type
+type Command struct {
+ mock.Mock
+}
+
+// Command provides a mock function with given fields: cmd, p
+func (_m *Command) Command(cmd string, p string) error {
+ ret := _m.Called(cmd, p)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string) error); ok {
+ r0 = rf(cmd, p)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Fatal provides a mock function with given fields: _a0
+func (_m *Command) Fatal(_a0 ...interface{}) string {
+ var _ca []interface{}
+ _ca = append(_ca, _a0...)
+ ret := _m.Called(_ca...)
+
+ var r0 string
+ if rf, ok := ret.Get(0).(func(...interface{}) string); ok {
+ r0 = rf(_a0...)
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+
+ return r0
+}
+
+// GetFourKeyPath provides a mock function with given fields:
+func (_m *Command) GetFourKeyPath() string {
+ ret := _m.Called()
+
+ var r0 string
+ if rf, ok := ret.Get(0).(func() string); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+
+ return r0
+}
+
+// GetRepositoriesPath provides a mock function with given fields: cloneDir
+func (_m *Command) GetRepositoriesPath(cloneDir string) string {
+ ret := _m.Called(cloneDir)
+
+ var r0 string
+ if rf, ok := ret.Get(0).(func(string) string); ok {
+ r0 = rf(cloneDir)
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+
+ return r0
+}
+
+// Good provides a mock function with given fields: _a0
+func (_m *Command) Good(_a0 ...interface{}) string {
+ var _ca []interface{}
+ _ca = append(_ca, _a0...)
+ ret := _m.Called(_ca...)
+
+ var r0 string
+ if rf, ok := ret.Get(0).(func(...interface{}) string); ok {
+ r0 = rf(_a0...)
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+
+ return r0
+}
+
+// Info provides a mock function with given fields: _a0
+func (_m *Command) Info(_a0 ...interface{}) string {
+ var _ca []interface{}
+ _ca = append(_ca, _a0...)
+ ret := _m.Called(_ca...)
+
+ var r0 string
+ if rf, ok := ret.Get(0).(func(...interface{}) string); ok {
+ r0 = rf(_a0...)
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+
+ return r0
+}
+
+// Open provides a mock function with given fields: path
+func (_m *Command) Open(path string) error {
+ ret := _m.Called(path)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string) error); ok {
+ r0 = rf(path)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Warn provides a mock function with given fields: _a0
+func (_m *Command) Warn(_a0 ...interface{}) string {
+ var _ca []interface{}
+ _ca = append(_ca, _a0...)
+ ret := _m.Called(_ca...)
+
+ var r0 string
+ if rf, ok := ret.Get(0).(func(...interface{}) string); ok {
+ r0 = rf(_a0...)
+ } else {
+ r0 = ret.Get(0).(string)
+ }
+
+ return r0
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..95e9f7a
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,19 @@
+module four-key
+
+go 1.13
+
+require (
+ github.com/brianvoe/gofakeit/v5 v5.4.3
+ github.com/kr/text v0.2.0 // indirect
+ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sergi/go-diff v1.1.0 // indirect
+ github.com/spf13/cobra v1.0.0
+ github.com/stretchr/testify v1.5.1
+ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
+ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
+ golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 // indirect
+ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+ gopkg.in/src-d/go-git.v4 v4.13.1
+ gopkg.in/yaml.v2 v2.2.8 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..245918f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,224 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/brianvoe/gofakeit/v5 v5.4.3 h1:JWRVZTw81gV1RxNlNmBvZ+1oOqv0U6tMCne3mPXR9N8=
+github.com/brianvoe/gofakeit/v5 v5.4.3/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE=
+golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
+gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/helpers/commit_helper.go b/helpers/commit_helper.go
new file mode 100644
index 0000000..623c4a0
--- /dev/null
+++ b/helpers/commit_helper.go
@@ -0,0 +1,166 @@
+package helpers
+
+import (
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+ "regexp"
+ "strings"
+ "time"
+)
+
+func GetTagFixAndFeatureCommits(fixPatterns []string, tagDateRangeTotalCommits []object.Commit, tagCommits []tagCommit) (metricTags []tagMetricData) {
+
+ for i := 0; i < len(tagCommits); i++ {
+ var featureCommits []object.Commit
+ var fixCommits []object.Commit
+ var tagTotalCommits []object.Commit
+
+ if tagCommits[i].isDateRange {
+ var startDate time.Time
+ var endDate time.Time
+ var baseDate = tagCommits[i].commit.Committer.When
+ if i == 0 {
+ endDate = tagCommits[i+1].commit.Committer.When
+ featureCommits = FetchFeatureCommitsInDateRange(fixPatterns, tagDateRangeTotalCommits, baseDate, endDate)
+ tagTotalCommits = GetTagTotalCommitsInDateRange(tagDateRangeTotalCommits, baseDate, endDate)
+ fixCommits = FetchFixFirstsCommitsInDateRange(fixPatterns, tagDateRangeTotalCommits, baseDate)
+ } else if i == (len(tagCommits) - 1) {
+ startDate = tagCommits[i-1].commit.Committer.When
+ featureCommits = FetchFeatureLastCommitsInDateRange(fixPatterns, tagDateRangeTotalCommits, baseDate)
+ tagTotalCommits = GetTagTotalCommitsInDateRange(tagDateRangeTotalCommits, startDate, baseDate)
+ fixCommits = FetchFixCommitsInDateRange(fixPatterns, tagDateRangeTotalCommits, startDate, baseDate)
+ } else {
+ startDate = tagCommits[i-1].commit.Committer.When
+ endDate = tagCommits[i+1].commit.Committer.When
+ featureCommits = FetchFeatureCommitsInDateRange(fixPatterns, tagDateRangeTotalCommits, baseDate, endDate)
+ tagTotalCommits = GetTagTotalCommitsInDateRange(tagDateRangeTotalCommits, baseDate, endDate)
+ fixCommits = FetchFixCommitsInDateRange(fixPatterns, tagDateRangeTotalCommits, startDate, baseDate)
+ }
+
+ tagMetricData := tagMetricData{
+ tagDate: tagCommits[i].tagDate,
+ tag: tagCommits[i].tag,
+ fixCommits: fixCommits,
+ featCommits: featureCommits,
+ totalCommits: tagTotalCommits,
+ }
+
+ metricTags = append(metricTags, tagMetricData)
+ }
+ }
+
+ return metricTags
+}
+
+func GetTagTotalCommitsInDateRange(tagDateRangeTotalCommits []object.Commit, startDate, endDate time.Time) (totalCommits []object.Commit) {
+ for i := 0; i < len(tagDateRangeTotalCommits); i++ {
+ if IsDateWithinRange(tagDateRangeTotalCommits[i].Committer.When, startDate, endDate) {
+ totalCommits = append(totalCommits, tagDateRangeTotalCommits[i])
+ }
+ }
+
+ return totalCommits
+}
+
+func FetchFeatureCommitsInDateRange(fixPatterns []string, tagDateRangeTotalCommits []object.Commit, startDate, endDate time.Time) (featureCommits []object.Commit) {
+ for i := 0; i < len(tagDateRangeTotalCommits); i++ {
+ if IsDateWithinRange(tagDateRangeTotalCommits[i].Committer.When, startDate, endDate) {
+ if !IsFix(fixPatterns, tagDateRangeTotalCommits[i].Message) {
+ featureCommits = append(featureCommits, tagDateRangeTotalCommits[i])
+ }
+ }
+ }
+
+ return featureCommits
+}
+
+func FetchFeatureLastCommitsInDateRange(fixPatterns []string, tagDateRangeTotalCommits []object.Commit, startDate time.Time) (featureCommits []object.Commit) {
+ for i := 0; i < len(tagDateRangeTotalCommits); i++ {
+ if tagDateRangeTotalCommits[i].Committer.When.Before(startDate) {
+ if !IsFix(fixPatterns, tagDateRangeTotalCommits[i].Message) {
+ featureCommits = append(featureCommits, tagDateRangeTotalCommits[i])
+ }
+ }
+ }
+
+ return featureCommits
+}
+
+func FetchFixFirstsCommitsInDateRange(fixPatterns []string, tagDateRangeTotalCommits []object.Commit, endDate time.Time) (fixCommits []object.Commit) {
+
+ for i := 0; i < len(tagDateRangeTotalCommits); i++ {
+ if tagDateRangeTotalCommits[i].Committer.When.After(endDate) {
+ if IsFix(fixPatterns, tagDateRangeTotalCommits[i].Message) {
+ fixCommits = append(fixCommits, tagDateRangeTotalCommits[i])
+ }
+ }
+ }
+
+ return fixCommits
+}
+
+func FetchFixCommitsInDateRange(fixPatterns []string, tagDateRangeTotalCommits []object.Commit, startDate, endDate time.Time) (fixCommits []object.Commit) {
+ for i := 0; i < len(tagDateRangeTotalCommits); i++ {
+ if IsDateWithinRange(tagDateRangeTotalCommits[i].Committer.When, startDate, endDate) {
+ if IsFix(fixPatterns, tagDateRangeTotalCommits[i].Message) {
+ fixCommits = append(fixCommits, tagDateRangeTotalCommits[i])
+ }
+ }
+ }
+
+ return fixCommits
+}
+
+func IsMergeCommit(commitMessage string) bool {
+ mergePatterns := [2]string{"merge pull request", "merge branch"}
+ var lowerCommitMessage = strings.ToLower(commitMessage)
+ for _, mergePattern := range mergePatterns {
+ matched, err := regexp.MatchString(mergePattern, lowerCommitMessage)
+ if err != nil {
+ println(err)
+ }
+ if matched {
+ return true
+ }
+ }
+
+ return false
+}
+
+func IsFix(fixPatterns []string, commitMessage string) bool {
+ var lowerCommitMessage = strings.ToLower(commitMessage)
+ for _, fixPattern := range fixPatterns {
+ matched, err := regexp.MatchString("\\b"+fixPattern+"\\b", lowerCommitMessage)
+ if err != nil {
+ println(err)
+ }
+ if matched {
+ return true
+ }
+ }
+
+ return false
+}
+
+func GetCommitFromTagHash(repo *git.Repository, tagHash plumbing.Hash) (*object.Commit, error) {
+ tag, err := repo.TagObject(tagHash)
+ if err != nil {
+ //fmt.Println(err)
+ }
+
+ if tag != nil {
+ cm, err := tag.Commit()
+ if err != nil {
+ return nil, err
+ }
+ return cm, nil
+ }
+
+ commit, err := repo.CommitObject(tagHash)
+ if err != nil {
+ return nil, err
+ }
+
+ return commit, nil
+}
diff --git a/helpers/metric_helper.go b/helpers/metric_helper.go
new file mode 100644
index 0000000..ebc1193
--- /dev/null
+++ b/helpers/metric_helper.go
@@ -0,0 +1,55 @@
+package helpers
+
+//import "math"
+
+func GetLeadTime(metricTags []tagMetricData) []tagMetricData {
+
+ for i := 0; i < len(metricTags); i++ {
+ var tagLeadTime float64 = 0
+ if metricTags[i].featCommits != nil {
+ for k := 0; k < len(metricTags[i].featCommits); k++ {
+ tagLeadTime += metricTags[i].tagDate.Sub(metricTags[i].featCommits[k].Committer.When).Seconds()
+ }
+ var average = tagLeadTime / float64(len(metricTags[i].featCommits))
+ metricTags[i].tagLeadTimeSeconds = average
+ }
+ }
+
+ return metricTags
+}
+
+func GetMeanTimeToRestore(metricTags []tagMetricData) []tagMetricData {
+
+ for i := 0; i < len(metricTags); i++ {
+ if metricTags[i].fixCommits != nil {
+ var tagMeanTimeToRestore float64
+ for k := 0; k < len(metricTags[i].fixCommits); k++ {
+ tagMeanTimeToRestore += metricTags[i].fixCommits[k].Committer.When.Sub(metricTags[i].tagDate).Seconds()
+ }
+ var average = (tagMeanTimeToRestore) / float64(len(metricTags[i].fixCommits))
+ metricTags[i].tagMeanTimeRestoreAverageSeconds = average
+ }
+ }
+
+ return metricTags
+}
+
+func GetChangeFailPercentage(metricTags []tagMetricData) []tagMetricData {
+
+ for i := 0; i < len(metricTags); i++ {
+ var totalFixCount = 0
+ var totalFeatureCount = 0
+
+ if metricTags[i].fixCommits != nil {
+ totalFixCount += len(metricTags[i].fixCommits)
+ }
+ if metricTags[i].featCommits != nil {
+ totalFeatureCount += len(metricTags[i].featCommits)
+ }
+ if totalFeatureCount != 0 {
+ metricTags[i].tagChangeFailPercentage = float64(totalFixCount) / float64(totalFeatureCount) * 100
+ }
+ }
+
+ return metricTags
+}
diff --git a/helpers/repository_helper.go b/helpers/repository_helper.go
new file mode 100644
index 0000000..fa00e70
--- /dev/null
+++ b/helpers/repository_helper.go
@@ -0,0 +1,150 @@
+package helpers
+
+import (
+ "errors"
+ "fmt"
+ Command "four-key/command"
+ "four-key/settings"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "path"
+ "regexp"
+ "strings"
+)
+
+type RepositoryWrapper struct {
+ Repository *git.Repository
+ Configurations settings.Repository
+}
+
+type IRepositoryHelper interface {
+ GetRepositories(s *settings.Settings) ([]RepositoryWrapper, error)
+}
+
+func GetRepositories(s *settings.Settings) ([]RepositoryWrapper, error) {
+ var repositoriesWrapper []RepositoryWrapper
+
+ for _, repo := range s.Repositories {
+ err := CheckDirectory(s.RepositoriesPath, repo.Name())
+
+ if err != nil {
+ err := CloneRepository(repo.CloneAddress, s.RepositoriesPath)
+ if err != nil {
+ return repositoriesWrapper, err
+ }
+ }
+
+ repository, err := git.PlainOpen(path.Join(Command.ACommander().GetRepositoriesPath(s.RepositoriesPath), repo.Name()))
+
+ if err != nil {
+ fmt.Println(Command.ACommander().Warn(err))
+ return nil, err
+ }
+
+ wrapper := RepositoryWrapper{
+ Repository: repository,
+ Configurations: repo,
+ }
+
+ repositoriesWrapper = append(repositoriesWrapper, wrapper)
+ }
+
+ return repositoriesWrapper, nil
+}
+
+func GetRepositoryByName(s *settings.Settings, repositoryName string) (RepositoryWrapper, error) {
+ var w RepositoryWrapper
+
+ for _, repo := range s.Repositories {
+ if repo.Name() == repositoryName {
+ err := CheckDirectory(s.RepositoriesPath, repositoryName)
+
+ if err != nil {
+ err := CloneRepository(repo.CloneAddress, s.RepositoriesPath)
+ if err != nil {
+ return w, err
+ }
+ }
+ repository, err := git.PlainOpen(path.Join(Command.ACommander().GetRepositoriesPath(s.RepositoriesPath), repo.Name()))
+
+ if err != nil {
+ fmt.Println(err)
+ return w, err
+ }
+
+ w.Repository = repository
+ w.Configurations = repo
+
+ return w, nil
+ }
+ }
+
+ return w, errors.New("repository not found with that given name -> " + repositoryName)
+}
+
+func RepoCheck(r *git.Repository) {
+ _, err := r.Log(&git.LogOptions{
+ From: plumbing.Hash{},
+ Order: 0,
+ FileName: nil,
+ All: false,
+ })
+
+ w, err := r.Worktree()
+ if err != nil {
+ panic(fmt.Sprintf("An error occured. Error: %v", err))
+ }
+
+ err = r.Fetch(&git.FetchOptions{
+ RemoteName: "origin",
+ Tags: 2,
+ Force: true,
+ })
+ err = w.Pull(&git.PullOptions{RemoteName: "origin", Force: true})
+
+ // Print the latest commit that was just pulled
+ ref, err := r.Head()
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = r.CommitObject(ref.Hash())
+
+ if err != nil {
+ panic(fmt.Sprintf("An error occured. Error: %v", err))
+ }
+
+}
+
+func CloneRepository(cloneLink string, p string) error {
+ fmt.Println(Command.ACommander().Good("Cloning repository - clone address -> ", cloneLink))
+
+ err := Command.ACommander().Command("git clone "+cloneLink+" --progress", Command.ACommander().GetRepositoriesPath(p))
+
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(Command.ACommander().Good("Cloned repository ", GetNameByRepositoryCloneUrl(cloneLink)))
+ return nil
+}
+
+func CreateDirectory(path string, name string) error {
+ err := Command.ACommander().Command("mkdir "+name, path)
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func CheckDirectory(args ...string) error {
+ paths := append([]string{Command.ACommander().GetFourKeyPath()}, args...)
+ return Command.ACommander().Command("pwd", path.Join(paths...))
+}
+
+func GetNameByRepositoryCloneUrl(cloneAddress string) string {
+ re := regexp.MustCompile(`([^/]+)\.git$`)
+ return strings.Replace(re.FindString(cloneAddress), ".git", "", 1)
+}
diff --git a/helpers/repository_helper_test.go b/helpers/repository_helper_test.go
new file mode 100644
index 0000000..345b806
--- /dev/null
+++ b/helpers/repository_helper_test.go
@@ -0,0 +1 @@
+package helpers
diff --git a/helpers/repository_metric_helper.go b/helpers/repository_metric_helper.go
new file mode 100644
index 0000000..24ee610
--- /dev/null
+++ b/helpers/repository_metric_helper.go
@@ -0,0 +1,228 @@
+package helpers
+
+import (
+ _ "container/list"
+ "errors"
+ . "four-key/models"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+ "regexp"
+ _ "sort"
+ "strings"
+ "time"
+)
+
+type tagCommit struct {
+ commit object.Commit
+ tag *plumbing.Reference
+ tagDate time.Time
+ isDateRange bool
+ tagType string
+ tagComment string
+}
+
+type FourKeyMetricsDto struct {
+ TagMetrics []tagMetricData
+ Df DeploymentFrequencyDto
+ RepoName string
+ TeamName string
+}
+
+type tagMetricData struct {
+ fixCommits []object.Commit
+ featCommits []object.Commit
+ totalCommits []object.Commit
+ tag *plumbing.Reference
+ tagDate time.Time
+ tagMeanTimeRestoreAverageSeconds float64
+ tagLeadTimeSeconds float64
+ tagChangeFailPercentage float64
+ deploymentFrequency float64
+}
+
+type DeploymentTag struct {
+ When time.Time `json:"date"`
+ Name string
+ Count int `json:"value"`
+}
+
+type DeploymentFrequencyDto struct {
+ Tags []DeploymentTag `json:"Deployments"`
+}
+
+func CalculateMetrics(repo *git.Repository, request MetricsRequest) (FourKeyMetricResultDto, error) {
+
+ var keyMetrics FourKeyMetricResultDto
+
+ RepoCheck(repo)
+
+ tagCommits, err := getTagCommitBetweenDates(repo, request)
+
+ if len(tagCommits) > 1 {
+
+ var tagDateRangeTotalCommits = GetTagDateRangeTotalCommits(repo, tagCommits)
+
+ //fix and fea commits found
+ var tagFixAndFeatureCommits = GetTagFixAndFeatureCommits(request.FixPatterns, tagDateRangeTotalCommits, tagCommits)
+
+ //added MeanTimeToRestore
+ tagFixAndFeatureCommits = GetMeanTimeToRestore(tagFixAndFeatureCommits)
+
+ //added ChangeFailPercentage
+ tagFixAndFeatureCommits = GetChangeFailPercentage(tagFixAndFeatureCommits)
+
+ //added LeadTime
+ tagFixAndFeatureCommits = GetLeadTime(tagFixAndFeatureCommits)
+
+ if err != nil {
+ return keyMetrics, err
+ }
+
+ var tagMetricDtoList []TagMetricDto
+ for _, tagMetricDateRange := range tagFixAndFeatureCommits {
+ tagNameParse := strings.Split(string(tagMetricDateRange.tag.Name()), "/")
+ tagMetricDto := TagMetricDto{
+ TagName: tagNameParse[len(tagNameParse)-1],
+ TagDate: tagMetricDateRange.tagDate,
+ MeanTimeRestoreAverage: GetSecondsToHour(tagMetricDateRange.tagMeanTimeRestoreAverageSeconds),
+ LeadTime: GetSecondsToHour(tagMetricDateRange.tagLeadTimeSeconds),
+ ChangeFailPercentage: tagMetricDateRange.tagChangeFailPercentage,
+ }
+ tagMetricDtoList = append(tagMetricDtoList, tagMetricDto)
+ }
+
+ keyMetrics.CreationDate = time.Now()
+ keyMetrics.MetricTags = tagMetricDtoList
+ keyMetrics.DateRangeEnd = request.EndDate
+ keyMetrics.DateRangeStart = request.StartDate
+ keyMetrics.DeploymentFrequencyCount = int64(len(tagMetricDtoList))
+
+ return keyMetrics, nil
+ }
+
+ return keyMetrics, errors.New("metrics could not be calculated because there is no release")
+}
+
+func GetTagDateRangeTotalCommits(repo *git.Repository, tagCommits []tagCommit) []object.Commit {
+
+ var descendingSortCommits = GetDescendingCommits(repo)
+
+ var lastTagCommit = tagCommits[len(tagCommits)-1]
+ var firstTagCommit = tagCommits[0]
+
+ var isFistTagDateRangeCommit = firstTagCommit.isDateRange
+ var isLastTagDateRangeCommit = lastTagCommit.isDateRange
+ var tagDateRangeTotalCommits []object.Commit
+
+ var isDateRangeCommitFounds = false
+ for _, sortCommit := range descendingSortCommits {
+ var isDateRangeCommit = IsDateWithinRange(sortCommit.Commit.Committer.When, firstTagCommit.tagDate, lastTagCommit.tagDate)
+
+ var isCommitAfterTagDate = sortCommit.Commit.Committer.When.After(firstTagCommit.tagDate)
+ if isCommitAfterTagDate && isFistTagDateRangeCommit {
+ isDateRangeCommitFounds = true
+ }
+
+ if isDateRangeCommit {
+ isDateRangeCommitFounds = true
+ }
+ var isCommitBeforeTagDate = sortCommit.Commit.Committer.When.Before(lastTagCommit.tagDate)
+ if isCommitBeforeTagDate && isLastTagDateRangeCommit {
+ isDateRangeCommitFounds = true
+ }
+
+ if isDateRangeCommitFounds {
+ if !IsMergeCommit(sortCommit.Commit.Message) {
+ tagDateRangeTotalCommits = append(tagDateRangeTotalCommits, sortCommit.Commit)
+ }
+ }
+
+ if !isDateRangeCommit {
+ isDateRangeCommitFounds = false
+ }
+ }
+
+ return tagDateRangeTotalCommits
+}
+
+func IsReleaseTag(tagName, releaseTagPattern string) bool {
+ var lowerTagName = strings.ToLower(tagName)
+ matched, err := regexp.MatchString(releaseTagPattern, lowerTagName)
+ if err != nil {
+ println(err)
+ }
+
+ return matched
+}
+
+func getTagCommitBetweenDates(r *git.Repository, request MetricsRequest) ([]tagCommit, error) {
+ var commitTags []tagCommit
+
+ var sortedTagList = GetAscendingOrderByTagDate(r)
+
+ var prevTag *tagCommit
+ var lastTag *tagCommit
+ var firstTag *tagCommit
+ lastTagFound := false
+ firstTagFound := false
+ for _, t := range sortedTagList {
+ if !IsReleaseTag(string(t.tag.Name()), request.ReleaseTagPattern) {
+ continue
+ }
+
+ tagCmt, err := GetCommitFromTagHash(r, t.tag.Hash())
+ if err != nil {
+ return commitTags, err
+ }
+
+ cTag := tagCommit{
+ commit: *tagCmt,
+ tag: t.tag,
+ isDateRange: true,
+ tagDate: tagCmt.Committer.When,
+ tagType: "TagIsDateRange",
+ tagComment: "Tag in the date range",
+ }
+
+ tagIsDateRange := inTimeSpan(request.StartDate, request.EndDate, tagCmt.Committer.When)
+
+ if tagIsDateRange {
+ if !lastTagFound {
+ lastTag = prevTag
+ lastTagFound = true
+ }
+
+ commitTags = append(commitTags, cTag)
+ }
+
+ if lastTagFound && !firstTagFound && !tagIsDateRange {
+ firstTag = &cTag
+ firstTagFound = true
+ }
+
+ prevTag = &cTag
+ }
+
+ var sortedCommitTags []tagCommit
+
+ if firstTag != nil {
+ firstTag.isDateRange = false
+ firstTag.tagType = "firstTag"
+ firstTag.tagComment = "First tag not in the date range"
+ sortedCommitTags = append(sortedCommitTags, *firstTag)
+ }
+
+ for i := len(commitTags) - 1; i >= 0; i-- {
+ sortedCommitTags = append(sortedCommitTags, commitTags[i])
+ }
+
+ if lastTag != nil {
+ lastTag.isDateRange = false
+ lastTag.tagType = "lastTag"
+ lastTag.tagComment = "Tag before last tag in date range"
+ sortedCommitTags = append(sortedCommitTags, *lastTag)
+ }
+
+ return sortedCommitTags, nil
+}
diff --git a/helpers/time_helper.go b/helpers/time_helper.go
new file mode 100644
index 0000000..2d85722
--- /dev/null
+++ b/helpers/time_helper.go
@@ -0,0 +1,127 @@
+package helpers
+
+import (
+ "fmt"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+ "sort"
+ "time"
+)
+
+type TagData struct {
+ tagDate time.Time
+ tag *plumbing.Reference
+}
+
+type timeSlice []TagData
+
+func (p timeSlice) Len() int {
+ return len(p)
+}
+
+func (p timeSlice) Less(i, j int) bool {
+ return p[i].tagDate.Before(p[j].tagDate)
+}
+
+func (p timeSlice) Swap(i, j int) {
+ p[i], p[j] = p[j], p[i]
+}
+
+func GetAscendingOrderByTagDate(r *git.Repository) (tags []TagData) {
+
+ var tagDataList = make(map[string]TagData)
+ rTags, err := r.Tags()
+ if err != nil {
+ println(err)
+ }
+
+ var i = 0
+ err = rTags.ForEach(func(t *plumbing.Reference) error {
+ cm, err := GetCommitFromTagHash(r, t.Hash())
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ if cm != nil {
+ tagDataList[string(i)] = TagData{cm.Committer.When, t}
+ i++
+ }
+
+ return nil
+ })
+
+ sortedTagDataList := make(timeSlice, 0, len(tagDataList))
+ for _, tag := range tagDataList {
+ sortedTagDataList = append(sortedTagDataList, tag)
+ }
+ sort.Sort(sortedTagDataList)
+
+ return sortedTagDataList
+}
+
+type CommitData struct {
+ CommitDate time.Time
+ Commit object.Commit
+}
+type timeCommitSlice []CommitData
+
+func (p timeCommitSlice) Len() int {
+ return len(p)
+}
+
+func (p timeCommitSlice) Less(i, j int) bool {
+ return p[i].CommitDate.After(p[j].CommitDate)
+}
+
+func (p timeCommitSlice) Swap(i, j int) {
+ p[i], p[j] = p[j], p[i]
+}
+
+func GetDescendingCommits(r *git.Repository) (tags []CommitData) {
+
+ var commitDataList = make(map[string]CommitData)
+ repoCommits, err := r.CommitObjects()
+ if err != nil {
+ println(err)
+ }
+
+ var i = 0
+ err = repoCommits.ForEach(func(commit *object.Commit) error {
+ if err != nil {
+ //fmt.Println(err)
+ }
+ commitDataList[string(i)] = CommitData{commit.Committer.When, *commit}
+ i++
+
+ return nil
+ })
+
+ sortedCommitDataList := make(timeCommitSlice, 0, len(commitDataList))
+ for _, tempCommit := range commitDataList {
+ sortedCommitDataList = append(sortedCommitDataList, tempCommit)
+ }
+ sort.Sort(sortedCommitDataList)
+
+ return sortedCommitDataList
+}
+
+func IsDateWithinRange(dateStamp, startDate, finishDate time.Time) bool {
+
+ if dateStamp.Before(startDate) && dateStamp.After(finishDate) {
+ return true
+ }
+
+ return false
+}
+func inTimeSpan(start, end, check time.Time) bool {
+ return check.After(start) && check.Before(end)
+}
+
+func GetSecondsToHour(seconds float64) float64 {
+ return seconds / 3600
+}
+
+func GetSecondsToDays(seconds float64) float64 {
+ return seconds / 86400
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..2f24883
--- /dev/null
+++ b/main.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+ "four-key/cmd"
+ "io/ioutil"
+ "log"
+)
+
+func main() {
+ log.SetOutput(ioutil.Discard)
+ cmd.Execute()
+}
diff --git a/models/document.go b/models/document.go
new file mode 100644
index 0000000..e2dce7d
--- /dev/null
+++ b/models/document.go
@@ -0,0 +1,13 @@
+package models
+
+type Document struct {
+ Output string `json:"output"`
+ Repositories []*DocumentRepository `json:"repositories"`
+}
+
+type DocumentRepository struct {
+ TeamName string `json:"teamName"`
+ CloneAddress string `json:"cloneAddress"`
+ ReleaseTagPattern string `json:"releaseTagPattern"`
+ FixCommitPatterns []string `json:"fixCommitPatterns"`
+}
diff --git a/models/four_key_metric_result_dto.go b/models/four_key_metric_result_dto.go
new file mode 100644
index 0000000..39048e4
--- /dev/null
+++ b/models/four_key_metric_result_dto.go
@@ -0,0 +1,13 @@
+package models
+
+import "time"
+
+type FourKeyMetricResultDto struct {
+ RepoName string
+ TeamName string
+ DateRangeStart time.Time `json:"DateRangeStart"`
+ DateRangeEnd time.Time `json:"DateRangeEnd"`
+ CreationDate time.Time `json:"CreationDate"`
+ MetricTags []TagMetricDto `json:"MetricTags"`
+ DeploymentFrequencyCount int64 `json:"DeploymentFrequencyCount"`
+}
diff --git a/models/metrics_request.go b/models/metrics_request.go
new file mode 100644
index 0000000..c863a9e
--- /dev/null
+++ b/models/metrics_request.go
@@ -0,0 +1,10 @@
+package models
+
+import "time"
+
+type MetricsRequest struct {
+ StartDate time.Time
+ EndDate time.Time
+ ReleaseTagPattern string
+ FixPatterns []string
+}
diff --git a/models/repository.go b/models/repository.go
new file mode 100644
index 0000000..76b716d
--- /dev/null
+++ b/models/repository.go
@@ -0,0 +1,6 @@
+package models
+
+type Repository struct {
+ Repository string `yaml:"repository"`
+ Branch string `yaml:"branch"`
+}
diff --git a/models/tag_metric_dto.go b/models/tag_metric_dto.go
new file mode 100644
index 0000000..8dc4cad
--- /dev/null
+++ b/models/tag_metric_dto.go
@@ -0,0 +1,11 @@
+package models
+
+import "time"
+
+type TagMetricDto struct {
+ TagName string `json:"Name"`
+ TagDate time.Time `json:"Date"`
+ LeadTime float64 `json:"LeadTime"`
+ MeanTimeRestoreAverage float64 `json:"MeanTimeRestoreAverage"`
+ ChangeFailPercentage float64 `json:"ChangeFailPercentage"`
+}
diff --git a/settings/.DS_Store b/settings/.DS_Store
new file mode 100644
index 0000000..5008ddf
Binary files /dev/null and b/settings/.DS_Store differ
diff --git a/settings/mock/four-key.json b/settings/mock/four-key.json
new file mode 100644
index 0000000..119b232
--- /dev/null
+++ b/settings/mock/four-key.json
@@ -0,0 +1,15 @@
+{
+ "output": "",
+ "repositories": [
+ {
+ "teamName": "reform",
+ "cloneAddress": "https://github.com/user/four-key-metrics.git",
+ "releaseTagPattern": "release-",
+ "fixCommitPatterns": [
+ "fix",
+ "hot-fix",
+ "hotfix"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/settings/setting_test.go b/settings/setting_test.go
new file mode 100644
index 0000000..4dc8b84
--- /dev/null
+++ b/settings/setting_test.go
@@ -0,0 +1,194 @@
+package settings
+
+import (
+ "errors"
+ "four-key/command/mocks"
+ "github.com/brianvoe/gofakeit/v5"
+ _ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "os"
+ "path"
+ "path/filepath"
+ "testing"
+)
+
+type Suite struct {
+ suite.Suite
+ mock mocks.Command
+
+ settings Setting
+}
+
+func (s *Suite) AfterTest(_, _ string) {
+ s.mock.AssertExpectations(s.T())
+ settings = Settings{}
+ isLoaded = false
+ s.mock = mocks.Command{}
+}
+
+func TestInit(t *testing.T) {
+ suite.Run(t, new(Suite))
+}
+
+func (s *Suite) SetupSuite() {
+}
+
+func (s *Suite) TestGet_WhenBeforeInitialize_ReturnsError() {
+ settings, err := Get()
+
+ s.NotNil(err)
+ s.Equal(err.Error(), "settings firstly must be initialized")
+ s.Nil(settings)
+}
+
+func (s *Suite) TestGet_WhenAfterInitialize_ReturnsSettings() {
+ isLoaded = true
+ settings, err := Get()
+
+ s.Nil(err)
+ s.NotNil(settings)
+}
+
+func (s *Suite) TestInitialize_ReturnsSettings() {
+ dir, err := os.Getwd()
+ s.Nil(err)
+ s.mock.On("GetFourKeyPath").Return(path.Join(dir, "/mock"))
+
+ expectedRepoName := "four-key-metrics"
+ expectedCloneAddress := "https://github.com/user/four-key-metrics.git"
+ expectedReleaseTagPattern := "release-"
+ expectedFixCommitPatterns := []string{"fix", "hot-fix", "hotfix"}
+ expectedTeamName := "reform"
+
+ err = Initialize(&s.mock)
+
+ s.Nil(err)
+ s.NotNil(settings)
+ s.Equal(1, len(settings.Repositories))
+ s.Equal(settings.Repositories[0].CloneAddress, expectedCloneAddress)
+ s.Equal(settings.Repositories[0].FixCommitPatterns, expectedFixCommitPatterns)
+ s.Equal(settings.Repositories[0].ReleaseTagPattern, expectedReleaseTagPattern)
+ s.Equal(settings.Repositories[0].Name(), expectedRepoName)
+ s.Equal(settings.Repositories[0].TeamName, expectedTeamName)
+}
+
+func (s *Suite) TestInitialize_WhenReaderReturnedError_CreatesNewConfigurationFileAndWritesDefaultTemplate() {
+ dir, err := os.Getwd()
+ s.Nil(err)
+
+ fourKeyDir := gofakeit.BeerMalt()
+ s.Nil(os.Mkdir(path.Join(dir, fourKeyDir), os.FileMode(0777)))
+
+ s.mock.On("GetFourKeyPath").Return(path.Join(dir, fourKeyDir))
+ s.mock.On("Open", path.Join(dir, fourKeyDir)).Return(nil)
+ s.mock.On("Warn", "Your configurations not found!").Return("Your configurations not found!")
+ s.mock.On("Warn", "Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName)).Return("Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName))
+ s.mock.On("Good", "Configuration file added.").Return("Configuration file added.")
+ s.mock.On("Good", "please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13").Return("please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13")
+
+ err = Initialize(&s.mock)
+
+ s.Nil(err)
+ s.NotNil(settings)
+ s.Equal(0, len(settings.Repositories))
+ err = removeContents(path.Join(dir, fourKeyDir))
+}
+
+func (s *Suite) TestInitialize_ReturnsSettingsFromCache() {
+ dir, err := os.Getwd()
+ s.Nil(err)
+
+ s.mock.On("GetFourKeyPath").Return(path.Join(dir, "/mock")).Twice()
+
+ err = Initialize(&s.mock)
+ err = Initialize(&s.mock)
+ err = Initialize(&s.mock)
+ err = Initialize(&s.mock)
+
+ s.NotNil(settings)
+ s.Equal(1, len(settings.Repositories))
+}
+
+func (s *Suite) TestInitialize_WhenFirstCreatingConfigurationFile_OpensFourKeyDirectory() {
+ dir, err := os.Getwd()
+ s.Nil(err)
+
+ fourKeyDir := gofakeit.BeerName()
+ s.Nil(os.Mkdir(path.Join(dir, fourKeyDir), os.FileMode(0777)))
+
+ s.mock.On("GetFourKeyPath").Return(path.Join(dir, fourKeyDir))
+ s.mock.On("Open", path.Join(dir, fourKeyDir)).Return(nil)
+ s.mock.On("Warn", "Your configurations not found!").Return("Your configurations not found!")
+ s.mock.On("Warn", "Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName)).Return("Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName))
+ s.mock.On("Good", "Configuration file added.").Return("Configuration file added.")
+ s.mock.On("Good", "please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13").Return("please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13")
+ s.mock.On("Open", path.Join(dir, fourKeyDir)).Return(nil)
+
+ err = Initialize(&s.mock)
+
+ s.Nil(err)
+ s.NotNil(settings)
+ s.Equal(0, len(settings.Repositories))
+ err = removeContents(path.Join(dir, fourKeyDir))
+}
+
+func (s *Suite) TestInitialize_WhenFirstCreatingConfigurationFileIfOpenReturnsError_LogsWarningOpeningDir() {
+ openError := errors.New("open error")
+ dir, err := os.Getwd()
+ s.Nil(err)
+
+ fourKeyDir := gofakeit.Adverb()
+ s.Nil(os.Mkdir(path.Join(dir, fourKeyDir), os.FileMode(0777)))
+
+ s.mock.On("GetFourKeyPath").Return(path.Join(dir, fourKeyDir))
+ s.mock.On("Warn", "Your configurations not found!").Return("Your configurations not found!")
+ s.mock.On("Warn", "Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName)).Return("Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName))
+ s.mock.On("Warn", "Configuration file directory not opened", openError.Error()).Return("Configuration file directory not opened", openError.Error())
+ s.mock.On("Good", "Configuration file added.").Return("Configuration file added.")
+ s.mock.On("Good", "please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13").Return("please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13")
+ s.mock.On("Open", path.Join(dir, fourKeyDir)).Return(openError)
+
+ err = Initialize(&s.mock)
+
+ s.Nil(err)
+ s.NotNil(settings)
+ s.Equal(0, len(settings.Repositories))
+ err = removeContents(path.Join(dir, fourKeyDir))
+}
+
+func (s *Suite) TestInitialize_IfReturnsErrorWhenCreatingFourKeyDir_ReturnsError() {
+ dir, err := os.Getwd()
+ s.Nil(err)
+ fourKeyDir := "\\//$+'"
+
+ s.mock.On("GetFourKeyPath").Return(path.Join(dir, fourKeyDir))
+ s.mock.On("Warn", "Your configurations not found!").Return("Your configurations not found!")
+ s.mock.On("Warn", "Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName)).Return("Generating configuration file to -> ", path.Join(dir, fourKeyDir, EnvironmentFileName))
+ s.mock.On("Fatal", "An error occurred while creating four-key.json to ", path.Join(dir, fourKeyDir, EnvironmentFileName)).Return("An error occurred while creating four-key.json to ", path.Join(dir, fourKeyDir, EnvironmentFileName))
+ s.mock.On("Fatal", "Configurations not loaded").Return("Configurations not loaded")
+
+ err = Initialize(&s.mock)
+
+ s.NotNil(err)
+ s.Equal(0, len(settings.Repositories))
+}
+
+func removeContents(dir string) error {
+ d, err := os.Open(dir)
+ if err != nil {
+ return err
+ }
+ defer d.Close()
+ names, err := d.Readdirnames(-1)
+ if err != nil {
+ return err
+ }
+ for _, name := range names {
+ err = os.RemoveAll(filepath.Join(dir, name))
+ if err != nil {
+ return err
+ }
+ }
+
+ return os.Remove(dir)
+}
diff --git a/settings/settings.go b/settings/settings.go
new file mode 100644
index 0000000..eb1852d
--- /dev/null
+++ b/settings/settings.go
@@ -0,0 +1,121 @@
+package settings
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ Command "four-key/command"
+ "io/ioutil"
+ "os"
+ "path"
+ "regexp"
+ "strings"
+)
+
+type Setting interface {
+ Load() error
+}
+
+var settings Settings
+var isLoaded = false
+
+const TemplateConfig = `{"repositories":[]}`
+const DefaultTeamName = "master"
+const EnvironmentFileName = "four-key.json"
+const DefaultRepositoryDirName = "repos"
+const AllTeamsDefaultDirName = "allTeams"
+const TeamBasedDefaultDirName = "teamBased"
+const DefaultGeneratedFileOutputDirName = "metrics"
+const DefaultDateFormat = "2006-01-02"
+
+type Configuration struct {
+ Key string `json:"key"`
+ Value string `json:"value"`
+}
+
+type Settings struct {
+ Output string
+ Repositories []Repository
+ RepositoriesPath string
+ commander Command.ICommand
+}
+
+type Repository struct {
+ CloneAddress string `json:"cloneAddress"`
+ TeamName string `json:"teamName"`
+ ReleaseTagPattern string `json:"releaseTagPattern"`
+ FixCommitPatterns []string `json:"fixCommitPatterns"`
+}
+
+func (r *Repository) Name() string {
+ rx := regexp.MustCompile(`([^/]+)\.git$`)
+ return strings.Replace(rx.FindString(r.CloneAddress), ".git", "", 1)
+}
+
+func (s *Settings) Load() error {
+ cfg, err := ioutil.ReadFile(path.Join(s.commander.GetFourKeyPath(), EnvironmentFileName))
+
+ if err != nil {
+ fmt.Println(s.commander.Warn("Your configurations not found!"))
+ fmt.Println(s.commander.Warn("Generating configuration file to -> ", path.Join(s.commander.GetFourKeyPath(), EnvironmentFileName)))
+
+ f, err := os.Create(path.Join(s.commander.GetFourKeyPath(), EnvironmentFileName))
+ if err != nil {
+ fmt.Println(s.commander.Fatal("An error occurred while creating four-key.json to ", path.Join(s.commander.GetFourKeyPath(), EnvironmentFileName)))
+ return err
+ }
+
+ _, err = f.WriteString(TemplateConfig)
+
+ fmt.Println(s.commander.Good("Configuration file added."))
+ fmt.Println(s.commander.Good("please add an repository and run command like -> ./four-key run -s 2018-01-13 -e 2021-01-13"))
+
+ err = s.commander.Open(s.commander.GetFourKeyPath())
+ if err != nil {
+ fmt.Println(s.commander.Warn("Configuration file directory not opened", err.Error()))
+ }
+
+ cfg, err = ioutil.ReadFile(path.Join(s.commander.GetFourKeyPath(), EnvironmentFileName))
+ }
+
+ jsonErr := json.Unmarshal(cfg, &settings)
+
+ if jsonErr != nil {
+ fmt.Println("Error while parsing config file", jsonErr)
+ return errors.New(fmt.Sprintf("An error occured. Error: %v", jsonErr))
+ }
+
+ if settings.RepositoriesPath == "" {
+ settings.RepositoriesPath = DefaultRepositoryDirName
+ }
+
+ if settings.Output == "" {
+ settings.Output = s.commander.GetFourKeyPath()
+ }
+
+ return nil
+}
+
+func Initialize(cmd Command.ICommand) error {
+ if isLoaded != true {
+ settings.commander = cmd
+ err := settings.Load()
+
+ if err != nil {
+ fmt.Println(cmd.Fatal("Configurations not loaded"))
+ return err
+ }
+
+ isLoaded = true
+ }
+
+ return nil
+}
+
+func Get() (*Settings, error) {
+ if isLoaded == true {
+ return &settings, nil
+ }
+
+ return nil, errors.New("settings firstly must be initialized")
+}
diff --git a/sonar-project-properties b/sonar-project-properties
new file mode 100644
index 0000000..9e6fcfe
--- /dev/null
+++ b/sonar-project-properties
@@ -0,0 +1,16 @@
+sonar.host.url=http://sonarhost
+#----- Default source code encoding
+sonar.sourceEncoding=UTF-8
+
+# must be unique in a given SonarQube instance
+sonar.projectKey=FourKey:master
+sonar.projectName=Four Key
+
+sonar.tests=./src
+sonar.test.inclusions=**/*_test.go
+sonar.go.coverage.reportPaths=cover.out
+
+sonar.qualitygate.wait=true
+sonar.verbose=true
+sonar.language=go
+
diff --git a/template/template.go b/template/template.go
new file mode 100644
index 0000000..5cdf8f0
--- /dev/null
+++ b/template/template.go
@@ -0,0 +1,602 @@
+package template
+
+func GetHtml() string {
+ return `
+
+
+
+
+
+ four-key Metrics
+
+
+
+
+
+
+
+
+
+
+
+
+
four-key Metrics
+
{repositoryName} | {teamName} | {startDate} - {endDate}
+
+
Deployment Frequencies
+
+
+
Lead Times
+
+
+
Mean Times
+
+
+
Fail Percentages
+
+
+
+
+
+
+
+ `
+}