├── .gitignore ├── CHANGELOG.md ├── Makefile ├── README.md ├── db.go ├── go.mod ├── go.sum ├── icon.png ├── info.plist ├── login.go ├── main.go ├── oauth.go └── repos.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | alfred-github-jump 3 | sqlite.db 4 | *.alfredworkflow 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [v3.0.0](https://github.com/lox/alfred-github-jump/tree/v3.0.0) (2019-10-24) 8 | [Full Changelog](https://github.com/lox/alfred-github-jump/compare/v1.2.0...v3.0.0) 9 | 10 | ### Changed 11 | - Move to go modules [#14](https://github.com/lox/alfred-github-jump/pull/14) (@lox) 12 | - Add fuzzy search [#13](https://github.com/lox/alfred-github-jump/pull/13) (@lox) 13 | 14 | ## [v1.2.0](https://github.com/lox/alfred-github-jump/tree/v1.2.0) (2018-08-19) 15 | [Full Changelog](https://github.com/lox/alfred-github-jump/compare/v1.1.0...v1.2.0) 16 | 17 | ### Changed 18 | - Add vendor dir and update to latest github lib [#12](https://github.com/lox/alfred-github-jump/pull/12) (@lox) 19 | 20 | ## [v1.1.0](https://github.com/lox/alfred-github-jump/tree/v1.1.0) (2016-09-05) 21 | [Full Changelog](https://github.com/lox/alfred-github-jump/compare/fa294c69b7df...v1.1.0) 22 | 23 | ### Changed 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES := $(wildcard *.go) 2 | BIN := alfred-github-jump 3 | FILES := $(BIN) info.plist icon.png 4 | 5 | build: alfred-github-jump 6 | 7 | package: Github\ Jump.alfredworkflow 8 | 9 | Github\ Jump.alfredworkflow: $(FILES) 10 | zip -j "$@" $^ 11 | 12 | alfred-github-jump: $(SOURCES) 13 | CGO_ENABLED=1 go build -ldflags="-s -w" -o alfred-github-jump $(SOURCES) 14 | 15 | clean: 16 | -rm $(BIN) Github\ Jump.alfredworkflow 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alfred Github Jump 2 | 3 | A [workflow for Alfred 3](https://www.alfredapp.com/help/workflows/) for indexing your github repositories, allowing you to quickly filter them and open them in your default browser 4 | 5 | ![](http://lachlan.me/s/wvjHZ.png) 6 | 7 | 8 | ## Development 9 | 10 | ```bash 11 | # Make sure the workflow dir exists 12 | mkdir -p "$HOME/Library/Application Support/Alfred 3/Alfred.alfredpreferences/workflows" 13 | 14 | # Then checkout the project 15 | go get -u github.com/lox/alfred-github-jump 16 | cd "$GOPATH/src/github.com/lox/alfred-github-jump" 17 | 18 | # Build it and link it into Alfred 19 | make build 20 | ln -s "$PWD" "$HOME/Library/Application Support/Alfred 3/Alfred.alfredpreferences/workflows/alfred-github-jump" 21 | ``` 22 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | _ "github.com/mattn/go-sqlite3" 7 | ) 8 | 9 | var respositorySql = ` 10 | CREATE TABLE IF NOT EXISTS repository ( 11 | id varchar(255) PRIMARY KEY, 12 | url varchar(255) NOT NULL, 13 | user varchar(255) NOT NULL, 14 | name varchar(255) NOT NULL, 15 | description text, 16 | pushed_at timestamp, 17 | created_at timestamp, 18 | updated_at timestamp 19 | )` 20 | 21 | func OpenDB() (*sql.DB, error) { 22 | db, err := sql.Open("sqlite3", "./sqlite.db") 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | if _, err = db.Exec(respositorySql); err != nil { 28 | return nil, err 29 | } 30 | 31 | return db, nil 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lox/alfred-github-jump 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 8 | github.com/golang/protobuf v1.2.0 // indirect 9 | github.com/google/go-github v17.0.1-0.20180816111355-fc33ffe77374+incompatible 10 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect 11 | github.com/mattn/go-sqlite3 v1.9.1-0.20180719091609-b3511bfdd742 12 | github.com/pascalw/go-alfred v0.0.0-20160913054623-16aeb807166c 13 | github.com/sahilm/fuzzy v0.1.0 14 | github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c 15 | github.com/stretchr/testify v1.4.0 // indirect 16 | golang.org/x/net v0.0.0-20180816102801-aaf60122140d // indirect 17 | golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc 18 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 19 | google.golang.org/appengine v1.1.1-0.20180731164958-4216e58b9158 // indirect 20 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 9 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/google/go-github v17.0.1-0.20180816111355-fc33ffe77374+incompatible h1:P9d2W7ZRvBQt4F8PpjkbI2I18UB83LiiHlWC5kAk5Ao= 11 | github.com/google/go-github v17.0.1-0.20180816111355-fc33ffe77374+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 12 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= 13 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 14 | github.com/mattn/go-sqlite3 v1.9.1-0.20180719091609-b3511bfdd742 h1:CxNxKbYu7Gc9ATyoSyjSLzf9wEryOLLhU5YCOR6x6MU= 15 | github.com/mattn/go-sqlite3 v1.9.1-0.20180719091609-b3511bfdd742/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 16 | github.com/pascalw/go-alfred v0.0.0-20160913054623-16aeb807166c h1:j6+n6AEmqTs6XyfTKVaq4Bzc4vCnjwF0BbchwR7UP+U= 17 | github.com/pascalw/go-alfred v0.0.0-20160913054623-16aeb807166c/go.mod h1:O821yF1VSi86tviHFEO4FN4TUjVt46UUqT6XGEaMYhk= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= 21 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 22 | github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c h1:fyKiXKO1/I/B6Y2U8T7WdQGWzwehOuGIrljPtt7YTTI= 23 | github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 25 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 26 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 27 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 28 | golang.org/x/net v0.0.0-20180816102801-aaf60122140d h1:211XH5RPVP5tOBkz6xm3/b7KxtjqVf6PYG+evqJpE08= 29 | golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 30 | golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc h1:3ElrZeO6IBP+M8kgu5YFwRo92Gqr+zBg3aooYQ6ziqU= 31 | golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 32 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 33 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | google.golang.org/appengine v1.1.1-0.20180731164958-4216e58b9158 h1:DLu24D8QphjtZaO7ZrMpJgxUV5pldWTLxEiMAYUZd1U= 36 | google.golang.org/appengine v1.1.1-0.20180731164958-4216e58b9158/go.mod h1:becAO19CitOrUq7nfYuyxycOBJNZgjTqYPI+mvwLjCs= 37 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 38 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 42 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lox/alfred-github-jump/1848f06e0a84ff8742d3b36be55d3047bdfacaee/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | me.lachlan.githubjump 7 | category 8 | Tools 9 | connections 10 | 11 | 0A14AB01-D5B4-4AA4-9894-48EE61682A69 12 | 13 | 550D97C0-A2B2-4A54-A472-F2AA760D121C 14 | 15 | 16 | destinationuid 17 | CF16946F-5549-4D50-A0FB-EFEE63A28F89 18 | modifiers 19 | 0 20 | modifiersubtext 21 | 22 | 23 | 24 | 84DAF9C0-AAF6-41D1-BE08-086FB0825C01 25 | 26 | 27 | destinationuid 28 | E6592B53-3D1B-4A93-A934-2F5EF9E857D1 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | 34 | 35 | CF16946F-5549-4D50-A0FB-EFEE63A28F89 36 | 37 | 38 | destinationuid 39 | 6A3A4092-8D62-4F7D-9684-8FDF7BCDA6B3 40 | modifiers 41 | 0 42 | modifiersubtext 43 | 44 | 45 | 46 | E6592B53-3D1B-4A93-A934-2F5EF9E857D1 47 | 48 | 49 | destinationuid 50 | 6A3A4092-8D62-4F7D-9684-8FDF7BCDA6B3 51 | modifiers 52 | 0 53 | modifiersubtext 54 | 55 | 56 | 57 | EC294EDC-565E-48AE-BF28-19E2B1C649C0 58 | 59 | 60 | destinationuid 61 | 0A14AB01-D5B4-4AA4-9894-48EE61682A69 62 | modifiers 63 | 0 64 | modifiersubtext 65 | 66 | 67 | 68 | 69 | createdby 70 | Lachlan Donald 71 | description 72 | Search / Open Recent Github Repos 73 | disabled 74 | 75 | name 76 | Github Jump 77 | objects 78 | 79 | 80 | config 81 | 82 | plusspaces 83 | 84 | url 85 | {query} 86 | utf8 87 | 88 | 89 | type 90 | alfred.workflow.action.openurl 91 | uid 92 | 0A14AB01-D5B4-4AA4-9894-48EE61682A69 93 | version 94 | 0 95 | 96 | 97 | config 98 | 99 | argumenttype 100 | 1 101 | escaping 102 | 102 103 | keyword 104 | gh 105 | queuedelaycustom 106 | 3 107 | queuedelayimmediatelyinitially 108 | 109 | queuedelaymode 110 | 0 111 | queuemode 112 | 1 113 | runningsubtext 114 | Loading github repositories 115 | script 116 | ./alfred-github-jump repos "{query}" 117 | title 118 | Jump to a Github Repository 119 | type 120 | 0 121 | withspace 122 | 123 | 124 | type 125 | alfred.workflow.input.scriptfilter 126 | uid 127 | EC294EDC-565E-48AE-BF28-19E2B1C649C0 128 | version 129 | 0 130 | 131 | 132 | config 133 | 134 | lastpathcomponent 135 | 136 | onlyshowifquerypopulated 137 | 138 | output 139 | 0 140 | removeextension 141 | 142 | sticky 143 | 144 | text 145 | {query} 146 | title 147 | Github Jump 148 | 149 | type 150 | alfred.workflow.output.notification 151 | uid 152 | 6A3A4092-8D62-4F7D-9684-8FDF7BCDA6B3 153 | version 154 | 0 155 | 156 | 157 | config 158 | 159 | concurrently 160 | 161 | escaping 162 | 102 163 | script 164 | ./alfred-github-jump update 165 | type 166 | 0 167 | 168 | type 169 | alfred.workflow.action.script 170 | uid 171 | CF16946F-5549-4D50-A0FB-EFEE63A28F89 172 | version 173 | 0 174 | 175 | 176 | config 177 | 178 | argumenttype 179 | 2 180 | keyword 181 | gh > update 182 | subtext 183 | Github Jump 184 | text 185 | Update Github Repositories 186 | withspace 187 | 188 | 189 | type 190 | alfred.workflow.input.keyword 191 | uid 192 | 550D97C0-A2B2-4A54-A472-F2AA760D121C 193 | version 194 | 0 195 | 196 | 197 | config 198 | 199 | argumenttype 200 | 2 201 | keyword 202 | gh > login 203 | subtext 204 | Github Jump 205 | text 206 | Login to Github 207 | withspace 208 | 209 | 210 | type 211 | alfred.workflow.input.keyword 212 | uid 213 | 84DAF9C0-AAF6-41D1-BE08-086FB0825C01 214 | version 215 | 0 216 | 217 | 218 | config 219 | 220 | concurrently 221 | 222 | escaping 223 | 102 224 | script 225 | ./alfred-github-jump login 226 | type 227 | 0 228 | 229 | type 230 | alfred.workflow.action.script 231 | uid 232 | E6592B53-3D1B-4A93-A934-2F5EF9E857D1 233 | version 234 | 0 235 | 236 | 237 | readme 238 | 239 | uidata 240 | 241 | 0A14AB01-D5B4-4AA4-9894-48EE61682A69 242 | 243 | ypos 244 | 180 245 | 246 | 550D97C0-A2B2-4A54-A472-F2AA760D121C 247 | 248 | ypos 249 | 300 250 | 251 | 6A3A4092-8D62-4F7D-9684-8FDF7BCDA6B3 252 | 253 | ypos 254 | 180 255 | 256 | 84DAF9C0-AAF6-41D1-BE08-086FB0825C01 257 | 258 | ypos 259 | 420 260 | 261 | CF16946F-5549-4D50-A0FB-EFEE63A28F89 262 | 263 | ypos 264 | 300 265 | 266 | E6592B53-3D1B-4A93-A934-2F5EF9E857D1 267 | 268 | ypos 269 | 420 270 | 271 | EC294EDC-565E-48AE-BF28-19E2B1C649C0 272 | 273 | ypos 274 | 180 275 | 276 | 277 | webaddress 278 | https://github.com/lox 279 | 280 | 281 | -------------------------------------------------------------------------------- /login.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/google/go-github/github" 12 | "github.com/skratchdot/open-golang/open" 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | var ( 17 | ServerAddress = "http://127.0.0.1:7024" 18 | ServerBind = ":7024" 19 | loginComplete sync.Mutex 20 | ) 21 | 22 | func handleMain(w http.ResponseWriter, r *http.Request) { 23 | http.Redirect(w, r, "/login", http.StatusMovedPermanently) 24 | } 25 | 26 | func handleGitHubLogin(w http.ResponseWriter, r *http.Request) { 27 | url := OAuthConf.AuthCodeURL(OAuthStateString, oauth2.AccessTypeOnline) 28 | http.Redirect(w, r, url, http.StatusTemporaryRedirect) 29 | } 30 | 31 | func handleGitHubCallback(w http.ResponseWriter, r *http.Request) { 32 | state := r.FormValue("state") 33 | if state != OAuthStateString { 34 | fmt.Printf("invalid oauth state, expected '%s', got '%s'\n", OAuthStateString, state) 35 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 36 | return 37 | } 38 | 39 | code := r.FormValue("code") 40 | token, err := OAuthConf.Exchange(oauth2.NoContext, code) 41 | if err != nil { 42 | fmt.Printf("oauthConf.Exchange() failed with '%s'\n", err) 43 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 44 | return 45 | } 46 | 47 | log.Printf("got token %#v", token) 48 | if err := saveToken(token); err != nil { 49 | http.Error(w, err.Error(), http.StatusInternalServerError) 50 | return 51 | } 52 | 53 | ctx := context.Background() 54 | oauthClient := OAuthConf.Client(oauth2.NoContext, token) 55 | client := github.NewClient(oauthClient) 56 | user, _, err := client.Users.Get(ctx, "") 57 | if err != nil { 58 | log.Printf("client.Users.Get() failed with %q", err) 59 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 60 | return 61 | } 62 | 63 | fmt.Printf("Logged in as GitHub user: %s\n", *user.Login) 64 | fmt.Fprintf(w, "Successfully authenticated! You can close this tab.") 65 | 66 | loginComplete.Unlock() 67 | } 68 | 69 | func loginCommand() error { 70 | ln, err := net.Listen("tcp", ServerBind) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | http.HandleFunc("/", handleMain) 76 | http.HandleFunc("/login", handleGitHubLogin) 77 | http.HandleFunc("/github_oauth_cb", handleGitHubCallback) 78 | 79 | log.Printf("Opening %s in default browser\n", ServerAddress) 80 | open.Run(ServerAddress) 81 | 82 | loginComplete.Lock() 83 | go http.Serve(ln, nil) 84 | 85 | loginComplete.Lock() 86 | return ln.Close() 87 | } 88 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/pascalw/go-alfred" 11 | "gopkg.in/alecthomas/kingpin.v2" 12 | ) 13 | 14 | func main() { 15 | var ( 16 | debug = kingpin.Flag("debug", "Show debugging output").Bool() 17 | repos = kingpin.Command("repos", "Show repositories from cache") 18 | reposFilter = repos.Arg("filter", "Fuzzy match the full repository name").String() 19 | login = kingpin.Command("login", "Login to github via oauth") 20 | update = kingpin.Command("update", "Updates repositories from Github") 21 | ) 22 | 23 | cmd := kingpin.Parse() 24 | 25 | if *debug == false { 26 | log.SetOutput(ioutil.Discard) 27 | } else { 28 | log.SetOutput(os.Stderr) 29 | } 30 | 31 | switch cmd { 32 | case repos.FullCommand(): 33 | reposCommand(*reposFilter) 34 | case login.FullCommand(): 35 | if err := loginCommand(); err != nil { 36 | fmt.Println(err) 37 | os.Exit(1) 38 | } 39 | case update.FullCommand(): 40 | updateCommand() 41 | } 42 | } 43 | 44 | func alfredError(err error) *alfred.AlfredResponseItem { 45 | return &alfred.AlfredResponseItem{ 46 | Valid: false, 47 | Uid: "error", 48 | Title: "Error Occurred", 49 | Subtitle: err.Error(), 50 | Icon: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns", 51 | } 52 | } 53 | 54 | func backgroundUpdate() error { 55 | cmd := exec.Command(os.Args[0], "update") 56 | if err := cmd.Start(); err != nil { 57 | return err 58 | } 59 | log.Printf("Background pid %#v", cmd.Process.Pid) 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /oauth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "golang.org/x/oauth2" 8 | githuboauth "golang.org/x/oauth2/github" 9 | ) 10 | 11 | var ( 12 | OAuthConf = &oauth2.Config{ 13 | ClientID: "d103e51684009fc22250", 14 | ClientSecret: "63079590549474872a6b656aed3c4aaecb8a3efc", 15 | Scopes: []string{"user:email", "repo"}, 16 | Endpoint: githuboauth.Endpoint, 17 | } 18 | OAuthStateString = "da39a3ee5e6b4b0d3255bfef95601890afd80709" 19 | ) 20 | 21 | func isLoggedIn() bool { 22 | _, err := os.Stat("token.json") 23 | return !os.IsNotExist(err) 24 | } 25 | 26 | func loadToken() (*oauth2.Token, error) { 27 | tokenFile, err := os.Open("token.json") 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | token := &oauth2.Token{} 33 | defer tokenFile.Close() 34 | 35 | err = json.NewDecoder(tokenFile).Decode(token) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return token, err 41 | } 42 | 43 | func saveToken(token *oauth2.Token) error { 44 | tokenFile, err := os.Create("token.json") 45 | if err != nil { 46 | return err 47 | } 48 | 49 | defer tokenFile.Close() 50 | return json.NewEncoder(tokenFile).Encode(token) 51 | } 52 | -------------------------------------------------------------------------------- /repos.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/google/go-github/github" 11 | "github.com/pascalw/go-alfred" 12 | "github.com/sahilm/fuzzy" 13 | 14 | "golang.org/x/oauth2" 15 | ) 16 | 17 | type Repository struct { 18 | URL, Name, User, Description string 19 | LastUpdated time.Time 20 | } 21 | 22 | func (r Repository) FullName() string { 23 | return fmt.Sprintf("%s/%s", r.User, r.Name) 24 | } 25 | 26 | func reposCommand(fuzzyFilter string) { 27 | response := alfred.NewResponse() 28 | defer response.Print() 29 | 30 | if !isLoggedIn() { 31 | response.AddItem(&alfred.AlfredResponseItem{ 32 | Valid: false, 33 | Uid: "login", 34 | Title: "You need to login first with gh-login", 35 | }) 36 | return 37 | } 38 | 39 | repos, err := ListRepositories() 40 | if err != nil { 41 | response.AddItem(alfredError(err)) 42 | return 43 | } 44 | 45 | var names []string 46 | 47 | for _, repo := range repos { 48 | names = append(names, repo.FullName()) 49 | } 50 | 51 | for _, match := range fuzzy.Find(fuzzyFilter, names) { 52 | response.AddItem(&alfred.AlfredResponseItem{ 53 | Valid: true, 54 | Uid: repos[match.Index].URL, 55 | Title: repos[match.Index].FullName(), 56 | Subtitle: repos[match.Index].Description, 57 | Arg: repos[match.Index].URL, 58 | }) 59 | } 60 | } 61 | 62 | func ListRepositories() ([]Repository, error) { 63 | db, err := OpenDB() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | rows, err := db.Query("SELECT id, url,description, name,user,updated_at FROM repository") 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | repos := []Repository{} 74 | 75 | for rows.Next() { 76 | var id, url, descr, name, user string 77 | var updated time.Time 78 | err = rows.Scan(&id, &url, &descr, &name, &user, &updated) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | repos = append(repos, Repository{ 84 | URL: url, 85 | Name: name, 86 | User: user, 87 | Description: descr, 88 | LastUpdated: updated, 89 | }) 90 | } 91 | 92 | return repos, nil 93 | } 94 | 95 | func nilableString(s *string) string { 96 | if s == nil { 97 | return "" 98 | } 99 | return *s 100 | } 101 | 102 | func githubTime(t *github.Timestamp) *time.Time { 103 | if t == nil { 104 | return nil 105 | } 106 | return &t.Time 107 | } 108 | 109 | func listUserRepositories(client *github.Client) ([]*github.Repository, error) { 110 | opt := &github.RepositoryListOptions{ 111 | ListOptions: github.ListOptions{PerPage: 45}, 112 | Sort: "pushed", 113 | } 114 | 115 | repos := []*github.Repository{} 116 | 117 | for { 118 | ctx := context.Background() 119 | result, resp, err := client.Repositories.List(ctx, "", opt) 120 | if err != nil { 121 | return repos, err 122 | } 123 | repos = append(repos, result...) 124 | if resp.NextPage == 0 { 125 | break 126 | } 127 | opt.ListOptions.Page = resp.NextPage 128 | } 129 | 130 | return repos, nil 131 | } 132 | 133 | func listStarredRepositories(client *github.Client) ([]*github.Repository, error) { 134 | opt := &github.ActivityListStarredOptions{ 135 | ListOptions: github.ListOptions{PerPage: 45}, 136 | Sort: "pushed", 137 | } 138 | 139 | repos := []*github.Repository{} 140 | 141 | for { 142 | ctx := context.Background() 143 | result, resp, err := client.Activity.ListStarred(ctx, "", opt) 144 | if err != nil { 145 | return repos, err 146 | } 147 | for _, starred := range result { 148 | repos = append(repos, starred.Repository) 149 | } 150 | if resp.NextPage == 0 { 151 | break 152 | } 153 | opt.ListOptions.Page = resp.NextPage 154 | } 155 | 156 | return repos, nil 157 | } 158 | 159 | func UpdateRepositories(token *oauth2.Token) (int64, error) { 160 | tc := OAuthConf.Client(oauth2.NoContext, token) 161 | client := github.NewClient(tc) 162 | 163 | userRepos, err := listUserRepositories(client) 164 | if err != nil { 165 | return 0, err 166 | } 167 | 168 | starredRepos, err := listStarredRepositories(client) 169 | if err != nil { 170 | return 0, err 171 | } 172 | 173 | db, err := OpenDB() 174 | if err != nil { 175 | return 0, err 176 | } 177 | 178 | tx, err := db.Begin() 179 | if err != nil { 180 | return 0, err 181 | } 182 | 183 | found := map[string]struct{}{} 184 | counter := int64(0) 185 | 186 | for _, repo := range append(userRepos, starredRepos...) { 187 | log.Printf("Updating %s/%s", *repo.Owner.Login, *repo.Name) 188 | 189 | name := fmt.Sprintf("%s/%s", *repo.Owner.Login, *repo.Name) 190 | res, err := db.Exec( 191 | `INSERT OR REPLACE INTO repository ( 192 | id, 193 | url, 194 | description, 195 | name, user, 196 | pushed_at, 197 | updated_at, 198 | created_at 199 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 200 | name, 201 | nilableString(repo.HTMLURL), 202 | nilableString(repo.Description), 203 | *repo.Name, 204 | *repo.Owner.Login, 205 | githubTime(repo.PushedAt), 206 | githubTime(repo.UpdatedAt), 207 | githubTime(repo.CreatedAt), 208 | ) 209 | if err != nil { 210 | return counter, err 211 | } 212 | found[name] = struct{}{} 213 | rows, _ := res.RowsAffected() 214 | counter += rows 215 | } 216 | 217 | existing, err := ListRepositories() 218 | if err != nil { 219 | return 0, err 220 | } 221 | 222 | // purge repos that don't exit any more 223 | for _, repo := range existing { 224 | if _, exists := found[repo.FullName()]; !exists { 225 | log.Printf("Repo %s doesn't exist, deleting", repo.FullName()) 226 | 227 | _, err := db.Exec( 228 | `DELETE FROM repository WHERE id=?`, 229 | repo.FullName(), 230 | ) 231 | if err != nil { 232 | return 0, err 233 | } 234 | 235 | } 236 | } 237 | 238 | return counter, tx.Commit() 239 | } 240 | 241 | func updateCommand() { 242 | token, err := loadToken() 243 | if err != nil { 244 | fmt.Println("Error", err) 245 | os.Exit(1) 246 | } 247 | 248 | n, err := UpdateRepositories(token) 249 | if err != nil { 250 | fmt.Println("Error", err) 251 | os.Exit(1) 252 | } 253 | 254 | fmt.Printf("Updated %d repositories from github", n) 255 | } 256 | --------------------------------------------------------------------------------