├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── ghs.go ├── ghs_test.go ├── go.mod └── go.sum /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ####### Go ####### 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | 26 | ghs 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Keith Smiley (http://keith.so) 2 | 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: 3 | 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | 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. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ghs 2 | 3 | ghs is a simple tool for searching Github repos using their [Search 4 | API](http://developer.github.com/v3/search/) 5 | 6 | [![Build Status](https://travis-ci.org/keith/ghs.png?branch=master)](https://travis-ci.org/keith/ghs) 7 | 8 | ## Installation 9 | 10 | ``` 11 | go get github.com/keith/ghs 12 | ``` 13 | 14 | ## Usage 15 | 16 | Search for repos matching `AFNetworking` 17 | 18 | ``` 19 | $ ghs AFNetworking 20 | 21 | AFNetworking/AFNetworking 11634 Objective-C 22 | AFNetworking/AFIncrementalStore 1662 Objective-C 23 | AFNetworking/AFOAuth2Client 566 Objective-C 24 | steipete/AFDownloadRequestOperation 519 Objective-C 25 | chroman/Doppio 299 Objective-C 26 | subdigital/AFProgressiveImageDownload 253 Objective-C 27 | jnjosh/JJAFAcceleratedDownloadRequestOperation 239 Objective-C 28 | xmartlabs/XLRemoteImageView 211 Objective-C 29 | AFNetworking/Xcode-Project-Templates 208 Objective-C 30 | AFNetworking/AFHTTPRequestOperationLogger 183 Objective-C 31 | ``` 32 | 33 | Search for a maximum of 2 repos matching `AFNetworking` 34 | 35 | ``` 36 | $ ghs -count=2 AFNetworking 37 | 38 | AFNetworking/AFNetworking 11634 Objective-C 39 | AFNetworking/AFIncrementalStore 1662 Objective-C 40 | ``` 41 | 42 | Search for a repo written in [Go](http://golang.org/) matching 43 | `postgres` 44 | 45 | ``` 46 | $ ghs -lang=Go postgres 47 | 48 | lib/pq 623 Go 49 | gosexy/db 160 Go 50 | vmihailenco/pg 96 Go 51 | lxn/go-pgsql 82 Go 52 | jbarham/gopgsqldriver 40 Go 53 | jbarham/pgsql.go 39 Go 54 | replicon/pgreplicaproxy 18 Go 55 | deafbybeheading/dog 12 Go 56 | JackC/pgx 11 Go 57 | jgallagher/go-libpq 9 Go 58 | ``` 59 | -------------------------------------------------------------------------------- /ghs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "golang.org/x/crypto/ssh/terminal" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "unicode/utf8" 17 | ) 18 | 19 | const baseURL = "https://api.github.com/search/repositories" 20 | const helpers = "&sort=stars&order=desc&page=1&per_page=" 21 | 22 | type Query struct { 23 | Q string 24 | Lang string 25 | Limit int 26 | } 27 | 28 | func escapeSearch(s string) string { 29 | return strings.Replace(s, " ", "+", -1) 30 | } 31 | 32 | func searchString(q Query) (string, error) { 33 | var buffer bytes.Buffer 34 | buffer.WriteString(baseURL) 35 | 36 | if q.Q == "" { 37 | return "", errors.New("You must enter a search query") 38 | } 39 | 40 | query := fmt.Sprintf("?q=%s", escapeSearch(q.Q)) 41 | buffer.WriteString(query) 42 | 43 | if q.Lang != "" { 44 | lang := fmt.Sprintf("+language:%s", q.Lang) 45 | buffer.WriteString(lang) 46 | } 47 | 48 | other := fmt.Sprintf("%s%d", helpers, q.Limit) 49 | buffer.WriteString(other) 50 | 51 | return buffer.String(), nil 52 | } 53 | 54 | func repoString(u string, s int, l string) string { 55 | url := strings.TrimPrefix(u, "https://github.com/") 56 | w, _, _ := terminal.GetSize(0) 57 | urlLen := utf8.RuneCountInString(url) 58 | starLen := utf8.RuneCountInString(strconv.Itoa(s)) 59 | langLen := utf8.RuneCountInString(l) 60 | 61 | // If the terminal has no width return an unformatted string 62 | if w < 1 { 63 | return fmt.Sprintf("%s %d %s\n", url, s, l) 64 | } 65 | 66 | spaceLen := w - urlLen - starLen - langLen - 1 67 | if spaceLen < 1 { 68 | spaceLen := w - starLen - langLen - 1 69 | spaces := strings.Repeat(" ", spaceLen) 70 | return fmt.Sprintf("%s\n%s%d %s\n", url, spaces, s, l) 71 | } 72 | 73 | spaces := strings.Repeat(" ", spaceLen) 74 | return fmt.Sprintf("%s%s%d %s\n", url, spaces, s, l) 75 | } 76 | 77 | func requestSearch(url string, client *http.Client) (r *http.Response, e error) { 78 | res, err := http.NewRequest("GET", url, nil) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | res.Header.Set("Accept", "application/vnd.github.v3+json") 84 | return client.Do(res) 85 | } 86 | 87 | func printFromJSON(n int, b []byte) error { 88 | var j map[string]interface{} 89 | err := json.Unmarshal(b, &j) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if j == nil { 95 | return errors.New("No matching repositories") 96 | } 97 | 98 | items := j["items"].([]interface{}) 99 | if len(items) < 1 { 100 | return errors.New("No matching repositories") 101 | } 102 | 103 | for i := 0; i < len(items); i++ { 104 | repo := items[i].(map[string]interface{}) 105 | // name := repo["name"].(string) 106 | url := repo["html_url"].(string) 107 | stars := int(repo["watchers"].(float64)) 108 | language := repo["language"] 109 | lang := "Unknown" 110 | if language != nil { 111 | lang = language.(string) 112 | } 113 | 114 | fmt.Print(repoString(url, stars, lang)) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | var count int 121 | var langNum string 122 | 123 | const countDefault = 10 124 | const countHelp = "The number of results to return" 125 | const langHelp = "The language of the repo" 126 | 127 | var Usage = func() { 128 | fmt.Fprintf(os.Stderr, "Usage: %s [options] query\n", os.Args[0]) 129 | flag.PrintDefaults() 130 | os.Exit(2) 131 | } 132 | 133 | func main() { 134 | flag.Usage = Usage 135 | flag.IntVar(&count, "count", countDefault, countHelp) 136 | flag.IntVar(&count, "c", countDefault, countHelp+" (shorthand)") 137 | flag.StringVar(&langNum, "lang", "", langHelp) 138 | flag.StringVar(&langNum, "l", "", langHelp+"(shorthand)") 139 | flag.Parse() 140 | 141 | if flag.NArg() == 0 || flag.NArg() > 1 { 142 | flag.Usage() 143 | } 144 | 145 | query := flag.Arg(0) 146 | url, err := searchString(Query{query, langNum, count}) 147 | if err != nil { 148 | log.Fatal(err) 149 | } 150 | 151 | client := &http.Client{} 152 | res, err := requestSearch(url, client) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | 157 | buffer, err := ioutil.ReadAll(res.Body) 158 | if err != nil { 159 | log.Fatal(err) 160 | } 161 | 162 | err = printFromJSON(count, buffer) 163 | if err != nil { 164 | fmt.Println(err) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /ghs_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestEscapeSearch(t *testing.T) { 10 | tests := []struct { 11 | in string 12 | out string 13 | }{ 14 | {"", ""}, 15 | {"foo", "foo"}, 16 | {"foo bar", "foo+bar"}, 17 | {"foo bar baz", "foo+bar+baz"}, 18 | } 19 | 20 | for _, test := range tests { 21 | res := escapeSearch(test.in) 22 | if res != test.out { 23 | t.Errorf("Expected (%s) for (%s) got (%s)", test.out, test.in, res) 24 | } 25 | } 26 | } 27 | 28 | func TestSearchString(t *testing.T) { 29 | tests := []struct { 30 | q Query 31 | out string 32 | err bool 33 | }{ 34 | {Query{"", "", 0}, "", true}, // Test empty error 35 | {Query{"foo", "", 0}, baseURL + "?q=foo" + helpers + "0", false}, // Test single query 36 | {Query{"foo bar", "", 1}, baseURL + "?q=foo+bar" + helpers + "1", false}, // Test spaced query 37 | {Query{"bar baz", "go", 2}, baseURL + "?q=bar+baz+language:go" + helpers + "2", false}, // Test spaced and language 38 | {Query{"baz qux", "objc", 5}, baseURL + "?q=baz+qux+language:objc" + helpers + "5", false}, // Test custom number 39 | } 40 | 41 | for _, test := range tests { 42 | res, err := searchString(test.q) 43 | if test.err != (err != nil) { 44 | t.Errorf("Expected error to be (%t) got text (%s) for (%+v)", test.err, err.Error(), test.q) 45 | } 46 | 47 | if res != test.out { 48 | t.Errorf("Expected (%s) for (%+v) got (%s)", test.out, test.q, res) 49 | } 50 | } 51 | } 52 | 53 | func TestRepoString(t *testing.T) { 54 | tests := []struct { 55 | url string 56 | stars int 57 | lang string 58 | name string 59 | }{ 60 | {"https://github.com/foo/bar", 10, "C", "foo/bar"}, 61 | {"othersite.com/foo/bar", 12, "C++", "othersite.com/foo/bar"}, 62 | } 63 | 64 | for _, test := range tests { 65 | str := repoString(test.url, test.stars, test.lang) 66 | fields := strings.Fields(str) 67 | if len(fields) < 1 { 68 | t.Errorf("Expected fields from (%+v)", test) 69 | } 70 | 71 | if fields[0] != test.name { 72 | t.Errorf("Expected (%s) got (%s) for (%+v)", test.name, fields[0], test) 73 | } 74 | 75 | if fields[1] != strconv.Itoa(test.stars) { 76 | t.Errorf("Expected (%d) got (%s) for (%+v)", test.stars, fields[1], test) 77 | } 78 | 79 | if fields[2] != test.lang { 80 | t.Errorf("Expected (%s) got (%s) for (%+v)", test.lang, fields[2], test) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keith/ghs 2 | 3 | go 1.17 4 | 5 | require golang.org/x/crypto v0.17.0 6 | 7 | require ( 8 | golang.org/x/sys v0.15.0 // indirect 9 | golang.org/x/term v0.15.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 4 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 5 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 6 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 7 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 8 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 9 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 10 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 11 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 12 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 13 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 16 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 17 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 24 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 25 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 26 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 27 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 28 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 29 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 30 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 34 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 35 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 36 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 37 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 38 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 39 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 40 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 41 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | --------------------------------------------------------------------------------