├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── root.go ├── gitpert.go ├── go.mod ├── go.sum └── pkg ├── gitlog ├── README.md └── gitlog.go └── score ├── score.go └── score_test.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v1 15 | - 16 | name: Set up Go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: '1.13.5' 20 | - 21 | name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v1 23 | with: 24 | version: latest 25 | args: release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: gitpert 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - 7 | main: ./gitpert.go 8 | env: 9 | - CGO_ENABLED=0 10 | brews: 11 | - 12 | github: 13 | owner: augmentable-dev 14 | name: homebrew-gitpert 15 | commit_author: 16 | name: augmentable 17 | email: patrick@augmentable.io 18 | homepage: "https://github.com/augmentable-dev/gitpert/" 19 | description: "measure the pertinence of git authors as a time-decayed measure of LOC added and removed to a repository (or a set of files in a repository)" 20 | 21 | archives: 22 | - replacements: 23 | darwin: Darwin 24 | linux: Linux 25 | windows: Windows 26 | 386: i386 27 | amd64: x86_64 28 | checksum: 29 | name_template: 'checksums.txt' 30 | snapshot: 31 | name_template: "{{ .Tag }}-next" 32 | changelog: 33 | sort: asc 34 | filters: 35 | exclude: 36 | - '^docs:' 37 | - '^test:' 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Augmentable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/augmentable-dev/gitpert?status.svg)](https://godoc.org/github.com/augmentable-dev/gitpert) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/augmentable-dev/gitpert)](https://goreportcard.com/report/github.com/augmentable-dev/gitpert) 3 | [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/augmentable-dev/gitpert)](https://www.tickgit.com/browse?repo=github.com/augmentable-dev/gitpert) 4 | 5 | # gitpert 6 | 7 | `gitpert` measures the "pertinence" of git authors as a time-decayed measure of LOC added and removed to a repository (or a set of files in a repository). 8 | It's meant to help identify who the most relevant contributors are based on commit recency, frequency and impact. 9 | 10 | - **impact** in this context is lines of code added plus lines of code removed by a commit. Vendored dependency files are ignored (as best as possible). 11 | - **decay rate** determines how long it takes for the impact of a commit to halve, based on how recently the commit was made. If the decay rate is 10 days, a commit that added 100 lines of code, authored 10 days ago, will be scored at 50. It is a half-life, and can be supplied as a config parameter. 12 | - **score** is the sum of the time decayed impact of every commit in a repository, for a given author. 13 | 14 | The net effect *should* be a ranked list of authors (contributors) where those who have more recently and more frequently contributed "larger" commits surface to the top. 15 | An author who committed the initial code many years ago (maybe higher impact) will likely rank lower than an author who has contributed less impactfully, but much more recently (depending on the decay rate and absolute numbers, of course). 16 | 17 | This could be useful for identifying who the best person to review a new code change might be, or who the best person to ask questions or seek help from might be. Scoring can be done at the repository level, and also for individual files (the most pertinent author for a repository might not be the most pertinent for a directory or file within that repository). 18 | 19 | 20 | ## Installation 21 | 22 | ### Homebrew 23 | 24 | ``` 25 | brew tap augmentable-dev/gitpert 26 | brew install gitpert 27 | ``` 28 | 29 | ## Usage 30 | 31 | Running 32 | 33 | ``` 34 | gitpert 35 | ``` 36 | 37 | in a git repository will output a ranking table like so: 38 | 39 | ``` 40 | Rank Email Name Score Impact Commits 41 | 1 user1@users.noreply.github.com User One 191 6064 92 42 | 2 user2@gmail.com User Two 171 931 27 43 | 3 user3@gmail.com User Three 40 4366 38 44 | ... 45 | ``` 46 | 47 | use the `-d` flag to set the decay rate (in days) `gitpert -d 90` will output a ranking table where score is based on impact that halves at commits made 90 days ago. 48 | 49 | ``` 50 | gitpert -h 51 | ``` 52 | 53 | will have more specific and up to date usage info. 54 | 55 | ## FAQ 56 | 57 | ### What about git-blame? 58 | `git-blame` will tell you about the last modification to lines in a file (the author and revision), and is certainly useful. This tool hopes to provide a higher level view of the net effect of authorship in a repository as a function of time. 59 | 60 | ### Why are changes to "vendored" dependencies ignored? 61 | Authoring a large commit because it adds or removes many dependencies (think the `vendor/` directory in golang projects), while in most contexts an important contribution, gives an outsized "impact" to that commit which probably shouldn't be evaluated in the same way as "typical" LOC. 62 | 63 | ### Should LOC added be weighed the same as LOC removed? 64 | Maybe. This could be worth exposing as a config parameter. One could argue that a LOC added should weigh some amount more than a LOC removed. 65 | 66 | ### How are commits aggregated to an author? 67 | By email address. Authors using multiple email addresses (say, because of GitHub's `...@users.noreply.github.com`) will be tracked as distinct authors. An option to enumerate which emails should be treated as belonging to the same author should be implemented. 68 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "math" 8 | "os" 9 | "text/tabwriter" 10 | 11 | "github.com/augmentable-dev/gitpert/pkg/gitlog" 12 | "github.com/augmentable-dev/gitpert/pkg/score" 13 | "github.com/go-git/go-git/v5" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func handleError(err error) { 18 | if err != nil { 19 | fmt.Println(err) 20 | os.Exit(1) 21 | } 22 | } 23 | 24 | var ( 25 | decayDays int 26 | remote bool 27 | ) 28 | 29 | func init() { 30 | rootCmd.Flags().IntVarP(&decayDays, "decay-rate", "d", 30, "determines how long it takes for the impact of a commit to halve, based on how recently the commit was made") 31 | rootCmd.Flags().BoolVarP(&remote, "remote", "r", false, "whether or not this is a remote repository") 32 | } 33 | 34 | var rootCmd = &cobra.Command{ 35 | Use: "gitpert", 36 | Short: "gitpert ranks committers ", 37 | Args: cobra.RangeArgs(0, 2), 38 | Run: func(cmd *cobra.Command, args []string) { 39 | 40 | // if first argument exists, it's the repoPath 41 | var repoPath string 42 | if len(args) > 0 { 43 | repoPath = args[0] 44 | } else { // otherwise, use the working directory 45 | p, err := os.Getwd() 46 | handleError(err) 47 | repoPath = p 48 | } 49 | 50 | if remote { 51 | dir, err := ioutil.TempDir("", "gitpert_remote_repo") 52 | handleError(err) 53 | defer os.RemoveAll(dir) 54 | 55 | _, err = git.PlainClone(dir, false, &git.CloneOptions{ 56 | URL: repoPath, 57 | // Progress: os.Stdout, 58 | }) 59 | handleError(err) 60 | 61 | repoPath = dir 62 | } 63 | 64 | var fileName string 65 | if len(args) > 1 { 66 | fileName = args[1] 67 | } 68 | 69 | commits, err := gitlog.Exec(context.Background(), repoPath, fileName, []string{}) 70 | handleError(err) 71 | 72 | authorScores := score.AuthorAggregates(commits, decayDays) 73 | 74 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) 75 | fmt.Fprintf(w, "Rank\tEmail\tName\tScore\tImpact\tCommits\n") 76 | for rank, agg := range authorScores { 77 | // ignore scores less than 0 78 | if agg.Score < 1 { 79 | continue 80 | } 81 | fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\t%d\n", rank+1, agg.Email, agg.Name, int(math.Round(agg.Score)), agg.Impact, agg.Commits) 82 | } 83 | w.Flush() 84 | }, 85 | } 86 | 87 | // Execute runs the root command 88 | func Execute() { 89 | err := rootCmd.Execute() 90 | handleError(err) 91 | } 92 | -------------------------------------------------------------------------------- /gitpert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/augmentable-dev/gitpert/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/augmentable-dev/gitpert 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-git/go-git/v5 v5.0.0 7 | github.com/spf13/cobra v1.0.0 8 | github.com/spf13/pflag v1.0.5 // indirect 9 | github.com/src-d/enry/v2 v2.1.0 10 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 // indirect 11 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect 12 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 5 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 9 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 10 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 13 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 14 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 15 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 18 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 19 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 20 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 21 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 22 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 23 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 28 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 29 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 30 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 31 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 32 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 33 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 34 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 35 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 36 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 37 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 38 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 39 | github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= 40 | github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 41 | github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= 42 | github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= 43 | github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= 44 | github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= 45 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 46 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 47 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 48 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 49 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 50 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 51 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 52 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 53 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 54 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 55 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 56 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 57 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 58 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 59 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 60 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 61 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 62 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 63 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 64 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 65 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 66 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 67 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 68 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 69 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 70 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 71 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 72 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 73 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 74 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 75 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 76 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 77 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 78 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 79 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 80 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 81 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 82 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 83 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 84 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 85 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 86 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 87 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 88 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 89 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 90 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 91 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 92 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 93 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 94 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 95 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 99 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 100 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 101 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 102 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 103 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 104 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 105 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 106 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 107 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 108 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 109 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 110 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 111 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 112 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 113 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 114 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 115 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 116 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 117 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 118 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 119 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 120 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 121 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 122 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 123 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 124 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 125 | github.com/src-d/enry/v2 v2.1.0 h1:z1L8t+B8bh3mmjPkJrgOTnVRpFGmTPJsplHX9wAn6BI= 126 | github.com/src-d/enry/v2 v2.1.0/go.mod h1:qQeCMRwzMF3ckeGr+h0tJLdxXnq+NVZsIDMELj0t028= 127 | github.com/src-d/go-oniguruma v1.1.0 h1:EG+Nm5n2JqWUaCjtM0NtutPxU7ZN5Tp50GWrrV8bTww= 128 | github.com/src-d/go-oniguruma v1.1.0/go.mod h1:chVbff8kcVtmrhxtZ3yBVLLquXbzCS6DrxQaAK/CeqM= 129 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 130 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 131 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 132 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 133 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 134 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 135 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 136 | github.com/toqueteos/trie v1.0.0 h1:8i6pXxNUXNRAqP246iibb7w/pSFquNTQ+uNfriG7vlk= 137 | github.com/toqueteos/trie v1.0.0/go.mod h1:Ywk48QhEqhU1+DwhMkJ2x7eeGxDHiGkAdc9+0DYcbsM= 138 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 139 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 140 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 141 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 142 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 143 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 144 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 145 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 146 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 147 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 148 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 149 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 150 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 151 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 152 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU= 153 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 154 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 155 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 156 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 157 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 158 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 159 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 160 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 161 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 162 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 163 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 164 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= 165 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 166 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 167 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 172 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= 178 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= 181 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 183 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 184 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 185 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 186 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 187 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 188 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 189 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 190 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 191 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 192 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 193 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 194 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 196 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 198 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 199 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 200 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 201 | gopkg.in/toqueteos/substring.v1 v1.0.2 h1:urLqCeMm6x/eTuQa1oZerNw8N1KNOIp5hD5kGL7lFsE= 202 | gopkg.in/toqueteos/substring.v1 v1.0.2/go.mod h1:Eb2Z1UYehlVK8LYW2WBVR2rwbujsz3aX8XDrM1vbNew= 203 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 204 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 205 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 206 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 207 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 209 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 210 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 211 | -------------------------------------------------------------------------------- /pkg/gitlog/README.md: -------------------------------------------------------------------------------- 1 | [![go.dev](https://badgen.net/badge/go.dev/reference/blue)](https://pkg.go.dev/github.com/augmentable-dev/gitpert/pkg/gitlog?tab=doc) 2 | 3 | ## gitlog 4 | 5 | Package for parsing the output of `git log` for the purposes of [`gitpert`](https://github.com/augmentable-dev/gitpert). 6 | Can be used for more general purpose `git log` parsing, with (likely) some cleanup. 7 | 8 | Essentially calls `git log --numstat --format=fuller --no-merges --no-decorate --date=iso8601-strict` with [`os/exec`](https://golang.org/pkg/os/exec/) and parses the output. 9 | -------------------------------------------------------------------------------- /pkg/gitlog/gitlog.go: -------------------------------------------------------------------------------- 1 | package gitlog 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // Commit represents a parsed commit from git log 16 | type Commit struct { 17 | SHA string 18 | Author Event 19 | Committer Event 20 | Stats map[string]Stat 21 | } 22 | 23 | // Event represents the who and when of a commit event 24 | type Event struct { 25 | Name string 26 | Email string 27 | When time.Time 28 | } 29 | 30 | // Stat holds the diff stat of a file 31 | type Stat struct { 32 | Additions int 33 | Deletions int 34 | } 35 | 36 | // Result is a list of commits 37 | type Result []*Commit 38 | 39 | func parseLog(reader io.Reader) (Result, error) { 40 | scanner := bufio.NewScanner(reader) 41 | res := make(Result, 0) 42 | 43 | // line prefixes for the `fuller` formatted output 44 | const ( 45 | commit = "commit " 46 | author = "Author: " 47 | authorDate = "AuthorDate: " 48 | 49 | committer = "Commit: " 50 | commitDate = "CommitDate: " 51 | ) 52 | 53 | var currentCommit *Commit 54 | for scanner.Scan() { 55 | line := scanner.Text() 56 | switch { 57 | case strings.HasPrefix(line, commit): 58 | if currentCommit != nil { // if we're seeing a new commit but already have a current commit, we've finished a commit 59 | res = append(res, currentCommit) 60 | } 61 | currentCommit = &Commit{ 62 | SHA: strings.TrimPrefix(line, commit), 63 | Stats: make(map[string]Stat), 64 | } 65 | case strings.HasPrefix(line, author): 66 | s := strings.TrimPrefix(line, author) 67 | spl := strings.Split(s, " ") 68 | email := strings.Trim(spl[len(spl)-1], "<>") 69 | name := strings.Join(spl[:len(spl)-1], " ") 70 | currentCommit.Author.Email = strings.Trim(email, "<>") 71 | currentCommit.Author.Name = strings.TrimSpace(name) 72 | case strings.HasPrefix(line, authorDate): 73 | authorDateString := strings.TrimPrefix(line, authorDate) 74 | aD, err := time.Parse(time.RFC3339, authorDateString) 75 | if err != nil { 76 | return nil, err 77 | } 78 | currentCommit.Author.When = aD 79 | case strings.HasPrefix(line, committer): 80 | s := strings.TrimPrefix(line, committer) 81 | spl := strings.Split(s, " ") 82 | email := strings.Trim(spl[len(spl)-1], "<>") 83 | name := strings.Join(spl[:len(spl)-1], " ") 84 | currentCommit.Committer.Email = strings.Trim(email, "<>") 85 | currentCommit.Committer.Name = strings.TrimSpace(name) 86 | case strings.HasPrefix(line, commitDate): 87 | commitDateString := strings.TrimPrefix(line, commitDate) 88 | cD, err := time.Parse(time.RFC3339, commitDateString) 89 | if err != nil { 90 | return nil, err 91 | } 92 | currentCommit.Committer.When = cD 93 | case strings.HasPrefix(line, " "): // ignore commit message lines 94 | case strings.TrimSpace(line) == "": // ignore empty lines 95 | default: 96 | s := strings.Split(line, "\t") 97 | var additions int 98 | var deletions int 99 | var err error 100 | if s[0] != "-" { 101 | additions, err = strconv.Atoi(s[0]) 102 | if err != nil { 103 | return nil, err 104 | } 105 | } 106 | if s[1] != "-" { 107 | deletions, err = strconv.Atoi(s[1]) 108 | if err != nil { 109 | return nil, err 110 | } 111 | } 112 | currentCommit.Stats[s[2]] = Stat{ 113 | Additions: additions, 114 | Deletions: deletions, 115 | } 116 | } 117 | } 118 | if currentCommit != nil { 119 | res = append(res, currentCommit) 120 | } 121 | 122 | return res, nil 123 | } 124 | 125 | // Exec runs the git log command 126 | func Exec(ctx context.Context, repoPath string, filePattern string, additionalFlags []string) (Result, error) { 127 | gitPath, err := exec.LookPath("git") 128 | if err != nil { 129 | return nil, fmt.Errorf("could not find git: %w", err) 130 | } 131 | 132 | args := []string{"log"} 133 | 134 | // TODO we should allow a way for the caller to specify additional flags (in a way that doesn't conflict with these) 135 | args = append(args, "--numstat", "--format=fuller", "--no-merges", "--no-decorate", "--date=iso8601-strict", "-w") 136 | args = append(args, additionalFlags...) 137 | if filePattern != "" { 138 | args = append(args, filePattern) 139 | } 140 | 141 | cmd := exec.CommandContext(ctx, gitPath, args...) 142 | cmd.Dir = repoPath 143 | 144 | stdout, err := cmd.StdoutPipe() 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | stderr, err := cmd.StderrPipe() 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | if err := cmd.Start(); err != nil { 155 | return nil, err 156 | } 157 | 158 | res, err := parseLog(stdout) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | errs, err := ioutil.ReadAll(stderr) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if err := cmd.Wait(); err != nil { 169 | fmt.Println(string(errs)) 170 | return nil, err 171 | } 172 | 173 | return res, nil 174 | } 175 | -------------------------------------------------------------------------------- /pkg/score/score.go: -------------------------------------------------------------------------------- 1 | package score 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | 7 | "github.com/augmentable-dev/gitpert/pkg/gitlog" 8 | "github.com/src-d/enry/v2" 9 | ) 10 | 11 | // AuthorAggregate is a summary of an author's stats (given a git history) 12 | type AuthorAggregate struct { 13 | Email string 14 | Name string 15 | Commits int 16 | Impact int 17 | Score float64 18 | } 19 | 20 | // AuthorAggregates calculates the aggregate scores of authors, given a list of commits 21 | func AuthorAggregates(commits []*gitlog.Commit, decayDays int) []*AuthorAggregate { 22 | decayHours := 24 * decayDays 23 | 24 | authors := map[string]*AuthorAggregate{} 25 | var authorEmails []string 26 | var firstCommit *gitlog.Commit 27 | for _, commit := range commits { 28 | if firstCommit == nil { 29 | firstCommit = commit 30 | } 31 | authorEmail := commit.Author.Email 32 | if _, ok := authors[authorEmail]; !ok { 33 | authors[authorEmail] = &AuthorAggregate{ 34 | Email: authorEmail, 35 | Name: commit.Author.Name, 36 | } 37 | authorEmails = append(authorEmails, authorEmail) 38 | } 39 | 40 | agg := authors[authorEmail] 41 | hoursAgo := firstCommit.Author.When.Sub(commit.Author.When).Hours() 42 | agg.Commits++ 43 | 44 | var additions int 45 | var deletions int 46 | for file, stat := range commit.Stats { 47 | // ignore diffs in vendor files 48 | // TODO perhaps it's worth allowing for the user to supply file path patterns be ignored? 49 | if enry.IsVendor(file) { 50 | continue 51 | } 52 | additions += stat.Additions 53 | deletions += stat.Deletions 54 | } 55 | agg.Impact += additions + deletions 56 | 57 | agg.Score += float64(additions+deletions) * math.Exp2(-hoursAgo/float64(decayHours)) 58 | } 59 | 60 | sort.SliceStable(authorEmails, func(i, j int) bool { 61 | return authors[authorEmails[j]].Score < authors[authorEmails[i]].Score 62 | }) 63 | 64 | res := make([]*AuthorAggregate, len(authorEmails)) 65 | for rank, authorEmail := range authorEmails { 66 | res[rank] = authors[authorEmail] 67 | } 68 | 69 | return res 70 | } 71 | -------------------------------------------------------------------------------- /pkg/score/score_test.go: -------------------------------------------------------------------------------- 1 | package score 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/augmentable-dev/gitpert/pkg/gitlog" 9 | ) 10 | 11 | func TestScoreSimple(t *testing.T) { 12 | commits := []*gitlog.Commit{ 13 | &gitlog.Commit{ 14 | SHA: "002", 15 | Author: gitlog.Event{ 16 | Name: "A", 17 | Email: "a", 18 | When: time.Now(), 19 | }, 20 | Stats: map[string]gitlog.Stat{ 21 | "file": gitlog.Stat{Additions: 100}, 22 | }, 23 | }, 24 | &gitlog.Commit{ 25 | SHA: "001", 26 | Author: gitlog.Event{ 27 | Name: "A", 28 | Email: "a", 29 | When: time.Now().Add(-30 * 24 * time.Hour), 30 | }, 31 | Stats: map[string]gitlog.Stat{ 32 | "file": gitlog.Stat{Additions: 100}, 33 | }, 34 | }, 35 | } 36 | aAggs := AuthorAggregates(commits, 30) 37 | 38 | if len(aAggs) != 1 { 39 | t.Fatalf("expected 1 unique author, got: %d", len(aAggs)) 40 | } 41 | 42 | if aAggs[0].Commits != 2 { 43 | t.Fatalf("expected 2 commits from the one author, got: %d", aAggs[0].Commits) 44 | } 45 | 46 | if aAggs[0].Impact != 200 { 47 | t.Fatalf("expected an impact of 200 from the one author, got: %d", aAggs[0].Impact) 48 | } 49 | 50 | if int(math.Round(aAggs[0].Score)) != 150 { 51 | t.Fatalf("expected a score of 150 from the one author, got: %d", int(math.Round(aAggs[0].Score))) 52 | } 53 | } 54 | --------------------------------------------------------------------------------