diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..44c08ce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI Workflow +on: [push, pull_request] + +jobs: + lint: + strategy: + matrix: + go-version: [1.13.x] + platform: [ubuntu-latest] + name: golangci-lint + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go-version }} + - run: | + export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14 + go get -u golang.org/x/lint/golint + golint -set_exit_status ./... + go vet ./... + tests: + strategy: + matrix: + go-version: [1.13.x] + platform: [ubuntu-latest, macos-latest] + name: tests + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go-version }} + - run: | + go get -u github.com/mfridman/tparse + go test -v -race -cover -json ./... | $(go env GOPATH)/bin/tparse -all diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a35403 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gothanks \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..640f058 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) + +## [0.1.0] - 2019-11-12 +### Added +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d61ee56 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Pantelis Sampaziotis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a36b007 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +

+ +![Build Status](https://github.com/psampaz/gothanks/workflows/CI%20Workflow/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/psampaz/gothanks)](https://goreportcard.com/report/github.com/psampaz/go-mod-outdated) +[![GoDoc](https://godoc.org/github.com/psampaz/gothanks?status.svg)](https://godoc.org/github.com/psampaz/gothanks) + +Give thanks (in the form of a GitHub ★) to your fellow Go modules maintainers. + +# About + +GoThanks performs the following operations + +- Sends a star to Go's repo (github.com/golang.go) +- Reads your go.mod file and sends a star to every Github repository that your app/library depends on + +This is a simple way to say thanks to the maintainers of the modules you use and the contributors of Go itself. + +# Credits + +GoThanks is a Go port of symfony/thanks implemented by Nicolas Grekas (nicolas-grekas) for PHP. + +Original idea by Doug Tangren (softprops) for Rust. + +# Installation + +```go +$ go get -u github.com/psampaz/gothanks +``` + +# Usage + +In order to run GoThanks you need to have a valid [Github Access Token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). + +You can pass the token as an argument to GoThanks or store it in an environmental variable named GITHUB_TOKEN. + +Inside the folder where your go.mod lives run: + +```go +$ ./gothanks -github-token=xxxxxx +``` + +or + +```go +$ export GITHUB_TOKEN=xxxxx +$ ./gothanks +``` +# Example + +```go +$ ./gothanks +Welcome to GoThanks :) + +You are about to star you beloved dependencies. + +Press y to continue or n to abort +y + +Sending your love.. + +Repository github.com/golang/go is already starred! +Repository github.com/golang/net is already starred! +Sending a star to github.com/google/go-github +Sending a star to github.com/stretchr/testify +Sending a star to github.com/sirupsen/logrus + +Thank you! +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..28ac317 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/psampaz/gothanks + +go 1.13 + +require ( + github.com/google/go-github/v28 v28.1.1 + github.com/sirkon/goproxy v1.4.8 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 // indirect + golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect + golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a279ea2 --- /dev/null +++ b/go.sum @@ -0,0 +1,48 @@ +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +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/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= +github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/pkg/errors v0.8.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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg= +github.com/sirkon/gitlab v0.0.5/go.mod h1:shZhI7CQWIXV84FhVUPietVUS3OcjOm9/YQwrgyVL0Q= +github.com/sirkon/goproxy v1.4.8 h1:99oq8rx+xfMb5tkyP6z6ddpkvpT9wJimvtwNmRmI7LA= +github.com/sirkon/goproxy v1.4.8/go.mod h1:bdsQaJ3VBi0Ua4fML6P3AFtmdkcbO3IHCfQoObjdO3c= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc h1:N3zlSgxkefUH/ecsl37RWTkESTB026kmXzNly8TuZCI= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/gothanks.png b/gothanks.png new file mode 100644 index 0000000..db4052d Binary files /dev/null and b/gothanks.png differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..7d87dce --- /dev/null +++ b/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "bufio" + "context" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "regexp" + "strings" + + "github.com/google/go-github/v28/github" + "github.com/sirkon/goproxy/gomod" + "golang.org/x/oauth2" +) + +func main() { + githubToken := flag.String("github-token", os.Getenv("GITHUB_TOKEN"), "Github access token") + flag.Parse() + + if *githubToken == "" { + fmt.Println("no Github token found") + os.Exit(-1) + } + + dir, err := os.Getwd() + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + input, err := ioutil.ReadFile(dir + "/go.mod") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + fmt.Printf("Welcome to GoThanks :)\n\nYou are about to star you beloved dependencies.\n\nPress y to continue or n to abort\n") + + confirmed, err := confirm(os.Stdin) + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + if !confirmed { + fmt.Println("Aborting.") + os.Exit(0) + } + + parseResult, err := gomod.Parse("", input) + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + ctx := context.Background() + client := githubClient(ctx, *githubToken) + + // Always send your love to Go! + repos := []string{"github.com/golang/go"} + for dep := range parseResult.Require { + repos = append(repos, dep) + } + + fmt.Print("\nSending your love..\n\n") + for _, dep := range repos { + + if rep, ok := isGithubRepo(dep); ok { + x, _, err := client.Activity.IsStarred(ctx, rep.owner, rep.repo) + if err != nil { + fmt.Println(err) + continue + } + + if x { + fmt.Printf("Repository %s is already starred!\n", rep.path) + continue + } + + fmt.Printf("Sending a star to %s\n", rep.path) + + _, err = client.Activity.Star(ctx, rep.owner, rep.repo) + if err != nil { + fmt.Printf("Could not star %s %s", rep.path, err) + } + } + } + + fmt.Println("\nThank you!") +} + +type githubRepo struct { + path string + owner string + repo string +} + +func githubClient(ctx context.Context, token string) *github.Client { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + + return github.NewClient(tc) +} + +func confirm(in io.Reader) (bool, error) { + reader := bufio.NewReader(in) + + char, _, err := reader.ReadRune() + if err != nil { + return false, err + } + + if i := strings.ToLower(strings.TrimSpace(string(char))); i == "y" { + return true, nil + } + + return false, nil +} + +func isGithubRepo(path string) (githubRepo, bool) { + // Make sure we do not forget to star the Github mirrors of Go's subpackages + path = strings.Replace(path, "golang.org/x/", "github.com/golang/", -1) + + re := regexp.MustCompile(`^github\.com\/[a-zA-Z\d-]+\/[a-zA-Z\d-]+`) + repoPath := re.FindString(path) + if repoPath != "" { + parts := strings.Split(repoPath, "/") + res := githubRepo{ + path: repoPath, + owner: parts[1], + repo: parts[2], + } + + return res, true + } + + return githubRepo{}, false +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..9f47926 --- /dev/null +++ b/main_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "io" + "reflect" + "strings" + "testing" + "testing/iotest" +) + +func TestConfirm(t *testing.T) { + tests := []struct { + name string + reader io.Reader + want bool + wantErr bool + }{ + { + name: "y", + reader: strings.NewReader("y"), + want: true, + wantErr: false, + }, + { + name: "y", + reader: strings.NewReader("no"), + want: false, + wantErr: false, + }, + { + name: "", + reader: iotest.DataErrReader(strings.NewReader("")), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := confirm(tt.reader) + if (err != nil) != tt.wantErr { + t.Errorf("Confirm() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Confirm() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_githubRepo(t *testing.T) { + tests := []struct { + name string + repo string + want githubRepo + want1 bool + }{ + { + "github.com/owner/repo", + "github.com/owner/repo", + githubRepo{ + path: "github.com/owner/repo", + owner: "owner", + repo: "repo", + }, + true, + }, + { + "github.com/owner/repo/v2", + "github.com/owner/repo/v2", + githubRepo{ + path: "github.com/owner/repo", + owner: "owner", + repo: "repo", + }, + true, + }, + { + "golang.org/x/lint", + "golang.org/x/lint", + githubRepo{ + path: "github.com/golang/lint", + owner: "golang", + repo: "lint", + }, + true, + }, + { + "xxx.com/owner/repo", + "xxx.com/owner/repo", + githubRepo{}, + false, + }, + { + "empty", + "", + githubRepo{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := isGithubRepo(tt.repo) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("githubRepo() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("githubRepo() got1 = %v, want %v", got1, tt.want1) + } + }) + } +}