├── .gitignore ├── .travis.yml ├── CHANGES ├── README.md ├── ghs.go ├── ghs_test.go ├── main_test.go ├── option.go ├── options_test.go ├── release ├── create_pkg.sh └── release_pkg.sh ├── repository.go ├── search.go ├── search_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | ghs 3 | .idea/ 4 | *.iml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - tip 5 | before_install: 6 | - go get github.com/mattn/goveralls 7 | - go get golang.org/x/tools/cmd/cover 8 | script: 9 | - $HOME/gopath/bin/goveralls -service=travis-ci 10 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 0.0.10 - Apr 15 2017 4 | * [bugfix] Update go-github version 5 | 0.0.9 - May 14 2016 6 | * [feature] Support fork option(-k, --fork) 7 | * [bugfix] Stopped when search result is 0 8 | 0.0.8 - May 03 2016 9 | * [bugfix] Fix pararell search bugs 10 | 0.0.7 - Dec 30 2015 11 | * [bugfix] Fix pararell search bugs 12 | 0.0.6 - Dec 30 2015 13 | * [feature] Support Pararell search 14 | * [feature] Support Fields option(-f, --fields) 15 | * [feature] Support Github API token option to avoid Github API rate limit(-t, --token). 16 | 17 | 0.0.5 - Oct 21 2015 18 | * [feature] Support limits number of search result(-m, --max). 19 | 20 | 0.0.4 - Jun 04 2015 21 | * [feature] Support Github Enterprise Option(-e, --enterprise). @kou-m 22 | * [feature] Support go-latest. check version when ghs -h or -v 23 | 24 | 0.0.3 - Mar 03 2015 25 | * [fix] repository full name overwrap description 26 | * [fix] Enable to run -u or -r without args 27 | 28 | 0.0.2 - Oct 26 2014 29 | * [feature] Support --language, --user and --repo options 30 | 31 | 0.0.1 - Oct 26 2014 32 | * Initial versioned release 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ghs 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/sonatard/ghs.svg?branch=master)](https://travis-ci.org/sonatard/ghs) 5 | [![Coverage Status](https://coveralls.io/repos/github/sonatard/ghs/badge.svg)](https://coveralls.io/github/sonatard/ghs) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/sonatard/ghs)](https://goreportcard.com/report/github.com/sonatard/ghs) 7 | 8 | `ghs` - command-line utility for searching Github repositoy. 9 | 10 | ![](http://f.st-hatena.com/images/fotolife/s/sona-zip/20141029/20141029212146_original.gif?1414585446) 11 | 12 | Trial 13 | =========== 14 | [ghs trial page](https://codepicnic.com/consoles/ghs/embed?sidebar=closed&hide=save,show_result,download,options,run,second_terminal,readme) 15 | 16 | 17 | ghs options 18 | =========== 19 | 20 | ```sh 21 | [sona ~]$ ghs --help 22 | Usage: 23 | ghs [OPTION] "QUERY" 24 | 25 | Application Options: 26 | -f, --fields= limits what fields are searched. 'name', 'description', or 'readme'. (default: name,description) 27 | -k, --fork= Forked repositories included in results. 'true', 'only' or 'false'. 28 | -s, --sort= The sort field. 'stars', 'forks', or 'updated'. (default: best match) 29 | -o, --order= The sort order. 'asc' or 'desc'. (default: desc) 30 | -l, --language= searches repositories based on the language they’re written in. 31 | -u, --user= limits searches to a specific user name. 32 | -r, --repo= limits searches to a specific repository. 33 | -m, --max= limits number of result. range 1-1000 (default: 100) 34 | -v, --version print version infomation and exit. 35 | -e, --enterprise= search from github enterprise. 36 | -t, --token= Github API token to avoid Github API rate limit 37 | -h, --help Show this help message 38 | 39 | Github search APIv3 QUERY infomation: 40 | https://developer.github.com/v3/search/ 41 | https://help.github.com/articles/searching-repositories/ 42 | 43 | Version: 44 | ghs 0.0.9 (https://github.com/sonatard/ghs.git) 45 | ``` 46 | 47 | Install 48 | =========== 49 | 50 | [homebrew](http://brew.sh/index_ja.html), [linuxbrew](http://brew.sh/linuxbrew/) 51 | 52 | ```zsh 53 | brew install sonatard/tools/ghs 54 | ``` 55 | 56 | for Windows 57 | [Releases sonatard/ghs](https://github.com/sonatard/ghs/releases) 58 | 59 | 60 | Usage 61 | =========== 62 | 63 | basic usage. 64 | default search target.(name, description and readme) 65 | ```zsh 66 | ghs "dotfiles" 67 | ``` 68 | 69 | You can restrict the search to just the repository name. 70 | ```zsh 71 | ghs -f name "dotfiles" 72 | ``` 73 | 74 | Limits searches to a specific user. 75 | ```zsh 76 | ghs -f name -u sonatard "dotfiles" 77 | sonatard/dotfiles dotfiles 78 | ``` 79 | 80 | Github Authentication to avoid Github API rate limit 81 | =========== 82 | 83 | Priority of authentication token 84 | 85 | 1. Exec `ghs` with `-t` or `--token` option 86 | 87 | ```bash 88 | $ ghs -t "....." 89 | ``` 90 | 91 | 2. `GITHUB_TOKEN` environmental variable 92 | ```bash 93 | $ export GITHUB_TOKEN="....." 94 | ``` 95 | 96 | 3. github.token in gitconfig 97 | 98 | ```bash 99 | $ git config --global github.token "....." 100 | ``` 101 | 102 | Search Github repository and git clone 103 | =========== 104 | 105 | with [motemen/ghq](https://github.com/motemen/ghq) and [peco/peco](https://github.com/peco/peco) 106 | 107 | ```sh 108 | ghs QUERY | peco | awk '{print $1}' | ghq import 109 | ``` 110 | 111 | create zsh function 112 | 113 | ```zsh 114 | function gpi () { 115 | [ "$#" -eq 0 ] && echo "Usage : gpi QUERY" && return 1 116 | ghs "$@" | peco | awk '{print $1}' | ghq import 117 | } 118 | ``` 119 | 120 | gpi usage 121 | 122 | ```sh 123 | gpi dotfiles 124 | ``` 125 | 126 | ![](http://f.st-hatena.com/images/fotolife/s/sona-zip/20141029/20141029210952_original.gif?1414584687) 127 | 128 | Remove local repository 129 | =========== 130 | 131 | ```zsh 132 | function gpr () { 133 | ghq list --full-path | peco | xargs rm -ir 134 | } 135 | ``` 136 | 137 | ```sh 138 | gpr 139 | ``` 140 | 141 | Search Github repository and open with browser 142 | =========== 143 | 144 | ```zsh 145 | function gho () { 146 | [ "$#" -eq 0 ] && echo "Usage : gho QUERY" && return 1 147 | ghs "$@" | peco | awk '{print "https://github.com/"$1}' | xargs open 148 | } 149 | ``` 150 | 151 | ```sh 152 | gho dotfiles 153 | ``` 154 | 155 | Contributors 156 | =========== 157 | 158 | [kou-m](https://github.com/kou-m) 159 | 160 | 161 | Author 162 | =========== 163 | 164 | [sonatard](https://github.com/sonatard) 165 | -------------------------------------------------------------------------------- /ghs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/motemen/go-gitconfig" 10 | "context" 11 | ) 12 | 13 | // Version is ghs version number 14 | const Version string = "0.0.10" 15 | 16 | const ( 17 | // ExitCodeOK is 0 18 | ExitCodeOK = iota 19 | // ExitCodeError is 1 20 | ExitCodeError 21 | ) 22 | 23 | func main() { 24 | num, err := ghs(os.Args[1:]) 25 | if err != nil { 26 | fmt.Printf("Error: %v\n", err) 27 | os.Exit(ExitCodeError) 28 | } 29 | Debug("Print %d\n", num) 30 | os.Exit(ExitCodeOK) 31 | } 32 | 33 | func ghs(args []string) (int, error) { 34 | flags, err := NewFlags(args) 35 | // --help or error 36 | if err != nil { 37 | Debug("Error : help or parse error\n") 38 | CheckVersion(Version) 39 | flags.PrintHelp() 40 | return 0, errors.New("help or parse error") 41 | } 42 | 43 | version, exitCode, sOpt := flags.ParseOption() 44 | // --version 45 | if version { 46 | Printf("ghs %s\n", Version) 47 | CheckVersion(Version) 48 | return 0, nil 49 | 50 | } 51 | // error options 52 | if exitCode == ExitCodeError { 53 | Debug("Error : Parse option error flags.ParseOption()\n") 54 | flags.PrintHelp() 55 | CheckVersion(Version) 56 | return 0, errors.New("Parse option error.") 57 | } 58 | getToken := func(optsToken string) string { 59 | // -t or --token option 60 | if optsToken != "" { 61 | Debug("Github token get from option value\n") 62 | return optsToken 63 | } 64 | 65 | // GITHUB_TOKEN environment 66 | if token := os.Getenv("GITHUB_TOKEN"); token != "" { 67 | Debug("Github token get from environment value\n") 68 | return token 69 | } 70 | 71 | // github.token in gitconfig 72 | if token, err := gitconfig.GetString("github.token"); err == nil { 73 | Debug("Github token get from gitconfig value\n") 74 | return token 75 | } 76 | 77 | Debug("Github token not found\n") 78 | return "" 79 | } 80 | sOpt.token = getToken(sOpt.token) 81 | 82 | c := context.Background() 83 | repo := NewRepo(NewSearch(c,sOpt)) 84 | reposChan, errChan := repo.Search(c) 85 | 86 | Debug("main thread select start...\n") 87 | reposNum := 0 88 | 89 | for { 90 | select { 91 | case oneReqRepos := <-reposChan: 92 | Debug("main thread chan reposChan\n") 93 | Debug("main thread oneReqRepos length %d\n", len(oneReqRepos)) 94 | var end bool 95 | end, reposNum = repo.Print(oneReqRepos) 96 | Debug("reposNum : %d\n", reposNum) 97 | if end { 98 | Debug("over max\n") 99 | return reposNum, nil 100 | 101 | } 102 | case err := <-errChan: 103 | Debug("main thread chan err\n") 104 | return 0, err 105 | } 106 | } 107 | } 108 | 109 | func Printf(format string, args ...interface{}) { 110 | if os.Getenv("GHS_PRINT") != "no" { 111 | fmt.Printf(format, args...) 112 | } 113 | } 114 | 115 | // Debug display values when DEBUG mode 116 | // This is used only for developer 117 | func Debug(format string, args ...interface{}) { 118 | if os.Getenv("GHS_DEBUG") != "" { 119 | log.Printf(format, args...) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /ghs_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func Test_Ghs(t *testing.T) { 17 | assert := func(result interface{}, want interface{}) { 18 | if !reflect.DeepEqual(result, want) { 19 | t.Errorf("Returned %+v, want %+v", result, want) 20 | } 21 | } 22 | 23 | Setup() 24 | defer Teardown() 25 | 26 | // Normal response 27 | handler := func(w http.ResponseWriter, r *http.Request) { 28 | w.Header().Add("Link", HeaderLink(100, 10)) 29 | var items []string 30 | for i := 1; i < 100+1; i++ { 31 | items = append(items, fmt.Sprintf(`{"id":%d, "full_name": "test/search_word%d", "description":"%d"}`, i, i, i)) 32 | } 33 | fmt.Fprintf(w, `{"total_count": 1000, "items": [%s]}`, strings.Join(items, ",")) 34 | } 35 | mux.HandleFunc("/search/repositories", handler) 36 | 37 | num, err := ghs(strings.Split(fmt.Sprintf("-e %s -m 1000 SEARCH_WORD", server.URL), " ")) 38 | assert(num, 1000) 39 | assert(err, nil) 40 | 41 | num, err = ghs(strings.Split(fmt.Sprintf("-e %s -m 110 SEARCH_WORD", server.URL), " ")) 42 | assert(num, 110) 43 | assert(err, nil) 44 | 45 | num, err = ghs(strings.Split("-v", " ")) 46 | assert(num, 0) 47 | assert(err, nil) 48 | 49 | num, err = ghs(strings.Split("-h", " ")) 50 | assert(num, 0) 51 | assert(err, errors.New("help or parse error")) 52 | 53 | num, err = ghs(strings.Split("-wrong_option", " ")) 54 | assert(num, 0) 55 | assert(err, errors.New("help or parse error")) 56 | 57 | num, err = ghs(strings.Split("-s stars", " ")) 58 | assert(num, 0) 59 | assert(err, errors.New("Parse option error.")) 60 | } 61 | 62 | func Test_GhsTokenTest(t *testing.T) { 63 | assert := func(result interface{}, want interface{}) { 64 | if !reflect.DeepEqual(result, want) { 65 | t.Errorf("Returned %+v, want %+v", result, want) 66 | } 67 | } 68 | 69 | Setup() 70 | defer Teardown() 71 | 72 | // Normal response 73 | handler := func(w http.ResponseWriter, r *http.Request) { 74 | w.Header().Add("Link", HeaderLink(100, 10)) 75 | var items []string 76 | for i := 1; i < 100+1; i++ { 77 | items = append(items, fmt.Sprintf(`{"id":%d, "full_name": "test/search_word%d", "description":"%d"}`, i, i, i)) 78 | } 79 | fmt.Fprintf(w, `{"total_count": 1000, "items": [%s]}`, strings.Join(items, ",")) 80 | } 81 | mux.HandleFunc("/search/repositories", handler) 82 | 83 | num, err := ghs(strings.Split(fmt.Sprintf("-t abcdefg -e %s -m 100 SEARCH_WORD", server.URL), " ")) 84 | assert(num, 100) 85 | assert(err, nil) 86 | 87 | os.Setenv("GITHUB_TOKEN", "abcdefg") 88 | num, err = ghs(strings.Split(fmt.Sprintf("-e %s -m 100 SEARCH_WORD", server.URL), " ")) 89 | assert(num, 100) 90 | assert(err, nil) 91 | os.Setenv("GITHUB_TOKEN", "") 92 | 93 | panicIf := func(err error) { 94 | if err != nil { 95 | panic(err) 96 | } 97 | } 98 | must := panicIf 99 | 100 | run := func(cmd string, args ...string) error { 101 | return exec.Command(cmd, args...).Run() 102 | } 103 | 104 | tmpHome, err := ioutil.TempDir("", "go-gitconfig") 105 | panicIf(err) 106 | 107 | repoDir := filepath.Join(tmpHome, "repo") 108 | must(os.Setenv("HOME", tmpHome)) 109 | must(os.MkdirAll(repoDir, 0777)) 110 | must(os.Chdir(repoDir)) 111 | 112 | must(run("git", "init")) 113 | must(run("git", "config", "--global", "github.token", "abcdefg")) 114 | 115 | num, err = ghs(strings.Split(fmt.Sprintf("-e %s -m 100 SEARCH_WORD", server.URL), " ")) 116 | assert(num, 100) 117 | assert(err, nil) 118 | must(os.RemoveAll(tmpHome)) 119 | } 120 | func Test_GhsInvalidResponse(t *testing.T) { 121 | assert := func(result interface{}, want interface{}) { 122 | if !reflect.DeepEqual(result, want) { 123 | t.Errorf("Returned %+v, want %+v", result, want) 124 | } 125 | } 126 | 127 | Setup() 128 | defer Teardown() 129 | 130 | // Invalid response 131 | handler := func(w http.ResponseWriter, r *http.Request) { 132 | w.Header().Set("Content-Type", "text/plain") 133 | w.WriteHeader(http.StatusNotFound) 134 | } 135 | mux.HandleFunc("/search/repositories", handler) 136 | 137 | num, err := ghs(strings.Split(fmt.Sprintf("-e %s -m 100 SEARCH_WORD", server.URL), " ")) 138 | assert(num, 0) 139 | assert(strings.Contains(err.Error(), "404"), true) 140 | } 141 | 142 | func Example_CheckVersion() { 143 | os.Setenv("GHS_PRINT", "yes") 144 | defer os.Setenv("GHS_PRINT", "no") 145 | 146 | CheckVersion("0.0.1") 147 | // Output: 148 | // 0.0.1 is not latest, you should upgrade to 0.0.10 149 | // -> $ brew update && brew upgrade sona-tar/tools/ghs 150 | } 151 | 152 | func Example_PrintHelp() { 153 | os.Setenv("GHS_PRINT", "yes") 154 | defer os.Setenv("GHS_PRINT", "no") 155 | args := strings.Split("ghs -h", " ")[1:] 156 | flags, _ := NewFlags(args) 157 | flags.PrintHelp() 158 | // Output: 159 | // Usage: 160 | // ghs [OPTION] "QUERY" 161 | // 162 | // Application Options: 163 | // -f, --fields= limits what fields are searched. 'name', 'description', or 164 | // 'readme'. 165 | // -k, --fork= Forked repositories icluded in results. 'true', 'only' or 166 | // 'false'. 167 | // -s, --sort= The sort field. 'stars', 'forks', or 'updated'. (default: 168 | // best match) 169 | // -o, --order= The sort order. 'asc' or 'desc'. (default: desc) 170 | // -l, --language= searches repositories based on the language they’re 171 | // written in. 172 | // -u, --user= limits searches to a specific user name. 173 | // -r, --repo= limits searches to a specific repository. 174 | // -m, --max= limits number of result. range 1-1000 (default: 100) 175 | // -v, --version print version infomation and exit. 176 | // -e, --enterprise= search from github enterprise. 177 | // -t, --token= Github API token to avoid Github API rate 178 | // -h, --help= Show this help message 179 | // 180 | // Github search APIv3 QUERY infomation: 181 | // https://developer.github.com/v3/search/ 182 | // https://help.github.com/articles/searching-repositories/ 183 | // 184 | // Version: 185 | // ghs 0.0.10 (https://github.com/sona-tar/ghs.git) 186 | } 187 | 188 | func Example_PrintfDebug() { 189 | os.Setenv("GHS_PRINT", "yes") 190 | Printf("test") 191 | // Output: 192 | // test 193 | os.Setenv("GHS_PRINT", "no") 194 | 195 | os.Setenv("GHS_DEBUG", "yes") 196 | 197 | Debug("test") 198 | // Output: 199 | // test 200 | os.Setenv("GHS_DEBUG", "") 201 | } 202 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "os" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | 14 | // setup() 15 | 16 | code := m.Run() 17 | // teardown() 18 | 19 | os.Exit(code) 20 | } 21 | 22 | var ( 23 | repo *Repo 24 | server *httptest.Server 25 | mux *http.ServeMux 26 | ) 27 | 28 | func Setup() { 29 | os.Setenv("GHS_PRINT", "no") 30 | mux = http.NewServeMux() 31 | server = httptest.NewServer(mux) 32 | } 33 | 34 | func Teardown() { 35 | server.Close() 36 | } 37 | 38 | func testMethod(t *testing.T, r *http.Request, want string) { 39 | if got := r.Method; got != want { 40 | t.Errorf("Request method: %v, want %v", got, want) 41 | } 42 | } 43 | 44 | type values map[string]string 45 | 46 | func testFormValues(t *testing.T, r *http.Request, values values) { 47 | want := url.Values{} 48 | for k, v := range values { 49 | want.Add(k, v) 50 | } 51 | 52 | r.ParseForm() 53 | if got := r.Form; !reflect.DeepEqual(got, want) { 54 | t.Errorf("Request parameters: %v, want %v", got, want) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "strings" 8 | 9 | "github.com/jessevdk/go-flags" 10 | ) 11 | 12 | // Flags is args, opts and parser 13 | type Flags struct { 14 | args []string 15 | cmdOpts CmdOptions 16 | parser *flags.Parser 17 | } 18 | 19 | // CmdOptions is ghs command line option list 20 | type CmdOptions struct { 21 | Fields string `short:"f" long:"fields" description:"limits what fields are searched. 'name', 'description', or 'readme'."` 22 | Fork string `short:"k" long:"fork" description:"Forked repositories icluded in results. 'true', 'only' or 'false'."` 23 | Sort string `short:"s" long:"sort" description:"The sort field. 'stars', 'forks', or 'updated'." default:"best match"` 24 | Order string `short:"o" long:"order" description:"The sort order. 'asc' or 'desc'." default:"desc"` 25 | Language string `short:"l" long:"language" description:"searches repositories based on the language they’re written in."` 26 | User string `short:"u" long:"user" description:"limits searches to a specific user name."` 27 | Repository string `short:"r" long:"repo" description:"limits searches to a specific repository."` 28 | Max int `short:"m" long:"max" description:"limits number of result. range 1-1000" default:"100"` 29 | Version bool `short:"v" long:"version" description:"print version infomation and exit."` 30 | Enterprise string `short:"e" long:"enterprise" description:"search from github enterprise."` 31 | Token string `short:"t" long:"token" description:"Github API token to avoid Github API rate "` 32 | Help string `short:"h" long:"help" description:"Show this help message"` 33 | } 34 | 35 | func NewFlags(args []string) (*Flags, error) { 36 | var opts CmdOptions 37 | parser := flags.NewParser(&opts, flags.None) 38 | parser.Name = "ghs" 39 | parser.Usage = "[OPTION] \"QUERY\"" 40 | args, err := parser.ParseArgs(args) 41 | return &Flags{args, opts, parser}, err 42 | } 43 | 44 | func (f *Flags) ParseOption() (bool, int, *SearchOpt) { 45 | if f.cmdOpts.Version { 46 | return true, ExitCodeOK, nil 47 | } 48 | 49 | errorQuery := func(opts CmdOptions, args []string) bool { 50 | noargs := len(args) == 0 51 | noopt := opts.User == "" && opts.Repository == "" && opts.Language == "" 52 | return noargs && noopt 53 | } 54 | 55 | if errorQuery(f.cmdOpts, f.args) { 56 | Debug("Error : noargs & noopt\n") 57 | return false, ExitCodeError, nil 58 | } 59 | 60 | if f.cmdOpts.Max < 1 || f.cmdOpts.Max > 1000 { 61 | return false, ExitCodeError, nil 62 | } 63 | 64 | var baseURL *url.URL 65 | if f.cmdOpts.Enterprise != "" { 66 | eURL, err := url.Parse(f.cmdOpts.Enterprise) 67 | Debug("%#v\n", eURL) 68 | if err != nil { 69 | Debug(`Error : Parse "%v"`+"\n", f.cmdOpts.Enterprise) 70 | return false, ExitCodeError, nil 71 | } 72 | baseURL = eURL 73 | } 74 | 75 | buildQuery := func(opts CmdOptions, args []string) string { 76 | var query []string 77 | 78 | if opts.Fields != "" { 79 | query = append(query, "in:"+opts.Fields) 80 | } 81 | if opts.Fork != "" { 82 | query = append(query, "fork:"+opts.Fork) 83 | } 84 | if opts.Language != "" { 85 | query = append(query, "language:"+opts.Language) 86 | } 87 | if opts.User != "" { 88 | query = append(query, "user:"+opts.User) 89 | } 90 | if opts.Repository != "" { 91 | query = append(query, "repo:"+opts.Repository) 92 | } 93 | query = append(query, args...) 94 | return strings.Join(query, " ") 95 | } 96 | 97 | return false, ExitCodeOK, 98 | &SearchOpt{ 99 | sort: f.cmdOpts.Sort, 100 | order: f.cmdOpts.Order, 101 | query: buildQuery(f.cmdOpts, f.args), 102 | perPage: 100, 103 | max: f.cmdOpts.Max, 104 | baseURL: baseURL, 105 | token: f.cmdOpts.Token, 106 | } 107 | } 108 | 109 | func (f *Flags) PrintHelp() { 110 | if os.Getenv("GHS_PRINT") != "no" { 111 | f.parser.WriteHelp(os.Stdout) 112 | fmt.Printf("\n") 113 | fmt.Printf("Github search APIv3 QUERY infomation:\n") 114 | fmt.Printf(" https://developer.github.com/v3/search/\n") 115 | fmt.Printf(" https://help.github.com/articles/searching-repositories/\n") 116 | fmt.Printf("\n") 117 | fmt.Printf("Version:\n") 118 | fmt.Printf(" ghs %s (https://github.com/sona-tar/ghs.git)\n", Version) 119 | fmt.Printf("\n") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type parseTestReulst struct { 11 | version bool 12 | exitCode int 13 | sOpt *SearchOpt 14 | } 15 | 16 | func TestOption_Parse(t *testing.T) { 17 | assert := func(result interface{}, want interface{}) { 18 | if !reflect.DeepEqual(result, want) { 19 | t.Errorf("Returned %+v, want %+v", result, want) 20 | } 21 | } 22 | 23 | // want :exit, exitCode , sOpt, url, token 24 | assert(testParse("ghs -v"), &parseTestReulst{true, ExitCodeOK, nil}) 25 | assert(testParse("ghs -h"), &parseTestReulst{false, ExitCodeError, nil}) 26 | 27 | defaultOpt := SearchOpt{ 28 | sort: "best match", 29 | order: "desc", 30 | query: "SEARCH_WORD", 31 | max: 100, 32 | perPage: 100, 33 | baseURL: nil, 34 | token: "", 35 | } 36 | 37 | // normal query test 38 | wantOpt := defaultOpt 39 | assert(testParse("ghs SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 40 | wantOpt = defaultOpt 41 | wantOpt.sort = "stars" 42 | assert(testParse("ghs -s stars SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 43 | wantOpt = defaultOpt 44 | wantOpt.order = "asc" 45 | assert(testParse("ghs -o asc SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 46 | wantOpt = defaultOpt 47 | wantOpt.max = 1000 48 | assert(testParse("ghs -m 1000 SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 49 | wantOpt = defaultOpt 50 | wantOpt.baseURL, _ = url.Parse("http://test.exmaple/") 51 | assert(testParse("ghs -e http://test.exmaple/ SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 52 | wantOpt = defaultOpt 53 | wantOpt.token = "abcdefg" 54 | assert(testParse("ghs -t abcdefg SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 55 | wantOpt = defaultOpt 56 | wantOpt.query = "in:name SEARCH_WORD" 57 | assert(testParse("ghs -f name SEARCH_WORD"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 58 | wantOpt = defaultOpt 59 | wantOpt.query = "user:sona-tar" 60 | assert(testParse("ghs -u sona-tar"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 61 | wantOpt = defaultOpt 62 | wantOpt.query = "repo:sona-tar/ghs" 63 | assert(testParse("ghs -r sona-tar/ghs"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 64 | wantOpt = defaultOpt 65 | wantOpt.query = "language:golang" 66 | assert(testParse("ghs -l golang"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 67 | 68 | // no args test 69 | assert(testParse("ghs"), &parseTestReulst{false, ExitCodeError, nil}) 70 | assert(testParse("ghs -o asc"), &parseTestReulst{false, ExitCodeError, nil}) 71 | wantOpt = defaultOpt 72 | wantOpt.query = "user:sona-tar" 73 | assert(testParse("ghs -u sona-tar"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 74 | wantOpt = defaultOpt 75 | wantOpt.query = "repo:sona-tar/ghs" 76 | assert(testParse("ghs -r sona-tar/ghs"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 77 | wantOpt = defaultOpt 78 | wantOpt.query = "language:golang" 79 | assert(testParse("ghs -l golang"), &parseTestReulst{false, ExitCodeOK, &wantOpt}) 80 | 81 | // invalid option value test 82 | assert(testParse("ghs -m 1001 SEARCH_WORD"), &parseTestReulst{false, ExitCodeError, nil}) 83 | assert(testParse("ghs -m 0 SEARCH_WORD"), &parseTestReulst{false, ExitCodeError, nil}) 84 | assert(testParse("ghs -e : SEARCH_WORD"), &parseTestReulst{false, ExitCodeError, nil}) 85 | } 86 | 87 | func testParse(args_string string) *parseTestReulst { 88 | args := strings.Split(args_string, " ")[1:] 89 | flags, _ := NewFlags(args) 90 | version, exitCode, sOpt := flags.ParseOption() 91 | return &parseTestReulst{version, exitCode, sOpt} 92 | } 93 | -------------------------------------------------------------------------------- /release/create_pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # @(#) create_pkg.sh create release packages. 4 | # 5 | # pkg 6 | # ├── archive 7 | # │   └── 0.0.1 8 | # │   ├── ghs-0.0.1-darwin_386.zip 9 | # │   ├── ghs-0.0.1-darwin_amd64.zip 10 | # │   ├── ghs-0.0.1-linux_386.tar.gz 11 | # │   ├── ghs-0.0.1-linux_amd64.tar.gz 12 | # │   ├── ghs-0.0.1-windows_386.zip 13 | # │   └── ghs-0.0.1-windows_amd64.zip 14 | # └── work 15 | # └── 0.0.1 16 | # ├── ghs-0.0.1-darwin_386 17 | # │   ├── CHANGES 18 | # │   ├── ghs 19 | # │   └── README.md 20 | # ├── ghs-0.0.1-darwin_amd64 21 | # │   ├── CHANGES 22 | # │   ├── ghs 23 | # │   └── README.md 24 | # ├── ghs-0.0.1-linux_386 25 | # │   ├── CHANGES 26 | # │   ├── ghs 27 | # │   └── README.md 28 | # ├── ghs-0.0.1-linux_amd64 29 | # │   ├── CHANGES 30 | # │   ├── ghs 31 | # │   └── README.md 32 | # ├── ghs-0.0.1-windows_386 33 | # │   ├── CHANGES 34 | # │   ├── ghs.exe 35 | # │   └── README.md 36 | # └── ghs-0.0.1-windows_amd64 37 | # ├── CHANGES 38 | # ├── ghs.exe 39 | # └── README.md 40 | 41 | repo_root=$(pwd) 42 | 43 | XC_VERSION=$1 44 | [ -z "${XC_VERSION}" ] && echo "usage : create_pkg.sh " && exit 1 45 | 46 | XC_ARCH=${XC_ARCH:-386 amd64} 47 | XC_OS=${XC_OS:-linux darwin windows} 48 | 49 | work_dir=${repo_root}/pkg/work/${XC_VERSION} 50 | rm -rf pkg/ 51 | gox \ 52 | -os="${XC_OS}" \ 53 | -arch="${XC_ARCH}" \ 54 | -output "${work_dir}/{{.Dir}}-${XC_VERSION}-{{.OS}}_{{.Arch}}/{{.Dir}}" 55 | 56 | 57 | 58 | archive_dir=${repo_root}/pkg/archive/${XC_VERSION} 59 | mkdir -p ${work_dir} ${archive_dir} 60 | 61 | cd ${work_dir} 62 | for target in *; 63 | do 64 | cp ${repo_root}/README.md ${target} 65 | cp ${repo_root}/CHANGES ${target} 66 | if [ $(echo $target | grep linux) ]; then 67 | tar zcvf ${archive_dir}/${target}.tar.gz ${target} 68 | else 69 | zip -r ${archive_dir}/${target}.zip ${target} 70 | fi 71 | done 72 | -------------------------------------------------------------------------------- /release/release_pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # @(#) release_pkg.sh create release packages. 4 | # 5 | # pkg 6 | # ├── archive 7 | # │   └── 0.0.1 8 | # │   ├── ghs-0.0.1-darwin_386.zip 9 | # │   ├── ghs-0.0.1-darwin_amd64.zip 10 | # │   ├── ghs-0.0.1-linux_386.tar.gz 11 | # │   ├── ghs-0.0.1-linux_amd64.tar.gz 12 | # │   ├── ghs-0.0.1-windows_386.zip 13 | # │   └── ghs-0.0.1-windows_amd64.zip 14 | # 15 | # Release Frow 16 | # $ git tag -a ${VERSION} 17 | # $ git push --tags 18 | # $ ./release/create_pkg.sh ${VERSION} 19 | # $ ./release/release_pkg.sh ${VERSION} 20 | 21 | XC_VERSION=$1 22 | [ -z "${XC_VERSION}" ] && echo "usage : release_pkg.sh " && exit 1 23 | 24 | ghr $2 ${XC_VERSION} pkg/archive/${XC_VERSION}/ 25 | 26 | openssl sha1 pkg/archive/${XC_VERSION}/* 27 | -------------------------------------------------------------------------------- /repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/google/go-github/github" 7 | "context" 8 | ) 9 | 10 | type Repo struct { 11 | search *Search 12 | maxItem int 13 | printCount int 14 | } 15 | 16 | func NewRepo(s *Search) *Repo { 17 | return &Repo{s, 0, 0} 18 | } 19 | 20 | func (r *Repo) Search(c context.Context) (<-chan []github.Repository, <-chan error) { 21 | var wg sync.WaitGroup 22 | reposBuff := make(chan []github.Repository, 1) 23 | errChan := make(chan error, 1) 24 | 25 | // 1st search 26 | 27 | repos, lastPage, max, err := r.search.First(c) 28 | if err != nil { 29 | Debug("Error First()\n") 30 | errChan <- err 31 | return reposBuff, errChan 32 | } 33 | r.maxItem = max 34 | // notify main thread of first search result 35 | reposBuff <- repos 36 | 37 | // 2nd - 10th search 38 | go func() { 39 | for page := 2; page < lastPage+1; page++ { 40 | Debug("sub thread start %d\n", page) 41 | wg.Add(1) 42 | go func(p int) { 43 | // notify main thread of 2nd - 10th search result 44 | rs, err := r.search.Exec(c,p) 45 | if err != nil { 46 | Debug("sub thread error %d\n", p) 47 | errChan <- err 48 | } 49 | reposBuff <- rs 50 | wg.Done() 51 | Debug("sub thread end %d\n", p) 52 | }(page) 53 | } 54 | Debug("sub thread wait...\n") 55 | wg.Wait() 56 | Debug("sub thread wakeup!!\n") 57 | close(reposBuff) 58 | }() 59 | 60 | Debug("main thread return\n") 61 | 62 | return reposBuff, errChan 63 | } 64 | 65 | func (r *Repo) Print(repos []github.Repository) (bool, int) { 66 | Debug("repos length %d\n", len(repos)) 67 | repoNameMaxLen := 0 68 | for _, repo := range repos { 69 | repoNamelen := len(*repo.FullName) 70 | if repoNamelen > repoNameMaxLen { 71 | repoNameMaxLen = repoNamelen 72 | } 73 | } 74 | printLine := func(repo *github.Repository) { 75 | if repo.FullName != nil { 76 | Printf("%v", *repo.FullName) 77 | } 78 | Printf(" ") 79 | paddingLen := repoNameMaxLen - len(*repo.FullName) 80 | for i := 0; i < paddingLen; i++ { 81 | Printf(" ") 82 | } 83 | if repo.Description != nil { 84 | Printf("%v", *repo.Description) 85 | } 86 | Printf("\n") 87 | } 88 | for _, repo := range repos { 89 | printLine(&repo) 90 | r.printCount++ 91 | Debug("printCount %d, r.maxItem %d\n", r.printCount, r.maxItem) 92 | if r.printCount >= r.maxItem { 93 | return true, r.printCount 94 | } 95 | } 96 | return false, r.printCount 97 | } 98 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/google/go-github/github" 9 | "golang.org/x/oauth2" 10 | "context" 11 | ) 12 | 13 | type Search struct { 14 | client *github.Client 15 | option *SearchOpt 16 | } 17 | 18 | type SearchOpt struct { 19 | sort string 20 | order string 21 | query string 22 | max int 23 | perPage int 24 | baseURL *url.URL 25 | token string 26 | } 27 | 28 | func NewSearch(c context.Context,opt *SearchOpt) *Search { 29 | var tc *http.Client 30 | 31 | if opt.token != "" { 32 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: opt.token}) 33 | tc = oauth2.NewClient(c, ts) 34 | } 35 | 36 | cli := github.NewClient(tc) 37 | 38 | if opt.baseURL != nil { 39 | cli.BaseURL = opt.baseURL 40 | } 41 | 42 | return &Search{client: cli, option: opt} 43 | } 44 | 45 | func repoSearch(c context.Context,client *github.Client, page int, opt *SearchOpt) (*github.RepositoriesSearchResult, *github.Response, error) { 46 | opts := &github.SearchOptions{ 47 | Sort: opt.sort, 48 | Order: opt.order, 49 | TextMatch: false, 50 | ListOptions: github.ListOptions{PerPage: opt.perPage, Page: page}, 51 | } 52 | ret, resp, err := client.Search.Repositories(c,opt.query, opts) 53 | return ret, resp, err 54 | } 55 | 56 | func (s *Search) First(c context.Context) (repos []github.Repository, lastPage int, maxItem int, err error) { 57 | ret, resp, err := repoSearch(c,s.client, 1, s.option) 58 | if err != nil { 59 | Debug("error repoSearch()\n") 60 | return nil, 0, 0, err 61 | } 62 | if len(ret.Repositories) == 0 { 63 | return nil, 0, 0, errors.New("Repository not found") 64 | } 65 | 66 | Debug("main thread repos length %d\n", len(ret.Repositories)) 67 | 68 | max := s.option.max 69 | Debug("Total = %d\n", *ret.Total) 70 | Debug("s.option.max = %d\n", s.option.max) 71 | if *ret.Total < max { 72 | max = *ret.Total 73 | } 74 | 75 | last := ((max - 1) / s.option.perPage) + 1 76 | Debug("resp.LastPage = %d\n", resp.LastPage) 77 | Debug("last = %d\n", last) 78 | if resp.LastPage < last { 79 | last = resp.LastPage 80 | } 81 | 82 | return ret.Repositories, last, max, nil 83 | } 84 | 85 | func (s *Search) Exec(c context.Context,page int) (repos []github.Repository, err error) { 86 | Debug("Page%d go func search start\n", page) 87 | 88 | Debug("Page%d query : %s\n", page, s.option.query) 89 | ret, _, err := repoSearch(c, s.client, page, s.option) 90 | if err != nil { 91 | return nil, err 92 | } 93 | Debug("Page%d go func search result length %d\n", page, len(ret.Repositories)) 94 | Debug("Page%d go func search end\n", page) 95 | 96 | return ret.Repositories, nil 97 | } 98 | -------------------------------------------------------------------------------- /search_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type pageTestReulst struct { 14 | printLastPage int 15 | printMax int 16 | } 17 | 18 | func TestSearch_Page(t *testing.T) { 19 | assert := func(result interface{}, want interface{}) { 20 | if !reflect.DeepEqual(result, want) { 21 | t.Errorf("Returned %+v, want %+v", result, want) 22 | } 23 | } 24 | // pageTest: max, total, perPage 25 | // Result: lastPage, max 26 | // normal test 27 | assert(pageTest(1000, 1000, 100), &pageTestReulst{10, 1000}) 28 | // total test 29 | assert(pageTest(1000, 100, 100), &pageTestReulst{1, 100}) 30 | assert(pageTest(1000, 101, 100), &pageTestReulst{2, 101}) 31 | assert(pageTest(1000, 500, 100), &pageTestReulst{5, 500}) 32 | assert(pageTest(1000, 1, 100), &pageTestReulst{1, 1}) 33 | assert(pageTest(2000, 1000, 100), &pageTestReulst{10, 1000}) 34 | // max test 35 | assert(pageTest(100, 1000, 100), &pageTestReulst{1, 100}) 36 | assert(pageTest(101, 1000, 100), &pageTestReulst{2, 101}) 37 | assert(pageTest(500, 1000, 100), &pageTestReulst{5, 500}) 38 | assert(pageTest(1, 1000, 100), &pageTestReulst{1, 1}) 39 | assert(pageTest(2000, 1000, 100), &pageTestReulst{10, 1000}) 40 | // perPage test 41 | assert(pageTest(1000, 1000, 1), &pageTestReulst{1000, 1000}) 42 | assert(pageTest(1000, 1000, 2), &pageTestReulst{500, 1000}) 43 | assert(pageTest(1000, 1000, 10), &pageTestReulst{100, 1000}) 44 | assert(pageTest(1000, 1000, 50), &pageTestReulst{20, 1000}) 45 | assert(pageTest(1000, 1000, 1000), &pageTestReulst{1, 1000}) 46 | } 47 | 48 | func pageTest(max int, total int, perPage int) *pageTestReulst { 49 | Setup() 50 | defer Teardown() 51 | c := context.Background() 52 | 53 | // create input 54 | lastPage := ((total - 1) / perPage) + 1 55 | url, _ := url.Parse(server.URL) 56 | repo = NewRepo(NewSearch(c, option(max, perPage, url, ""))) 57 | 58 | // create output 59 | mux.HandleFunc("/search/repositories", func(w http.ResponseWriter, r *http.Request) { 60 | w.Header().Add("Link", HeaderLink(perPage, lastPage)) 61 | fmt.Fprintf(w, `{"total_count": %d, "items": [{"id":1}]}`, total) 62 | }) 63 | 64 | // test 65 | _, printLastPage, printMax, _ := repo.search.First(c) 66 | 67 | return &pageTestReulst{ 68 | printLastPage: printLastPage, 69 | printMax: printMax, 70 | } 71 | } 72 | 73 | func HeaderLink(perPage int, lastPage int) string { 74 | link := func(per int, page int, rel string) string { 75 | url := "https://api.github.com/search/repositories" 76 | query := fmt.Sprintf("?q=ghs&per_page=%d&page=%d", per, page) 77 | link := "<" + url + query + ">" 78 | return link + "; " + fmt.Sprintf(`rel="%s"`, rel) 79 | } 80 | next := link(perPage, 2, "next") 81 | last := link(perPage, lastPage, "last") 82 | return "Link: " + next + ", " + last 83 | 84 | } 85 | 86 | func option(max int, perPage int, url *url.URL, token string) *SearchOpt { 87 | return &SearchOpt{ 88 | sort: "best match", 89 | order: "desc", 90 | query: "ghs", 91 | perPage: perPage, 92 | max: max, 93 | baseURL: url, 94 | token: token, 95 | } 96 | } 97 | 98 | type requestTestReulst struct { 99 | repolen int 100 | printMax int 101 | } 102 | 103 | func TestSearch_Request(t *testing.T) { 104 | assert := func(result interface{}, want interface{}) { 105 | if !reflect.DeepEqual(result, want) { 106 | t.Errorf("Returned %+v, want %+v", result, want) 107 | } 108 | } 109 | 110 | var handler func(w http.ResponseWriter, r *http.Request) 111 | // Normal response 112 | handler = func(w http.ResponseWriter, r *http.Request) { 113 | testMethod(t, r, "GET") 114 | testFormValues(t, r, values{ 115 | "q": "ghs", 116 | "sort": "best match", 117 | "order": "desc", 118 | "page": "1", 119 | "per_page": "100", 120 | }) 121 | var items []string 122 | for i := 1; i < 100+1; i++ { 123 | items = append(items, fmt.Sprintf(`{"id":%d}`, i)) 124 | } 125 | fmt.Fprintf(w, `{"total_count": 102, "items": [%s]}`, strings.Join(items, ",")) 126 | } 127 | ret, _ := firstRequestTest(t, handler) 128 | assert(ret, &requestTestReulst{100, 102}) 129 | 130 | // Repository not found 131 | handler = func(w http.ResponseWriter, r *http.Request) { 132 | testMethod(t, r, "GET") 133 | testFormValues(t, r, values{ 134 | "q": "ghs", 135 | "sort": "best match", 136 | "order": "desc", 137 | "page": "1", 138 | "per_page": "100", 139 | }) 140 | fmt.Fprintf(w, `{"total_count": 0, "items": []}`) 141 | } 142 | ret, _ = firstRequestTest(t, handler) 143 | assert(ret, &requestTestReulst{0, 0}) 144 | 145 | // Invalid response 146 | handler = func(w http.ResponseWriter, r *http.Request) { 147 | testMethod(t, r, "GET") 148 | testFormValues(t, r, values{ 149 | "q": "ghs", 150 | "sort": "best match", 151 | "order": "desc", 152 | "page": "1", 153 | "per_page": "100", 154 | }) 155 | w.Header().Set("Content-Type", "text/plain") 156 | w.WriteHeader(http.StatusNotFound) 157 | } 158 | ret, err := firstRequestTest(t, handler) 159 | assert(strings.Contains(err.Error(), "404"), true) 160 | assert(ret, &requestTestReulst{0, 0}) 161 | 162 | // Normal response 163 | handler = func(w http.ResponseWriter, r *http.Request) { 164 | testMethod(t, r, "GET") 165 | testFormValues(t, r, values{ 166 | "q": "ghs", 167 | "sort": "best match", 168 | "order": "desc", 169 | "page": "2", 170 | "per_page": "100", 171 | }) 172 | fmt.Fprint(w, `{"total_count": 102, "items": [{"id":1},{"id":2}]}`) 173 | } 174 | repoNum, _ := secondRequestTest(t, handler) 175 | assert(repoNum, 2) 176 | // Invalid response 177 | handler = func(w http.ResponseWriter, r *http.Request) { 178 | testMethod(t, r, "GET") 179 | testFormValues(t, r, values{ 180 | "q": "ghs", 181 | "sort": "best match", 182 | "order": "desc", 183 | "page": "2", 184 | "per_page": "100", 185 | }) 186 | w.Header().Set("Content-Type", "text/plain") 187 | w.WriteHeader(http.StatusNotFound) 188 | } 189 | repoNum, err = secondRequestTest(t, handler) 190 | assert(strings.Contains(err.Error(), "404"), true) 191 | assert(repoNum, 0) 192 | } 193 | 194 | func firstRequestTest(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (*requestTestReulst, error) { 195 | Setup() 196 | defer Teardown() 197 | c := context.Background() 198 | 199 | // create input 200 | max := 1000 201 | perPage := 100 202 | url, _ := url.Parse(server.URL) 203 | repo = NewRepo(NewSearch(c, option(max, perPage, url, ""))) 204 | 205 | // create output 206 | mux.HandleFunc("/search/repositories", handler) 207 | 208 | // test 209 | repos, _, printMax, err := repo.search.First(c) 210 | 211 | return &requestTestReulst{ 212 | repolen: len(repos), 213 | printMax: printMax, 214 | }, err 215 | } 216 | 217 | func secondRequestTest(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (int, error) { 218 | Setup() 219 | defer Teardown() 220 | c := context.Background() 221 | 222 | // create input 223 | max := 1000 224 | perPage := 100 225 | url, _ := url.Parse(server.URL) 226 | repo = NewRepo(NewSearch(c, option(max, perPage, url, "abcdefg"))) 227 | 228 | // create output 229 | mux.HandleFunc("/search/repositories", handler) 230 | 231 | // test 232 | repos, err := repo.search.Exec(c, 2) 233 | 234 | return len(repos), err 235 | } 236 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/tcnksm/go-latest" 8 | ) 9 | 10 | func CheckVersion(ver string) { 11 | if os.Getenv("GHS_PRINT") != "no" { 12 | githubTag := &latest.GithubTag{ 13 | Owner: "sona-tar", 14 | Repository: "ghs", 15 | } 16 | res, _ := latest.Check(githubTag, ver) 17 | if res.Outdated { 18 | fmt.Printf(fmt.Sprintf("%s is not latest, you should upgrade to %s\n", ver, res.Current)) 19 | fmt.Printf("-> $ brew update && brew upgrade sona-tar/tools/ghs\n") 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------