From 06219059334d21236d6ed4b7ae72ab63667396c8 Mon Sep 17 00:00:00 2001 From: Burak Olgun Date: Fri, 22 May 2020 09:35:10 +0300 Subject: [PATCH] initial commit --- .DS_Store | Bin 0 -> 8196 bytes .gitignore | 56 +++ .travis.yml | 18 + LICENSE.md | 7 + README.md | 146 +++++++ assets/logo.png | Bin 0 -> 67921 bytes cmd/repository_command.go | 226 ++++++++++ cmd/root.go | 19 + cmd/run_command.go | 384 +++++++++++++++++ cmd/set_command.go | 67 +++ command/command.go | 116 ++++++ command/mocks/Command.go | 130 ++++++ go.mod | 19 + go.sum | 224 ++++++++++ helpers/commit_helper.go | 166 ++++++++ helpers/metric_helper.go | 55 +++ helpers/repository_helper.go | 150 +++++++ helpers/repository_helper_test.go | 1 + helpers/repository_metric_helper.go | 228 ++++++++++ helpers/time_helper.go | 127 ++++++ main.go | 12 + models/document.go | 13 + models/four_key_metric_result_dto.go | 13 + models/metrics_request.go | 10 + models/repository.go | 6 + models/tag_metric_dto.go | 11 + settings/.DS_Store | Bin 0 -> 6148 bytes settings/mock/four-key.json | 15 + settings/setting_test.go | 194 +++++++++ settings/settings.go | 121 ++++++ sonar-project-properties | 16 + template/template.go | 602 +++++++++++++++++++++++++++ 32 files changed, 3152 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 assets/logo.png create mode 100644 cmd/repository_command.go create mode 100644 cmd/root.go create mode 100644 cmd/run_command.go create mode 100644 cmd/set_command.go create mode 100644 command/command.go create mode 100644 command/mocks/Command.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helpers/commit_helper.go create mode 100644 helpers/metric_helper.go create mode 100644 helpers/repository_helper.go create mode 100644 helpers/repository_helper_test.go create mode 100644 helpers/repository_metric_helper.go create mode 100644 helpers/time_helper.go create mode 100644 main.go create mode 100644 models/document.go create mode 100644 models/four_key_metric_result_dto.go create mode 100644 models/metrics_request.go create mode 100644 models/repository.go create mode 100644 models/tag_metric_dto.go create mode 100644 settings/.DS_Store create mode 100644 settings/mock/four-key.json create mode 100644 settings/setting_test.go create mode 100644 settings/settings.go create mode 100644 sonar-project-properties create mode 100644 template/template.go diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9df718e30cf46258ff4a23cc0ae0833581ec4af6 GIT binary patch literal 8196 zcmeHMzi-n(7=0Iq)&e9tcCtDgpjg>!ufdMgq#8d_r-u)oHv*UC_Aij(4KI^{s-S_GqJiUOj* zf2n|eJ|wK#rp^}Dnnwp4vjw1Sv)dG2&*=-=#M#u@!diM*lnte|p~`JBlnrNp;&7?6 zg|#*u%56TB%dFfEMVagzKQVBq)LLmp0a2i?fZn^eaR&qJVC3)L)%mk~zAqnkyPMf? zlYQ}rr>9SjZ=5ZM{@zso{&smb(7IeO6qlHH2R-z$;b#VJeG0vP?&qsZyDqmEpAXCI zlM#M?z#LJ`U;_vI^fAIgz#xj3$RP0h9Pa;k_iC_U?!SU5Su|k+9+7!kMc6`)Ucdm! zA{jW3pL_SqoAV>9{n%S(t$TxfNI58GnQzF9Iq!e3nm1ahBmDfIuP*+)Zu3{h@#ml! z74k9nKZ|Y7LB>hx<56WMuzYIx1PGtc*^84yw;n3v_ZUhltVx80n@I^G}4=QRx%@>=_SLMiWmJwsAN zfr}N0EM#8q|L-=R|6e?iAVdLC;Gb1MwY%$G{j<;Qt!p>ZVo$>QfK?ZbE37pkY&1mo go9#I5_zy!~C(f5TTUbjEiyQ + + 4-key-metrics + +

+ +

+ 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 0000000000000000000000000000000000000000..ca7fee64aaa5f96557e6316720ebc583f5005606 GIT binary patch literal 67921 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%0D~jTVrWY{_-E4o6r=s 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 M)N9F7aRa~q1$nj5k^lez literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 ", 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
+
+
+ + + + + + ` +}