diff --git a/.travis.yml b/.travis.yml index 20a6489..9a0115c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,12 @@ language: go -go: 1.7.4 -install: - - go get github.com/Masterminds/glide - - glide install +go: 1.8 +install: make setup script: - - go test -cover `glide nv` - - go run main.go --org getantibody + - make ci + - go run ./cmd/main/main.go --org goreleaser after_success: - - gem install fpm - - test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash + - go get github.com/mattn/goveralls + - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken="$COVERALLS_TOKEN" + - test -n "$TRAVIS_TAG" && gem install fpm && curl -sL http://git.io/goreleaser | bash notifications: email: false diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..0524ca6 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,49 @@ +memo = "a1e436d5dd32e1ea1ae17c69f248460de8e7f7d7997e0b9dbe8abb574797b841" + +[[projects]] + name = "github.com/caarlos0/spin" + packages = ["."] + revision = "f1f63cceace559408b09577f25210edb9b2215c3" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/golang/protobuf" + packages = ["proto"] + revision = "2bba0603135d7d7f5cb73b2125beeda19c09f4ef" + +[[projects]] + branch = "master" + name = "github.com/google/go-github" + packages = ["github"] + revision = "de33c46a823d3f723514fc5333b75ef2e937b7df" + +[[projects]] + branch = "master" + name = "github.com/google/go-querystring" + packages = ["query"] + revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" + +[[projects]] + name = "github.com/urfave/cli" + packages = ["."] + revision = "0bdeddeeb0f650497d603c4ad7b20cfe685682f6" + version = "v1.19.1" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = ["context"] + revision = "5602c733f70afc6dcec6766be0d5034d4c4f14de" + +[[projects]] + branch = "master" + name = "golang.org/x/oauth2" + packages = [".","internal"] + revision = "a6bd8cefa1811bd24b86f8902872e4e8225f74c4" + +[[projects]] + name = "google.golang.org/appengine" + packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"] + revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" + version = "v1.0.0" diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..0300679 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,16 @@ + +[[dependencies]] + name = "github.com/caarlos0/spin" + version = "^1.0.0" + +[[dependencies]] + branch = "master" + name = "github.com/google/go-github" + +[[dependencies]] + name = "github.com/urfave/cli" + version = "^1.19.1" + +[[dependencies]] + branch = "master" + name = "golang.org/x/oauth2" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9d90340 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) +TEST_PATTERN?=. +TEST_OPTIONS?= + +setup: ## Install all the build and lint dependencies + go get -u github.com/alecthomas/gometalinter + go get -u github.com/golang/dep/... + go get -u github.com/pierrre/gotestcover + go get -u golang.org/x/tools/cmd/cover + dep ensure + gometalinter --install --update + +test: ## Run all the tests + gotestcover $(TEST_OPTIONS) -covermode=count -coverprofile=coverage.out $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=30s + +cover: test ## RUn all the tests and opens the coverage report + go tool cover -html=coverage.out + +fmt: ## gofmt and goimports all go files + find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done + +lint: ## Run all the linters + gometalinter --vendor --disable-all \ + --enable=deadcode \ + --enable=ineffassign \ + --enable=gosimple \ + --enable=staticcheck \ + --enable=gofmt \ + --enable=goimports \ + --enable=dupl \ + --enable=misspell \ + --enable=errcheck \ + --enable=vet \ + --enable=vetshadow \ + --deadline=10m \ + ./... + +ci: lint test ## Run all the tests and code checks + +build: ## Build a beta version + go build -o org-stats ./cmd/main/main.go + +# Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := build diff --git a/README.md b/README.md index 1a2c20f..93e17db 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ -# org-stats [![Build Status](https://travis-ci.org/caarlos0/org-stats.svg?branch=master)](https://travis-ci.org/caarlos0/org-stats) [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=flat-square)](https://github.com/goreleaser) [![SayThanks.io](https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg?style=flat-square)](https://saythanks.io/to/caarlos0) +# org-stats + +[![Release](https://img.shields.io/github/release/caarlos0/org-stats.svg?style=flat-square)](https://github.com/caarlos0/org-stats/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Travis](https://img.shields.io/travis/caarlos0/org-stats.svg?style=flat-square)](https://travis-ci.org/caarlos0/org-stats) +[![Go Report Card](https://goreportcard.com/badge/github.com/caarlos0/org-stats?style=flat-square)](https://goreportcard.com/report/github.com/caarlos0/org-stats) +[![Godoc](https://godoc.org/github.com/caarlos0/org-stats?status.svg&style=flat-square)](http://godoc.org/github.com/caarlos0/org-stats) +[![SayThanks.io](https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg?style=flat-square)](https://saythanks.io/to/caarlos0) +[![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=flat-square)](https://github.com/goreleaser) + Get the contributor stats summary from all repos of any given organization diff --git a/main.go b/cmd/main/main.go similarity index 66% rename from main.go rename to cmd/main/main.go index e9ae224..ccf3c95 100644 --- a/main.go +++ b/cmd/main/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/caarlos0/org-stats/internal/stats" + orgstats "github.com/caarlos0/org-stats" "github.com/caarlos0/spin" "github.com/urfave/cli" ) @@ -29,44 +29,45 @@ func main() { }, } app.Action = func(c *cli.Context) error { - token := c.String("token") + var token = c.String("token") + var org = c.String("org") if token == "" { - return cli.NewExitError("Missing GitHub API token", 1) + return cli.NewExitError("missing github api token", 1) } - org := c.String("org") if org == "" { - return cli.NewExitError("Missing organization name", 1) + return cli.NewExitError("missing organization name", 1) } - s := spin.New(" \033[36m%s Gathering data for '" + org + "'...\033[m") - s.Set(spin.Spin10) - s.Start() - allStats, err := stats.Gather(token, org) - s.Stop() + var spin = spin.New(" \033[36m%s Gathering data for '" + org + "'...\033[m") + spin.Start() + allStats, err := orgstats.Gather(token, org) + spin.Stop() if err != nil { return cli.NewExitError(err.Error(), 1) } printHighlights(allStats) return nil } - app.Run(os.Args) + if err := app.Run(os.Args); err != nil { + panic(err) + } } -func printHighlights(s stats.Stats) { +func printHighlights(s orgstats.Stats) { data := []struct { - stats []stats.StatPair + stats []orgstats.StatPair trophy string kind string }{ { - stats: stats.Sort(s, stats.ExtractCommits), + stats: orgstats.Sort(s, orgstats.ExtractCommits), trophy: "Commit", kind: "commits", }, { - stats: stats.Sort(s, stats.ExtractAdditions), + stats: orgstats.Sort(s, orgstats.ExtractAdditions), trophy: "Lines Added", kind: "lines added", }, { - stats: stats.Sort(s, stats.ExtractDeletions), + stats: orgstats.Sort(s, orgstats.ExtractDeletions), trophy: "Housekeeper", kind: "lines removed", }, diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 7fb2816..0000000 --- a/glide.lock +++ /dev/null @@ -1,38 +0,0 @@ -hash: bb57acc7f887dcb16029c3d6d1df93282b329901750a81c02ed04e10b8b471f2 -updated: 2016-12-13T09:45:50.005740322-02:00 -imports: -- name: github.com/caarlos0/spin - version: f1f63cceace559408b09577f25210edb9b2215c3 -- name: github.com/golang/protobuf - version: df1d3ca07d2d07bba352d5b73c4313b4e2a6203e - subpackages: - - proto -- name: github.com/google/go-github - version: 939928b912496ed4fbeb6e4beb2467b4fa540af7 - subpackages: - - github -- name: github.com/google/go-querystring - version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 - subpackages: - - query -- name: github.com/urfave/cli - version: 0bdeddeeb0f650497d603c4ad7b20cfe685682f6 -- name: golang.org/x/net - version: 6d3beaea10370160dea67f5c9327ed791afd5389 - subpackages: - - context -- name: golang.org/x/oauth2 - version: 1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5 - subpackages: - - internal -- name: google.golang.org/appengine - version: 3f4dbbc0ec153a39878fd524ece9f39732bd4998 - subpackages: - - internal - - internal/base - - internal/datastore - - internal/log - - internal/remote_api - - internal/urlfetch - - urlfetch -testImports: [] diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index b493d91..0000000 --- a/glide.yaml +++ /dev/null @@ -1,10 +0,0 @@ -package: github.com/caarlos0/org-stats -import: -- package: github.com/google/go-github - subpackages: - - github -- package: github.com/urfave/cli - version: ^1.18.1 -- package: golang.org/x/oauth2 -- package: github.com/caarlos0/spin - version: ^1.0.0 diff --git a/goreleaser.yml b/goreleaser.yml index ead1a24..698d687 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -1,5 +1,9 @@ +build: + main: ./cmd/main/main.go brew: - repo: caarlos0/homebrew-tap + github: + owner: caarlos0 + name: homebrew-tap folder: Formula fpm: formats: diff --git a/internal/stats/sort.go b/sort.go similarity index 50% rename from internal/stats/sort.go rename to sort.go index 64ccfac..2f25e1b 100644 --- a/internal/stats/sort.go +++ b/sort.go @@ -1,27 +1,33 @@ -package stats +package orgstats import "sort" +// Extract is a function that converts a multiple stat into a single stat type Extract func(st Stat) int +// ExtractCommits extract the commit section of the given stat var ExtractCommits = func(st Stat) int { return st.Commits } +// ExtractAdditions extract the adds section of the given stat var ExtractAdditions = func(st Stat) int { return st.Additions } +// ExtractDeletions extract the rms section of the given stat var ExtractDeletions = func(st Stat) int { return st.Deletions } func Sort(s Stats, extract Extract) []StatPair { - var result statPairList - for key, value := range s.Stats { + var result []StatPair + for key, value := range s { result = append(result, StatPair{Key: key, Value: extract(value)}) } - sort.Sort(sort.Reverse(result)) + sort.Slice(result, func(i int, j int) bool { + return result[i].Value > result[j].Value + }) return result } @@ -29,17 +35,3 @@ type StatPair struct { Key string Value int } - -type statPairList []StatPair - -func (b statPairList) Len() int { - return len(b) -} - -func (b statPairList) Less(i, j int) bool { - return b[i].Value < b[j].Value -} - -func (b statPairList) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} diff --git a/internal/stats/stats.go b/stats.go similarity index 50% rename from internal/stats/stats.go rename to stats.go index 6f402c4..045b648 100644 --- a/internal/stats/stats.go +++ b/stats.go @@ -1,29 +1,55 @@ -package stats +package orgstats import ( + "context" "time" "github.com/google/go-github/github" "golang.org/x/oauth2" ) +// Stat represents an user adds, rms and commits count type Stat struct { Additions, Deletions, Commits int } -type Stats struct { - Stats map[string]Stat -} +// Stats contains the user->Stat mapping +type Stats map[string]Stat +// NewStats return a new Stats map func NewStats() Stats { - return Stats{make(map[string]Stat)} + return make(map[string]Stat) +} + +// Gather a given organization's stats +func Gather(token, org string) (Stats, error) { + var ctx = context.Background() + var ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + var client = github.NewClient(oauth2.NewClient(ctx, ts)) + var allStats = NewStats() + + allRepos, err := repos(ctx, client, org) + if err != nil { + return allStats, err + } + + for _, repo := range allRepos { + stats, serr := getStats(ctx, client, org, *repo.Name) + if serr != nil { + return allStats, serr + } + for _, cs := range stats { + allStats.add(cs) + } + } + return allStats, err } func (s Stats) add(cs *github.ContributorStats) { if cs.Author == nil { return } - stat := s.Stats[*cs.Author.Login] + stat := s[*cs.Author.Login] var adds int var rms int var commits int @@ -35,17 +61,16 @@ func (s Stats) add(cs *github.ContributorStats) { stat.Additions += adds stat.Deletions += rms stat.Commits += commits - s.Stats[*cs.Author.Login] = stat + s[*cs.Author.Login] = stat } -func repos(org string, client *github.Client) ([]*github.Repository, error) { - opt := &github.RepositoryListByOrgOptions{ +func repos(ctx context.Context, client *github.Client, org string) ([]*github.Repository, error) { + var opt = &github.RepositoryListByOrgOptions{ ListOptions: github.ListOptions{PerPage: 10}, } - var allRepos []*github.Repository for { - repos, resp, err := client.Repositories.ListByOrg(org, opt) + repos, resp, err := client.Repositories.ListByOrg(ctx, org, opt) if err != nil { return allRepos, err } @@ -58,41 +83,16 @@ func repos(org string, client *github.Client) ([]*github.Repository, error) { return allRepos, nil } -func getStats(org, repo string, client *github.Client) ([]*github.ContributorStats, error) { - stats, _, err := client.Repositories.ListContributorsStats(org, repo) +func getStats(ctx context.Context, client *github.Client, org, repo string) ([]*github.ContributorStats, error) { + stats, _, err := client.Repositories.ListContributorsStats(ctx, org, repo) if err != nil { if _, ok := err.(*github.RateLimitError); ok { time.Sleep(time.Duration(15) * time.Second) - return getStats(org, repo, client) + return getStats(ctx, client, org, repo) } if _, ok := err.(*github.AcceptedError); ok { - return getStats(org, repo, client) + return getStats(ctx, client, org, repo) } } return stats, err } - -func Gather(token, org string) (Stats, error) { - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - tc := oauth2.NewClient(oauth2.NoContext, ts) - client := github.NewClient(tc) - allStats := NewStats() - - allRepos, err := repos(org, client) - if err != nil { - return allStats, err - } - - for _, repo := range allRepos { - stats, err := getStats(org, *repo.Name, client) - if err != nil { - return allStats, err - } - for _, cs := range stats { - allStats.add(cs) - } - } - return allStats, err -}