├── .gitignore ├── README.md ├── handler.go ├── timestamp.go ├── types.go ├── types_test.go ├── validate.go ├── validate_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-github-webhook 2 | 3 | A GitHub webhook handler written in Go. It simplifies payload validation, etc ... 4 | 5 | ## Sample app 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "net/http" 13 | 14 | "github.com/GitbookIO/go-github-webhook" 15 | ) 16 | 17 | func main() { 18 | // Your GitHub secret (this could also be dynamic), if you're secret is empty it will skip validation 19 | secret := "" 20 | 21 | if err := http.ListenAndServe(":8000", WebhookLog(secret)); err != nil { 22 | fmt.Errorf("Error: %s", err) 23 | } 24 | } 25 | 26 | func WebhookLog(secret string) http.Handler { 27 | return github.Handler(secret, func(event string, payload *github.GitHubPayload, req *http.Request) error { 28 | // Log webhook 29 | fmt.Println("Received", event, "for ", payload.Repository.Name) 30 | 31 | // You'll probably want to do some real processing 32 | fmt.Println("Can clone repo at:", payload.Repository.CloneURL) 33 | 34 | // All is good (return an error to fail) 35 | return nil 36 | }) 37 | } 38 | ``` -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type WebhookHandler func(eventname string, payload *GitHubPayload, req *http.Request) error 12 | 13 | func Handler(secret string, fn WebhookHandler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 15 | event := req.Header.Get("x-github-event") 16 | delivery := req.Header.Get("x-github-delivery") 17 | signature := req.Header.Get("x-hub-signature") 18 | 19 | // Utility funcs 20 | _fail := func(err error) { 21 | fail(w, event, err) 22 | } 23 | _succeed := func() { 24 | succeed(w, event) 25 | } 26 | 27 | // Ensure headers are all there 28 | if event == "" || delivery == "" { 29 | _fail(fmt.Errorf("Missing x-github-* and x-hub-* headers")) 30 | return 31 | } 32 | 33 | // No secret provided to github 34 | if signature == "" && secret != "" { 35 | _fail(fmt.Errorf("GitHub isn't providing a signature, whilst a secret is being used (please give github's webhook the secret)")) 36 | return 37 | } 38 | 39 | // Read body 40 | body, err := ioutil.ReadAll(req.Body) 41 | if err != nil { 42 | _fail(err) 43 | return 44 | } 45 | 46 | // Validate payload (only when secret is provided) 47 | if secret != "" { 48 | if err := validePayloadSignature(secret, signature, body); err != nil { 49 | // Valied validation 50 | _fail(err) 51 | return 52 | } 53 | } 54 | 55 | // Get payload 56 | payload := GitHubPayload{} 57 | if err := json.Unmarshal(body, &payload); err != nil { 58 | _fail(fmt.Errorf("Could not deserialize payload")) 59 | return 60 | } 61 | 62 | // Do something with payload 63 | if err := fn(event, &payload, req); err == nil { 64 | _succeed() 65 | } else { 66 | _fail(err) 67 | } 68 | }) 69 | } 70 | 71 | func validePayloadSignature(secret, signatureHeader string, body []byte) error { 72 | // Check header is valid 73 | signature_parts := strings.SplitN(signatureHeader, "=", 2) 74 | if len(signature_parts) != 2 { 75 | return fmt.Errorf("Invalid signature header: '%s' does not contain two parts (hash type and hash)", signatureHeader) 76 | } 77 | 78 | // Ensure secret is a sha1 hash 79 | signature_type := signature_parts[0] 80 | signature_hash := signature_parts[1] 81 | if signature_type != "sha1" { 82 | return fmt.Errorf("Signature should be a 'sha1' hash not '%s'", signature_type) 83 | } 84 | 85 | // Check that payload came from github 86 | // skip check if empty secret provided 87 | if !IsValidPayload(secret, signature_hash, body) { 88 | return fmt.Errorf("Payload did not come from GitHub") 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func succeed(w http.ResponseWriter, event string) { 95 | render(w, PayloadPong{ 96 | Ok: true, 97 | Event: event, 98 | }) 99 | } 100 | 101 | func fail(w http.ResponseWriter, event string, err error) { 102 | w.WriteHeader(500) 103 | render(w, PayloadPong{ 104 | Ok: false, 105 | Event: event, 106 | Error: err.Error(), 107 | }) 108 | } 109 | 110 | func render(w http.ResponseWriter, v interface{}) { 111 | data, err := json.Marshal(v) 112 | if err != nil { 113 | http.Error(w, err.Error(), 500) 114 | return 115 | } 116 | w.Write(data) 117 | } 118 | -------------------------------------------------------------------------------- /timestamp.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type GitHubTimestamp struct { 9 | *time.Time 10 | } 11 | 12 | func (t *GitHubTimestamp) MarshalJSON() ([]byte, error) { 13 | return t.Time.MarshalJSON() 14 | } 15 | 16 | func (t *GitHubTimestamp) UnmarshalJSON(data []byte) error { 17 | // Try deserializing as timestamp then time 18 | var x int64 = 0 19 | newt := time.Time{} 20 | if err := json.Unmarshal(data, &x); err != nil { 21 | if err := json.Unmarshal(data, &newt); err != nil { 22 | return err 23 | } 24 | } else { 25 | newt = time.Unix(x, 0) 26 | } 27 | 28 | // From timestamp 29 | t.Time = &newt 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | type GitCommit struct { 4 | Author string `json:"author"` 5 | Email string `json:"email"` 6 | Repo string `json:"repo"` 7 | RepoOwner string `json:"repo_owner"` 8 | Message string `json:"message"` 9 | Date string `json:"date"` 10 | Hash string `json:"hash"` 11 | } 12 | 13 | type GitHubCommit struct { 14 | Distinct bool `json:"distinct"` 15 | URL string `json:"url"` 16 | Id string `json:"id"` 17 | Timestamp GitHubTimestamp `json:"timestamp"` 18 | Added []string `json:"added"` 19 | Message string `json:"message"` 20 | Committer GitHubPerson `json:"committer"` 21 | Author GitHubPerson `json:"author"` 22 | Modified []string `json:"modified"` 23 | Removed []string `json:"removed"` 24 | } 25 | 26 | type GitHubRepo struct { 27 | HasIssues bool `json:"has_issues"` 28 | HasWiki bool `json:"has_wiki"` 29 | Size int `json:"size"` 30 | Description string `json:"description"` 31 | Owner GitHubPerson `json:"owner"` 32 | Homepage string `json:"homepage"` 33 | Watchers int `json:"watchers"` 34 | Language string `json:"language"` 35 | PushedAt GitHubTimestamp `json:"pushed_at"` 36 | Name string `json:"name"` 37 | FullName string `json:"full_name"` 38 | Organization string `json:"organization"` 39 | HasDownloads bool `json:"has_downloads"` 40 | CreatedAt GitHubTimestamp `json:"created_at"` 41 | URL string `json:"url"` 42 | CloneURL string `json:"clone_url"` 43 | OpenIssues int `json:"open_issues"` 44 | Forks int `json:"forks"` 45 | Private bool `json:"private"` 46 | Fork bool `json:"fork"` 47 | Stargazers int `json:"stargazers"` 48 | DefaultBranch string `json:"default_branch"` 49 | } 50 | 51 | type GitHubPerson struct { 52 | Email *string `json:"email"` 53 | Name *string `json:"name"` 54 | Username *string `json:"username"` 55 | } 56 | 57 | type GitHubPayload struct { 58 | Before string `json:"before"` 59 | Created bool `json:"created"` 60 | Ref string `json:"ref"` 61 | Deleted bool `json:"deleted"` 62 | After string `json:"after"` 63 | HeadCommit GitHubCommit `json:"head_commit"` 64 | Commits []GitHubCommit `json:"commits"` 65 | Repository GitHubRepo `json:"repository"` 66 | Forced bool `json:"forced"` 67 | Compare string `json:"compare"` 68 | Pusher GitHubPerson `json:"pusher"` 69 | } 70 | 71 | type PayloadPong struct { 72 | Ok bool `json:"ok"` 73 | Event string `json:"event"` 74 | Error string `json:"error,omitempty"` 75 | } 76 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestDeserialize(t *testing.T) { 9 | payload := _unmarshal(t) 10 | if payload.Repository.CreatedAt.String() != "2014-05-20 11:51:35 +0200 CEST" { 11 | t.Error("Timestamp is not parsed as expected: %s", payload.Repository.CreatedAt.String()) 12 | } 13 | 14 | if payload.HeadCommit.Timestamp.String() != "2014-11-23 02:19:56 +0100 CET" { 15 | t.Errorf("Time is not parsed as expected: %s", payload.HeadCommit.Timestamp.String()) 16 | } 17 | 18 | if payload.Repository.DefaultBranch != "master" { 19 | t.Errorf("DefaultBranch is not parsed as expected: %s", payload.Repository.DefaultBranch) 20 | } 21 | 22 | if payload.Repository.FullName != "GitbookIO/documentation" { 23 | t.Errorf("FullName is not parsed as expected: %s", payload.Repository.DefaultBranch) 24 | } 25 | } 26 | 27 | func _unmarshal(t *testing.T) GitHubPayload { 28 | payload := GitHubPayload{} 29 | if err := json.Unmarshal([]byte(jsonPayload), &payload); err != nil { 30 | t.Error(err) 31 | } 32 | return payload 33 | } 34 | 35 | const jsonPayload string = ` 36 | { 37 | "ref": "refs/heads/master", 38 | "before": "421eadcf4c46b860e7b62b899e15c61233a3351d", 39 | "after": "14a7480c37bff138ae2629c38d2e1c7e9f1324f9", 40 | "created": false, 41 | "deleted": false, 42 | "forced": false, 43 | "base_ref": null, 44 | "compare": "https://github.com/GitbookIO/documentation/compare/421eadcf4c46...14a7480c37bf", 45 | "commits": [ 46 | { 47 | "id": "14a7480c37bff138ae2629c38d2e1c7e9f1324f9", 48 | "distinct": true, 49 | "message": "Update format.md", 50 | "timestamp": "2014-11-23T02:19:56+01:00", 51 | "url": "https://github.com/GitbookIO/documentation/commit/14a7480c37bff138ae2629c38d2e1c7e9f1324f9", 52 | "author": { 53 | "name": "Aaron O'Mullan", 54 | "email": "aaron.omullan@gmail.com", 55 | "username": "AaronO" 56 | }, 57 | "committer": { 58 | "name": "Aaron O'Mullan", 59 | "email": "aaron.omullan@gmail.com", 60 | "username": "AaronO" 61 | }, 62 | "added": [ 63 | 64 | ], 65 | "removed": [ 66 | 67 | ], 68 | "modified": [ 69 | "book/format.md" 70 | ] 71 | } 72 | ], 73 | "head_commit": { 74 | "id": "14a7480c37bff138ae2629c38d2e1c7e9f1324f9", 75 | "distinct": true, 76 | "message": "Update format.md", 77 | "timestamp": "2014-11-23T02:19:56+01:00", 78 | "url": "https://github.com/GitbookIO/documentation/commit/14a7480c37bff138ae2629c38d2e1c7e9f1324f9", 79 | "author": { 80 | "name": "Aaron O'Mullan", 81 | "email": "aaron.omullan@gmail.com", 82 | "username": "AaronO" 83 | }, 84 | "committer": { 85 | "name": "Aaron O'Mullan", 86 | "email": "aaron.omullan@gmail.com", 87 | "username": "AaronO" 88 | }, 89 | "added": [ 90 | 91 | ], 92 | "removed": [ 93 | 94 | ], 95 | "modified": [ 96 | "book/format.md" 97 | ] 98 | }, 99 | "repository": { 100 | "id": 19975935, 101 | "name": "documentation", 102 | "full_name": "GitbookIO/documentation", 103 | "owner": { 104 | "name": "GitbookIO", 105 | "email": "contact@gitbook.io" 106 | }, 107 | "private": false, 108 | "html_url": "https://github.com/GitbookIO/documentation", 109 | "description": "Documentation for GitBook and gitbook.io", 110 | "fork": false, 111 | "url": "https://github.com/GitbookIO/documentation", 112 | "forks_url": "https://api.github.com/repos/GitbookIO/documentation/forks", 113 | "keys_url": "https://api.github.com/repos/GitbookIO/documentation/keys{/key_id}", 114 | "collaborators_url": "https://api.github.com/repos/GitbookIO/documentation/collaborators{/collaborator}", 115 | "teams_url": "https://api.github.com/repos/GitbookIO/documentation/teams", 116 | "hooks_url": "https://api.github.com/repos/GitbookIO/documentation/hooks", 117 | "issue_events_url": "https://api.github.com/repos/GitbookIO/documentation/issues/events{/number}", 118 | "events_url": "https://api.github.com/repos/GitbookIO/documentation/events", 119 | "assignees_url": "https://api.github.com/repos/GitbookIO/documentation/assignees{/user}", 120 | "branches_url": "https://api.github.com/repos/GitbookIO/documentation/branches{/branch}", 121 | "tags_url": "https://api.github.com/repos/GitbookIO/documentation/tags", 122 | "blobs_url": "https://api.github.com/repos/GitbookIO/documentation/git/blobs{/sha}", 123 | "git_tags_url": "https://api.github.com/repos/GitbookIO/documentation/git/tags{/sha}", 124 | "git_refs_url": "https://api.github.com/repos/GitbookIO/documentation/git/refs{/sha}", 125 | "trees_url": "https://api.github.com/repos/GitbookIO/documentation/git/trees{/sha}", 126 | "statuses_url": "https://api.github.com/repos/GitbookIO/documentation/statuses/{sha}", 127 | "languages_url": "https://api.github.com/repos/GitbookIO/documentation/languages", 128 | "stargazers_url": "https://api.github.com/repos/GitbookIO/documentation/stargazers", 129 | "contributors_url": "https://api.github.com/repos/GitbookIO/documentation/contributors", 130 | "subscribers_url": "https://api.github.com/repos/GitbookIO/documentation/subscribers", 131 | "subscription_url": "https://api.github.com/repos/GitbookIO/documentation/subscription", 132 | "commits_url": "https://api.github.com/repos/GitbookIO/documentation/commits{/sha}", 133 | "git_commits_url": "https://api.github.com/repos/GitbookIO/documentation/git/commits{/sha}", 134 | "comments_url": "https://api.github.com/repos/GitbookIO/documentation/comments{/number}", 135 | "issue_comment_url": "https://api.github.com/repos/GitbookIO/documentation/issues/comments/{number}", 136 | "contents_url": "https://api.github.com/repos/GitbookIO/documentation/contents/{+path}", 137 | "compare_url": "https://api.github.com/repos/GitbookIO/documentation/compare/{base}...{head}", 138 | "merges_url": "https://api.github.com/repos/GitbookIO/documentation/merges", 139 | "archive_url": "https://api.github.com/repos/GitbookIO/documentation/{archive_format}{/ref}", 140 | "downloads_url": "https://api.github.com/repos/GitbookIO/documentation/downloads", 141 | "issues_url": "https://api.github.com/repos/GitbookIO/documentation/issues{/number}", 142 | "pulls_url": "https://api.github.com/repos/GitbookIO/documentation/pulls{/number}", 143 | "milestones_url": "https://api.github.com/repos/GitbookIO/documentation/milestones{/number}", 144 | "notifications_url": "https://api.github.com/repos/GitbookIO/documentation/notifications{?since,all,participating}", 145 | "labels_url": "https://api.github.com/repos/GitbookIO/documentation/labels{/name}", 146 | "releases_url": "https://api.github.com/repos/GitbookIO/documentation/releases{/id}", 147 | "created_at": 1400579495, 148 | "updated_at": "2014-11-22T23:50:39Z", 149 | "pushed_at": 1416705597, 150 | "git_url": "git://github.com/GitbookIO/documentation.git", 151 | "ssh_url": "git@github.com:GitbookIO/documentation.git", 152 | "clone_url": "https://github.com/GitbookIO/documentation.git", 153 | "svn_url": "https://github.com/GitbookIO/documentation", 154 | "homepage": "http://help.gitbook.io/", 155 | "size": 1336, 156 | "stargazers_count": 37, 157 | "watchers_count": 37, 158 | "language": null, 159 | "has_issues": true, 160 | "has_downloads": true, 161 | "has_wiki": false, 162 | "has_pages": false, 163 | "forks_count": 35, 164 | "mirror_url": null, 165 | "open_issues_count": 2, 166 | "forks": 35, 167 | "open_issues": 2, 168 | "watchers": 37, 169 | "default_branch": "master", 170 | "stargazers": 37, 171 | "master_branch": "master", 172 | "organization": "GitbookIO" 173 | }, 174 | "pusher": { 175 | "name": "AaronO", 176 | "email": "aaron.omullan@gmail.com" 177 | }, 178 | "organization": { 179 | "login": "GitbookIO", 180 | "id": 7111340, 181 | "url": "https://api.github.com/orgs/GitbookIO", 182 | "repos_url": "https://api.github.com/orgs/GitbookIO/repos", 183 | "events_url": "https://api.github.com/orgs/GitbookIO/events", 184 | "members_url": "https://api.github.com/orgs/GitbookIO/members{/member}", 185 | "public_members_url": "https://api.github.com/orgs/GitbookIO/public_members{/member}", 186 | "avatar_url": "https://avatars.githubusercontent.com/u/7111340?v=3" 187 | }, 188 | "sender": { 189 | "login": "AaronO", 190 | "id": 949223, 191 | "avatar_url": "https://avatars.githubusercontent.com/u/949223?v=3", 192 | "gravatar_id": "", 193 | "url": "https://api.github.com/users/AaronO", 194 | "html_url": "https://github.com/AaronO", 195 | "followers_url": "https://api.github.com/users/AaronO/followers", 196 | "following_url": "https://api.github.com/users/AaronO/following{/other_user}", 197 | "gists_url": "https://api.github.com/users/AaronO/gists{/gist_id}", 198 | "starred_url": "https://api.github.com/users/AaronO/starred{/owner}{/repo}", 199 | "subscriptions_url": "https://api.github.com/users/AaronO/subscriptions", 200 | "organizations_url": "https://api.github.com/users/AaronO/orgs", 201 | "repos_url": "https://api.github.com/users/AaronO/repos", 202 | "events_url": "https://api.github.com/users/AaronO/events{/privacy}", 203 | "received_events_url": "https://api.github.com/users/AaronO/received_events", 204 | "type": "User", 205 | "site_admin": false 206 | } 207 | } 208 | ` 209 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "fmt" 7 | ) 8 | 9 | // IsValidPayload checks if the github payload's hash fits with 10 | // the hash computed by GitHub sent as a header 11 | func IsValidPayload(secret, headerHash string, payload []byte) bool { 12 | hash := HashPayload(secret, payload) 13 | return hmac.Equal( 14 | []byte(hash), 15 | []byte(headerHash), 16 | ) 17 | } 18 | 19 | // HashPayload computes the hash of payload's body according to the webhook's secret token 20 | // see https://developer.github.com/webhooks/securing/#validating-payloads-from-github 21 | // returning the hash as a hexadecimal string 22 | func HashPayload(secret string, playloadBody []byte) string { 23 | hm := hmac.New(sha1.New, []byte(secret)) 24 | hm.Write(playloadBody) 25 | sum := hm.Sum(nil) 26 | return fmt.Sprintf("%x", sum) 27 | } 28 | -------------------------------------------------------------------------------- /validate_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHashing(t *testing.T) { 8 | // Hash 9 | hash := HashPayload("secret", []byte(TEST_HASH_PAYLOAD)) 10 | // Check that it matches up with what GitHub computed 11 | if hash != "178bc5c43ed5fab45dd958d7f057697b9a7a1278" { 12 | t.Errorf("Unexpected hash: %s", hash) 13 | } 14 | } 15 | 16 | // A real payload from GitHub 17 | const TEST_HASH_PAYLOAD = `{"zen":"Keep it logically awesome.","hook_id":6508402,"hook":{"url":"https://api.github.com/repos/AaronO/go-git-http/hooks/6508402","test_url":"https://api.github.com/repos/AaronO/go-git-http/hooks/6508402/test","ping_url":"https://api.github.com/repos/AaronO/go-git-http/hooks/6508402/pings","id":6508402,"name":"web","active":true,"events":["*"],"config":{"url":"http://dummy.com","content_type":"json","insecure_ssl":"0","secret":"********"},"last_response":{"code":null,"status":"unused","message":null},"updated_at":"2015-11-25T16:41:29Z","created_at":"2015-11-25T16:41:29Z"},"repository":{"id":22745825,"name":"go-git-http","full_name":"AaronO/go-git-http","owner":{"login":"AaronO","id":949223,"avatar_url":"https://avatars.githubusercontent.com/u/949223?v=3","gravatar_id":"","url":"https://api.github.com/users/AaronO","html_url":"https://github.com/AaronO","followers_url":"https://api.github.com/users/AaronO/followers","following_url":"https://api.github.com/users/AaronO/following{/other_user}","gists_url":"https://api.github.com/users/AaronO/gists{/gist_id}","starred_url":"https://api.github.com/users/AaronO/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AaronO/subscriptions","organizations_url":"https://api.github.com/users/AaronO/orgs","repos_url":"https://api.github.com/users/AaronO/repos","events_url":"https://api.github.com/users/AaronO/events{/privacy}","received_events_url":"https://api.github.com/users/AaronO/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/AaronO/go-git-http","description":"A Smart Git Http server library in Go (golang)","fork":false,"url":"https://api.github.com/repos/AaronO/go-git-http","forks_url":"https://api.github.com/repos/AaronO/go-git-http/forks","keys_url":"https://api.github.com/repos/AaronO/go-git-http/keys{/key_id}","collaborators_url":"https://api.github.com/repos/AaronO/go-git-http/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/AaronO/go-git-http/teams","hooks_url":"https://api.github.com/repos/AaronO/go-git-http/hooks","issue_events_url":"https://api.github.com/repos/AaronO/go-git-http/issues/events{/number}","events_url":"https://api.github.com/repos/AaronO/go-git-http/events","assignees_url":"https://api.github.com/repos/AaronO/go-git-http/assignees{/user}","branches_url":"https://api.github.com/repos/AaronO/go-git-http/branches{/branch}","tags_url":"https://api.github.com/repos/AaronO/go-git-http/tags","blobs_url":"https://api.github.com/repos/AaronO/go-git-http/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/AaronO/go-git-http/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/AaronO/go-git-http/git/refs{/sha}","trees_url":"https://api.github.com/repos/AaronO/go-git-http/git/trees{/sha}","statuses_url":"https://api.github.com/repos/AaronO/go-git-http/statuses/{sha}","languages_url":"https://api.github.com/repos/AaronO/go-git-http/languages","stargazers_url":"https://api.github.com/repos/AaronO/go-git-http/stargazers","contributors_url":"https://api.github.com/repos/AaronO/go-git-http/contributors","subscribers_url":"https://api.github.com/repos/AaronO/go-git-http/subscribers","subscription_url":"https://api.github.com/repos/AaronO/go-git-http/subscription","commits_url":"https://api.github.com/repos/AaronO/go-git-http/commits{/sha}","git_commits_url":"https://api.github.com/repos/AaronO/go-git-http/git/commits{/sha}","comments_url":"https://api.github.com/repos/AaronO/go-git-http/comments{/number}","issue_comment_url":"https://api.github.com/repos/AaronO/go-git-http/issues/comments{/number}","contents_url":"https://api.github.com/repos/AaronO/go-git-http/contents/{+path}","compare_url":"https://api.github.com/repos/AaronO/go-git-http/compare/{base}...{head}","merges_url":"https://api.github.com/repos/AaronO/go-git-http/merges","archive_url":"https://api.github.com/repos/AaronO/go-git-http/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/AaronO/go-git-http/downloads","issues_url":"https://api.github.com/repos/AaronO/go-git-http/issues{/number}","pulls_url":"https://api.github.com/repos/AaronO/go-git-http/pulls{/number}","milestones_url":"https://api.github.com/repos/AaronO/go-git-http/milestones{/number}","notifications_url":"https://api.github.com/repos/AaronO/go-git-http/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/AaronO/go-git-http/labels{/name}","releases_url":"https://api.github.com/repos/AaronO/go-git-http/releases{/id}","created_at":"2014-08-08T04:18:00Z","updated_at":"2015-11-04T16:44:09Z","pushed_at":"2015-05-04T18:42:16Z","git_url":"git://github.com/AaronO/go-git-http.git","ssh_url":"git@github.com:AaronO/go-git-http.git","clone_url":"https://github.com/AaronO/go-git-http.git","svn_url":"https://github.com/AaronO/go-git-http","homepage":null,"size":439,"stargazers_count":43,"watchers_count":43,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"open_issues_count":1,"forks":3,"open_issues":1,"watchers":43,"default_branch":"master"},"sender":{"login":"AaronO","id":949223,"avatar_url":"https://avatars.githubusercontent.com/u/949223?v=3","gravatar_id":"","url":"https://api.github.com/users/AaronO","html_url":"https://github.com/AaronO","followers_url":"https://api.github.com/users/AaronO/followers","following_url":"https://api.github.com/users/AaronO/following{/other_user}","gists_url":"https://api.github.com/users/AaronO/gists{/gist_id}","starred_url":"https://api.github.com/users/AaronO/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AaronO/subscriptions","organizations_url":"https://api.github.com/users/AaronO/orgs","repos_url":"https://api.github.com/users/AaronO/repos","events_url":"https://api.github.com/users/AaronO/events{/privacy}","received_events_url":"https://api.github.com/users/AaronO/received_events","type":"User","site_admin":false}}` 18 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | const VERSION = `1.0.0` 4 | --------------------------------------------------------------------------------