├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example └── cli │ ├── cli.go │ └── screenshot.png ├── githubinfocard.go ├── githubinfocard_test.go ├── query.txt └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .notes.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.11.x 5 | - tip 6 | 7 | before_install: 8 | - go get github.com/tsdtsdtsd/githubinfocard 9 | - go get -v golang.org/x/lint/golint 10 | 11 | install: false 12 | 13 | script: 14 | - diff <(gofmt -d .) <(echo -n) 15 | - go vet ./... 16 | - golint ./... 17 | - go build -o example-cli ./example/cli 18 | - go test -race 19 | 20 | matrix: 21 | allow_failures: 22 | - go: 'tip' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-infocard 2 | 3 | > Experimental Go library which fetches summaries of GitHub repositories 4 | 5 | [![Godoc][godoc-image]][godoc-url] 6 | [![Build Status][travis-image]][travis-url] 7 | [![Go Report Card][grc-image]][grc-url] 8 | 9 | ![CLI example](example/cli/screenshot.png "CLI example") 10 | ## Idea 11 | 12 | I wanted to create an element on my blog, which displays a summary for a given GitHub repository. It could show visitors information like: 13 | 14 | - the repository owners name 15 | - amount of stars, forks and open issues 16 | - date of last commit 17 | - maybe even the language details (the narrow colored bar above the branch button) 18 | 19 | Searching for ways to fetch this kind of data I found the GitHub API and specificaly it's [GraphQL API](https://developer.github.com/v4/guides/intro-to-graphql/). 20 | As I never messed with GraphQL, I thought that this is the perfect opportunity to learn some new things. 21 | 22 | My current idea is to develop a library that helps fetching the data.
23 | As an example and testing application, I want to add a CLI client, which just shows the desired data in an extraordinarily unspectacular manner. 24 | 25 | ## GitHub API 26 | 27 | You need a *Personal access token* for the API, which you can [generate here](https://github.com/settings/tokens) (you need to be signed in first). 28 | 29 | ## Usage 30 | 31 | ``` 32 | go get github.com/tsdtsdtsd/githubinfocard 33 | ``` 34 | 35 | There is an example CLI client in `example/cli/`. It needs two things from you: 36 | - Environment variable named `GITHUB_TOKEN`, containing your personal access token 37 | - Parameter `--url` to indicate a repository 38 | 39 | The following commands assume that you are in `$GO_PATH/github.com/tsdtsdtsd/githubinfocard`: 40 | 41 | ### Windows 42 | 43 | ``` 44 | cmd /V /C "set GITHUB_TOKEN=##TOKEN## && go run example\cli\cli.go --url https://github.com/torvalds/linux" 45 | ``` 46 | 47 | ### Linux 48 | 49 | ``` 50 | GITHUB_TOKEN=##TOKEN## bash -c 'go run example/cli/cli.go --url https://github.com/torvalds/linux 51 | ``` 52 | 53 | 54 | [travis-image]: https://travis-ci.org/tsdtsdtsd/githubinfocard.svg?branch=master 55 | [travis-url]: https://travis-ci.org/tsdtsdtsd/githubinfocard 56 | [grc-image]: https://goreportcard.com/badge/github.com/tsdtsdtsd/githubinfocard 57 | [grc-url]: https://goreportcard.com/report/github.com/tsdtsdtsd/githubinfocard 58 | [godoc-image]: https://godoc.org/github.com/tsdtsdtsd/githubinfocard?status.svg 59 | [godoc-url]: https://godoc.org/github.com/tsdtsdtsd/githubinfocard -------------------------------------------------------------------------------- /example/cli/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/tsdtsdtsd/githubinfocard" 10 | ) 11 | 12 | var ( 13 | flagURL string 14 | ) 15 | 16 | func main() { 17 | 18 | if os.Getenv("GITHUB_TOKEN") == "" { 19 | log.Fatal("missing mandatory environment variable \"GITHUB_TOKEN\"") 20 | } 21 | 22 | flag.StringVar(&flagURL, "url", "", "Complete GitHub-URL (e.g. https://github.com/username/reponame)") 23 | flag.Parse() 24 | 25 | if flagURL == "" { 26 | log.Fatal("missing mandatory parameter \"--url\"") 27 | } 28 | 29 | card, remaining, err := githubinfocard.Load(flagURL, os.Getenv("GITHUB_TOKEN")) 30 | if err != nil { 31 | log.Fatal("error while loading infocard: " + err.Error()) 32 | } 33 | 34 | fmt.Printf("\n### GitHub Infocard for %s ###\n", card.URL) 35 | fmt.Printf("Repository: %s\n", card.Name) 36 | fmt.Printf("Owner: %s\n", card.Owner) 37 | 38 | fmt.Printf("Forks: %d\n", card.Forks) 39 | fmt.Printf("Stars: %d\n", card.Stars) 40 | fmt.Printf("Open issues: %d\n", card.OpenIssues) 41 | 42 | fmt.Printf("Last release: %s\n", card.LastRelease) 43 | fmt.Println("Languages:") 44 | for _, lang := range card.Languages { 45 | fmt.Printf(" - %s\n", lang.Name) 46 | } 47 | 48 | var prio string 49 | switch { 50 | case remaining <= 100: 51 | prio = "! " 52 | case remaining <= 50: 53 | prio = "!! " 54 | case remaining <= 20: 55 | prio = "!!! " 56 | } 57 | fmt.Printf("\n%s(Remaining API calls: %d)\n", prio, remaining) 58 | } 59 | -------------------------------------------------------------------------------- /example/cli/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsdtsdtsd/githubinfocard/aa5e5c278373bd8a17a402c1857648c10aeab28c/example/cli/screenshot.png -------------------------------------------------------------------------------- /githubinfocard.go: -------------------------------------------------------------------------------- 1 | package githubinfocard 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | // "github.com/davecgh/go-spew/spew" 11 | "github.com/shurcooL/githubv4" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | const ( 16 | githubPrefix = "https://github.com/" 17 | ) 18 | 19 | // Card defines a repository info card 20 | type Card struct { 21 | Forks int 22 | LastRelease string 23 | Languages []Language 24 | Name string 25 | OpenIssues int 26 | Owner string 27 | URL string 28 | Stars int 29 | } 30 | 31 | // Language defines a single programming language 32 | type Language struct { 33 | Name string 34 | Color string 35 | } 36 | 37 | // Load tries to load a Card for given repository URL 38 | func Load(url, token string) (*Card, int, error) { 39 | 40 | c := Card{ 41 | URL: url, 42 | } 43 | 44 | if !strings.HasPrefix(url, githubPrefix) { 45 | return &c, 0, fmt.Errorf("URL has to start with \"%s\"", githubPrefix) 46 | } 47 | 48 | // Extract owner and repo name from url 49 | var err error 50 | c.Owner, c.Name, err = parseURL(url) 51 | if err != nil { 52 | return &c, 0, err 53 | } 54 | 55 | // API call 56 | graph, err := fetchGraph(token, c.Owner, c.Name) 57 | if err != nil { 58 | return &c, 0, err 59 | } 60 | // printJSON(graph) 61 | 62 | // Extract from graph 63 | c.Forks = int(graph.Repository.Forks.TotalCount) 64 | c.OpenIssues = int(graph.Repository.Issues.TotalCount) 65 | c.Stars = int(graph.Repository.Stargazers.TotalCount) 66 | 67 | if len(graph.Repository.Releases.Nodes) >= 1 { 68 | c.LastRelease = string(graph.Repository.Releases.Nodes[0].Name) 69 | } 70 | 71 | for _, lang := range graph.Repository.Languages.Nodes { 72 | c.Languages = append(c.Languages, Language{ 73 | Name: string(lang.Name), 74 | Color: string(lang.Color), 75 | }) 76 | } 77 | 78 | return &c, int(graph.RateLimit.Remaining), nil 79 | } 80 | 81 | func parseURL(url string) (string, string, error) { 82 | 83 | tmp := strings.Replace(url, githubPrefix, "", -1) 84 | tmp = strings.TrimRight(tmp, "/") 85 | parts := strings.Split(tmp, "/") 86 | if len(parts) != 2 { 87 | return "", "", fmt.Errorf("URL invalid, expected \"ownerName/repoName\", got \"%s\"", tmp) 88 | } 89 | 90 | if parts[0] == "" || parts[1] == "" { 91 | return "", "", fmt.Errorf("URL invalid, expected \"ownerName/repoName\", got \"%s\"", tmp) 92 | } 93 | 94 | return parts[0], parts[1], nil 95 | } 96 | 97 | func fetchGraph(token, owner, repo string) (*Graph, error) { 98 | 99 | src := oauth2.StaticTokenSource( 100 | &oauth2.Token{AccessToken: token}, 101 | ) 102 | httpClient := oauth2.NewClient(context.Background(), src) 103 | client := githubv4.NewClient(httpClient) 104 | 105 | gqlVars := map[string]interface{}{ 106 | "repositoryOwner": githubv4.String(owner), 107 | "repositoryName": githubv4.String(repo), 108 | } 109 | 110 | var graph Graph 111 | err := client.Query(context.Background(), &graph, gqlVars) 112 | if err != nil { 113 | return &graph, fmt.Errorf("could not query GitHub API: \"%s\"", err.Error()) 114 | } 115 | 116 | return &graph, nil 117 | } 118 | 119 | // printJSON prints v as JSON encoded with indent to stdout. It panics on any error. 120 | // only for testing. 121 | func printJSON(v interface{}) { 122 | w := json.NewEncoder(os.Stdout) 123 | w.SetIndent("", "\t") 124 | err := w.Encode(v) 125 | if err != nil { 126 | panic(err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /githubinfocard_test.go: -------------------------------------------------------------------------------- 1 | package githubinfocard_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/tsdtsdtsd/githubinfocard" 8 | ) 9 | 10 | var repoURL = "https://github.com/tsdtsdtsd/githubinfocard" 11 | 12 | func TestLoad(t *testing.T) { 13 | 14 | _, _, err := githubinfocard.Load(repoURL, os.Getenv("GITHUB_TOKEN")) 15 | 16 | if err != nil { 17 | t.Error("could not load card:", err) 18 | } 19 | 20 | // @todo this needs a fix now that we have githubinfocard.Card.Languages 21 | // 22 | // validCard := githubinfocard.Card{ 23 | // URL: repoURL, 24 | // } 25 | // 26 | // if *card != validCard { 27 | // t.Error("invalid card loaded, expected:", validCard, "got:", *card) 28 | // } 29 | } 30 | -------------------------------------------------------------------------------- /query.txt: -------------------------------------------------------------------------------- 1 | This is the complete query: 2 | 3 | query { 4 | repository(owner:"pi-hole", name:"pi-hole") { 5 | 6 | forks { 7 | totalCount 8 | } 9 | 10 | stargazers { 11 | totalCount 12 | } 13 | 14 | issues(states:OPEN) { 15 | totalCount 16 | } 17 | 18 | commitComments(last:1) { 19 | nodes{ 20 | publishedAt 21 | }} 22 | 23 | releases(last:1) { 24 | nodes{ 25 | name 26 | publishedAt 27 | }} 28 | 29 | languages(first:50) { 30 | nodes{ 31 | name 32 | color 33 | }} 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package githubinfocard 2 | 3 | import "github.com/shurcooL/githubv4" 4 | 5 | // Graph explains the GraphQL request & response 6 | type Graph struct { 7 | Repository struct { 8 | DatabaseID githubv4.Int 9 | Name githubv4.String 10 | Description githubv4.String 11 | URL githubv4.URI 12 | Owner struct { 13 | Name githubv4.String `graphql:"login"` 14 | } 15 | 16 | Issues struct { 17 | TotalCount githubv4.Int 18 | } `graphql:"issues(states:OPEN)"` 19 | 20 | Forks struct { 21 | TotalCount githubv4.Int 22 | } 23 | 24 | Stargazers struct { 25 | TotalCount githubv4.Int 26 | } 27 | 28 | CommitComments struct { 29 | Nodes []struct { 30 | PublishedAt githubv4.DateTime 31 | } 32 | } `graphql:"commitComments(last:1)"` 33 | 34 | Releases struct { 35 | Nodes []struct { 36 | Name githubv4.String 37 | PublishedAt githubv4.DateTime 38 | } 39 | } `graphql:"releases(last:1)"` 40 | 41 | Languages struct { 42 | Nodes []struct { 43 | Name githubv4.String 44 | Color githubv4.String 45 | } 46 | } `graphql:"languages(first:10)"` 47 | } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` 48 | 49 | RateLimit struct { 50 | Cost githubv4.Int 51 | Limit githubv4.Int 52 | Remaining githubv4.Int 53 | ResetAt githubv4.DateTime 54 | } 55 | } 56 | --------------------------------------------------------------------------------