├── .gitignore ├── README.mkd ├── LICENSE └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | getgithubrepos 2 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | # getgithubrepos 2 | 3 | A tool to list the SSH clone URLs for the given GitHub user. 4 | 5 | It doesn't use auth so beware of [rate limits](https://developer.github.com/v3/#rate-limiting). 6 | 7 | ## Install 8 | 9 | ▶ go get github.com/tomnomnom/getgithubrepos 10 | 11 | ## Usage 12 | 13 | ▶ getgithubrepos tomnomnom 14 | git@github.com:TomNomNom/All-About-SPL.git 15 | git@github.com:TomNomNom/api.tomnomnom.com.git 16 | git@github.com:TomNomNom/ASCIIPoint.git 17 | git@github.com:TomNomNom/branchdemo.git 18 | ... 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Tom Hudson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 4 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 | 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 12 | IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/tomnomnom/linkheader" 13 | ) 14 | 15 | // repoError is a special error type that includes an exit code 16 | type repoError struct { 17 | error 18 | code int 19 | } 20 | 21 | // Error values 22 | var errNoUsername = &repoError{errors.New("Usage: getgithubrepos "), 1} 23 | var errBadUsername = &repoError{errors.New("No such username"), 2} 24 | var errRateLimitExceeded = &repoError{errors.New("Rate limit exceeded"), 3} 25 | var errHTTPFail = &repoError{errors.New("HTTP Error"), 4} 26 | var errJSONDecode = &repoError{errors.New("Failed to decode JSON response"), 5} 27 | 28 | // handleError takes appropriate action for the provided error 29 | func handleError(err *repoError) { 30 | if err == nil { 31 | return 32 | } 33 | fmt.Println(err.Error()) 34 | os.Exit(err.code) 35 | } 36 | 37 | func main() { 38 | flag.Parse() 39 | 40 | user := flag.Arg(0) 41 | if user == "" { 42 | handleError(errNoUsername) 43 | } 44 | 45 | url := fmt.Sprintf("https://api.github.com/users/%s/repos", user) 46 | 47 | r := struct { 48 | repos []repo 49 | url string 50 | }{ 51 | repos: make([]repo, 0), 52 | url: url, 53 | } 54 | 55 | for r.url != "" { 56 | fetched, nextURL, err := getRepos(r.url) 57 | handleError(err) 58 | r.repos = append(r.repos, fetched...) 59 | r.url = nextURL 60 | } 61 | 62 | for _, i := range r.repos { 63 | fmt.Printf("%s\n", i.SSHUrl) 64 | } 65 | } 66 | 67 | // repo is a struct to unmarshal the JSON response in to 68 | type repo struct { 69 | ID int `json:"id"` 70 | Name string `json:"name"` 71 | SSHUrl string `json:"ssh_url"` 72 | } 73 | 74 | // getRepos gets the repositories from a GitHub API URL 75 | // e.g. https://api.github.com/users/tomnomnom/repos 76 | // also returns the URL for the next page of results (if any) 77 | // and any error that occurred 78 | func getRepos(url string) ([]repo, string, *repoError) { 79 | 80 | var repos []repo 81 | 82 | resp, err := http.Get(url) 83 | if err != nil { 84 | return repos, "", errHTTPFail 85 | } 86 | defer resp.Body.Close() 87 | 88 | // Check for 'expected' errors 89 | switch resp.StatusCode { 90 | case http.StatusNotFound: 91 | return repos, "", errBadUsername 92 | case http.StatusForbidden: 93 | return repos, "", errRateLimitExceeded 94 | } 95 | 96 | body, err := ioutil.ReadAll(resp.Body) 97 | err = json.Unmarshal(body, &repos) 98 | if err != nil { 99 | return repos, "", errJSONDecode 100 | } 101 | 102 | // Check for a link to the next page 103 | next := "" 104 | if links, exists := resp.Header["Link"]; exists { 105 | next = getNext(links) 106 | } 107 | 108 | return repos, next, nil 109 | } 110 | 111 | // getNext looks for a rel="next" Link and returns it if it exists 112 | func getNext(headers []string) string { 113 | links := linkheader.ParseMultiple(headers).FilterByRel("next") 114 | if len(links) > 0 { 115 | return links[0].URL 116 | } 117 | return "" 118 | } 119 | --------------------------------------------------------------------------------