From c2c1bcd030bd1d6df32fc3f50294e4bc0e606bf9 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Thu, 26 Nov 2015 17:22:49 -0500 Subject: [PATCH 01/12] rm BasicAuth, which couldn't be used anyway --- src/utils/auth.jl | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/utils/auth.jl b/src/utils/auth.jl index 9b30e0e..7a85a10 100644 --- a/src/utils/auth.jl +++ b/src/utils/auth.jl @@ -4,11 +4,6 @@ abstract Authorization -immutable BasicAuth <: Authorization - user::GitHubString - password::GitHubString -end - immutable OAuth2 <: Authorization token::GitHubString end @@ -19,10 +14,6 @@ immutable AnonymousAuth <: Authorization end # API Methods # ############### -function authenticate(user::AbstractString, password::AbstractString) - return BasicAuth(user, password) -end - function authenticate(token::AbstractString) auth = OAuth2(token) r = github_get("/"; params = Dict("access_token" => auth.token)) @@ -34,28 +25,17 @@ end # Header Authentication # ######################### +authenticate_headers!(headers, auth::AnonymousAuth) = headers + function authenticate_headers!(headers, auth::OAuth2) headers["Authorization"] = "token $(auth.token)" return headers end -function authenticate_headers!(headers, auth::BasicAuth) - error("authentication with BasicAuth is not fully supported") -end - -function authenticate_headers!(headers, auth::AnonymousAuth) - return headers # nothing to be done -end - ################### # Pretty Printing # ################### -function Base.show(io::IO, a::BasicAuth) - pw_str = repeat("*", 8) - print(io, "GitHub Authorization ($(a.user), $pw_str))") -end - function Base.show(io::IO, a::OAuth2) token_str = a.token[1:6] * repeat("*", length(a.token) - 6) print(io, "GitHub Authorization ($token_str)") From 30cd920c0a7d8f4a84ffb7aaea9f4ac3b062c077 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Thu, 26 Nov 2015 17:41:33 -0500 Subject: [PATCH 02/12] partial work done on new documentation --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/GitHub.jl | 2 +- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36d39d7..de4c090 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,84 @@ julia> star("JuliaWeb/GitHub.jl"; auth = my_auth) # :) ## API -### Authentication +### Keyword Arguments + +The following table describes the various keyword arguments accepted by API methods that make requests to GitHub: + +| keyword | type | default value | description | +|----------------|-------------------------|--------------------------|------------------------------------------------------------------------------------------------| +| `auth` | `GitHub.Authorization` | `GitHub.AnonymousAuth()` | The request's authorization | +| `params` | `Dict` | `Dict()` | The request's query parameters | +| `headers` | `Dict` | `Dict()` | The request's headers | +| `handle_error` | `Bool` | `true` | If `true`, a Julia error will be thrown in the event that GitHub's response reports an error. | +| `page_limit` | `Real` | `Inf` | The number of pages to return (only applies to paginated results, obviously) | + +### `GitHubType`s + +GitHub's JSON responses are parsed into types `G<:GitHub.GitHubType`. Here's some useful information about these types: + +- All their fields are `Nullable`. +- Their field names generally match the corresponding field in GitHub's JSON representation (the exception is `"type"`, which has the corresponding field name `typ` to avoid the obvious language conflict). +- These types can be passed as arguments to API methods in place of (and in combination with) regular identifying values (e.g. `create_status(repo, sha)` could be called as `create_status(::GitHub.Repo, ::AbstractString)`, or `create_status(::GitHub.Repo, ::GitHub.Commit)`, etc.) + +Here's a table that matches up the provided `GitHubType`s with their corresponding API documentation: + +| type | link(s) to documentation | +|---------------|---------------------------------------------------------------------------------------------------------| +| `Owner` | [organizations](https://developer.github.com/v3/orgs/), [users](https://developer.github.com/v3/users/) | +| `Repo` | [repositories](https://developer.github.com/v3/repos/) | +| `Commit` | [repository commits](https://developer.github.com/v3/repos/commits/) | +| `Content` | [repository contents](https://developer.github.com/v3/repos/contents/) | +| `Comment` | [repository comments](https://developer.github.com/v3/repos/comments/) | +| `Status` | [commit statuses](https://developer.github.com/v3/repos/statuses/) | +| `PullRequest` | [pull requests](https://developer.github.com/v3/pulls/) | +| `Issue` | [issues](https://developer.github.com/v3/issues/) | + +### Methods + +Here are the methods exported by GitHub.jl, along with their return types and links to the corresponding GitHub API requests: + +| signature | return type | documentation | +|-----------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `owner(login, isorg = false)` | `Owner` | [get a user](https://developer.github.com/v3/users/#get-a-single-user), [get an org](https://developer.github.com/v3/orgs/#get-an-organization) | +| `orgs(owner)` | `Vector{Owner}` | +| `followers(owner)` | `Vector{Owner}` | +| `following(owner)` | `Vector{Owner}` | +| `repos(owner, isorg = false)` | `Vector{Repo}` | +| `repo(owner)` | `Repo` | +| `create_fork(repo)` | `Repo` | +| `forks(repo)` | `Vector{Repo}` | +| `contributors(repo)` | `Vector{Owner}` | +| `collaborators(repo)` | `Vector{Owner}` | +| `iscollaborator(repo, user)` | `Bool` | +| `add_collaborator(repo, user)` | `HttpCommon.Response` | +| `remove_collaborator(repo, user)` | `HttpCommon.Response` | +| `stats(repo, stat, attempts = 3)` | `HttpCommon.Response` | +| `commit(repo, sha)` | `Commit` | +| `commits(repo)` | `Vector{Commit}` | +| `file(repo, path)` | `Content` | +| `directory(repo, path)` | `Vector{Content}` | +| `create_file(repo, path)` | `Dict` | +| `update_file(repo, path)` | `Dict` | +| `delete_file(repo, path)` | `Dict` | +| `readme(repo)` | `Content` | +| `create_status(repo, sha)` | `Status` | +| `statuses(repo, ref)` | `Vector{Status}` | +| `pull_requests(repo)` | `Vector{PullRequest}` | +| `pull_request(repo, pr)` | `PullRequest` | +| `issue(repo, number)` | `Issue` | +| `issues(repo)` | `Vector{Issue}` | +| `create_issue(repo)` | `Issue` | +| `edit_issue(repo, number)` | `Issue` | +| `issue_comments(repo, number)` | `Vector{Comment}` | +| `star(repo)` | `HttpCommon.Response` | +| `unstar(repo)` | `HttpCommon.Response` | +| `stargazers(user)` | `Vector{Owner}` | +| `starred(user)` | `Vector{Repo}` | +| `watchers(repo)` | `Vector{Owner}` | +| `watched(owner)` | `Vector{Repo}` | +| `watch(repo)` | `HttpCommon.Response` | +| `unwatch(repo)` | `HttpCommon.Response` | All API methods accept a keyword `auth` of type `GitHub.Authorization`. By default, this parameter will be an instance of `AnonymousAuth`, and the API request will be made without any privileges. diff --git a/src/GitHub.jl b/src/GitHub.jl index a0282dc..dbbdc4d 100644 --- a/src/GitHub.jl +++ b/src/GitHub.jl @@ -67,7 +67,7 @@ include("repositories/comments.jl") export # repositories.jl Repo, repo, - fork, + create_fork, forks, contributors, collaborators, From 856fbe145aea214e478a12987144514770001da0 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Sat, 28 Nov 2015 13:12:53 -0500 Subject: [PATCH 03/12] less ambiguous argument names --- src/issues/issues.jl | 4 ++-- src/owners/owners.jl | 4 ++-- src/repositories/repositories.jl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/issues/issues.jl b/src/issues/issues.jl index 6c506be..3a3f82f 100644 --- a/src/issues/issues.jl +++ b/src/issues/issues.jl @@ -35,8 +35,8 @@ namefield(issue::Issue) = issue.number # API Methods # ############### -function issue(repo, issue; options...) - path = "/repos/$(name(repo))/issues/$(name(issue))" +function issue(repo, i; options...) + path = "/repos/$(name(repo))/issues/$(name(i))" return Issue(github_get_json(path; options...)) end diff --git a/src/owners/owners.jl b/src/owners/owners.jl index 2701552..9a13fff 100644 --- a/src/owners/owners.jl +++ b/src/owners/owners.jl @@ -43,8 +43,8 @@ typprefix(isorg) = isorg ? "orgs" : "users" isorg(owner::Owner) = get(owner.typ, "") == "Organization" -owner(obj::Owner; options...) = owner(name(obj), isorg(obj); options...) -owner(obj, isorg = false; options...) = Owner(github_get_json("/$(typprefix(isorg))/$(name(obj))"; options...)) +owner(owner_obj::Owner; options...) = owner(name(owner_obj), isorg(owner_obj); options...) +owner(owner_obj, isorg = false; options...) = Owner(github_get_json("/$(typprefix(isorg))/$(name(owner_obj))"; options...)) orgs(owner; options...) = map(Owner, github_get_json("/users/$(name(owner))/orgs"; options...)) diff --git a/src/repositories/repositories.jl b/src/repositories/repositories.jl index 9883647..9dbdf13 100644 --- a/src/repositories/repositories.jl +++ b/src/repositories/repositories.jl @@ -45,7 +45,7 @@ namefield(repo::Repo) = repo.full_name # repos # #-------# -repo(repo; options...) = Repo(github_get_json("/repos/$(name(repo))"; options...)) +repo(repo_obj; options...) = Repo(github_get_json("/repos/$(name(repo_obj))"; options...)) # forks # #-------# From 9b2c6404204a533042cc8eb50a6417802321d3ab Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Sat, 28 Nov 2015 13:13:15 -0500 Subject: [PATCH 04/12] simpler OAuth2 pretty printing --- src/utils/auth.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/auth.jl b/src/utils/auth.jl index 7a85a10..3fa3245 100644 --- a/src/utils/auth.jl +++ b/src/utils/auth.jl @@ -38,5 +38,5 @@ end function Base.show(io::IO, a::OAuth2) token_str = a.token[1:6] * repeat("*", length(a.token) - 6) - print(io, "GitHub Authorization ($token_str)") + print(io, "GitHub.OAuth2($token_str)") end From 03b3370e55316b2ba1afe4543c5764fcf06690c0 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Sat, 28 Nov 2015 13:21:24 -0500 Subject: [PATCH 05/12] resolve #18 by adding method/test for checking rate limit status, removed exportation of basic request methods --- src/GitHub.jl | 6 +----- src/utils/requests.jl | 6 ++++++ test/read_only_api_tests.jl | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/GitHub.jl b/src/GitHub.jl index dbbdc4d..a1d696b 100644 --- a/src/GitHub.jl +++ b/src/GitHub.jl @@ -26,11 +26,7 @@ export # auth.jl authenticate export # requests.jl - github_get, - github_post, - github_put, - github_patch, - github_delete + rate_limit ################################## # Owners (organizations + users) # diff --git a/src/utils/requests.jl b/src/utils/requests.jl index 5cd9829..62db664 100644 --- a/src/utils/requests.jl +++ b/src/utils/requests.jl @@ -47,6 +47,12 @@ github_patch_json(endpoint = ""; options...) = jsonify(github_patch(endpoint; op jsonify(r::HttpCommon.Response) = Requests.json(r) jsonify(arr::Array) = mapreduce(jsonify, vcat, arr) +################# +# Rate Limiting # +################# + +rate_limit(; options...) = github_get_json("/rate_limit"; options...)::Dict + ############## # Pagination # ############## diff --git a/test/read_only_api_tests.jl b/test/read_only_api_tests.jl index 14b351e..1ab7798 100644 --- a/test/read_only_api_tests.jl +++ b/test/read_only_api_tests.jl @@ -21,6 +21,8 @@ auth = authenticate(string(circshift(["bcc", "3fc", "03a", "33e", "fc6", "77b", '5', "9cf", "868", "033"], 3)...)) +@test rate_limit(; auth = auth)["rate"]["limit"] == 5000 + ########## # Owners # ########## From 85edd558370aaf8cdf4ab0dcbdeca880ebd34bcf Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Sat, 28 Nov 2015 13:59:27 -0500 Subject: [PATCH 06/12] implement methods/tests for retrieving combined status of a ref --- src/GitHub.jl | 3 ++- src/repositories/statuses.jl | 9 +++++++++ test/ghtype_tests.jl | 14 +++++++++++++- test/read_only_api_tests.jl | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/GitHub.jl b/src/GitHub.jl index a1d696b..d75ddce 100644 --- a/src/GitHub.jl +++ b/src/GitHub.jl @@ -89,7 +89,8 @@ export # contents.jl export # statuses.jl Status, create_status, - statuses + statuses, + status export # comments.jl Comment diff --git a/src/repositories/statuses.jl b/src/repositories/statuses.jl index d037c6e..d834748 100644 --- a/src/repositories/statuses.jl +++ b/src/repositories/statuses.jl @@ -4,14 +4,18 @@ type Status <: GitHubType id::Nullable{Int} + total_count::Nullable{Int} state::Nullable{GitHubString} description::Nullable{GitHubString} context::Nullable{GitHubString} + sha::Nullable{GitHubString} url::Nullable{HttpCommon.URI} target_url::Nullable{HttpCommon.URI} created_at::Nullable{Dates.DateTime} updated_at::Nullable{Dates.DateTime} creator::Nullable{Owner} + repository::Nullable{Repo} + statuses::Nullable{Vector{Status}} end Status(data::Dict) = json2github(Status, data) @@ -32,3 +36,8 @@ function statuses(repo, ref; options...) path = "/repos/$(name(repo))/commits/$(name(ref))/statuses" return map(Status, github_get_json(path; options...)) end + +function status(repo, ref; options...) + path = "/repos/$(name(repo))/commits/$(name(ref))/status" + return Status(github_get_json(path; options...)) +end diff --git a/test/ghtype_tests.jl b/test/ghtype_tests.jl index 2f4f8e1..e64ab3d 100644 --- a/test/ghtype_tests.jl +++ b/test/ghtype_tests.jl @@ -294,6 +294,14 @@ status_json = JSON.parse( "url": "https://api.github.com/repos/octocat/Hello-World/statuses/1", "creator": { "login": "octocat" + }, + "statuses": [ + { + "id": 366962428 + } + ], + "repository": { + "full_name": "JuliaWeb/GitHub.jl" } } """ @@ -301,14 +309,18 @@ status_json = JSON.parse( status_result = Status( Nullable{Int}(Int(status_json["id"])), + Nullable{Int}(), Nullable{GitHubString}(), Nullable{GitHubString}(GitHubString(status_json["description"])), Nullable{GitHubString}(), + Nullable{GitHubString}(), Nullable{HttpCommon.URI}(HttpCommon.URI(status_json["url"])), Nullable{HttpCommon.URI}(), Nullable{Dates.DateTime}(Dates.DateTime(chop(status_json["created_at"]))), Nullable{Dates.DateTime}(), - Nullable{Owner}(Owner(status_json["creator"])) + Nullable{Owner}(Owner(status_json["creator"])), + Nullable{Repo}(Repo(status_json["repository"])), + Nullable{Vector{Status}}(map(Status, status_json["statuses"])) ) @test Status(status_json) == status_result diff --git a/test/read_only_api_tests.jl b/test/read_only_api_tests.jl index 1ab7798..7a0df67 100644 --- a/test/read_only_api_tests.jl +++ b/test/read_only_api_tests.jl @@ -48,6 +48,7 @@ auth = authenticate(string(circshift(["bcc", "3fc", "03a", "33e", @test file(ghjl, "README.md"; auth = auth) == readme(ghjl; auth = auth) @test hasghobj("src/GitHub.jl", directory(ghjl, "src"; auth = auth)) @test !(isempty(statuses(ghjl, testcommit; auth = auth))) +@test get(status(ghjl, testcommit; auth = auth).sha) == name(testcommit) # These require `auth` to have push-access (it's currently a read-only token) # @test hasghobj("jrevels", collaborators(ghjl; auth = auth)) From efcf69f89e1c24ea8788bfb4b39a479d2b6dddcf Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Sun, 29 Nov 2015 06:05:13 -0500 Subject: [PATCH 07/12] added more comment functionality --- src/GitHub.jl | 16 ++++++---- src/issues/comments.jl | 58 ++++++++++++++++++++++++++++++++++++ src/issues/issues.jl | 9 ++---- src/repositories/comments.jl | 27 ----------------- test/read_only_api_tests.jl | 32 ++++++++++++++++++-- 5 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 src/issues/comments.jl delete mode 100644 src/repositories/comments.jl diff --git a/src/GitHub.jl b/src/GitHub.jl index d75ddce..ac4a056 100644 --- a/src/GitHub.jl +++ b/src/GitHub.jl @@ -56,7 +56,6 @@ include("repositories/repositories.jl") include("repositories/contents.jl") include("repositories/commits.jl") include("repositories/statuses.jl") -include("repositories/comments.jl") # export ------- @@ -92,9 +91,6 @@ export # statuses.jl statuses, status -export # comments.jl - Comment - ########## # Issues # ########## @@ -103,6 +99,7 @@ export # comments.jl include("issues/pull_requests.jl") include("issues/issues.jl") +include("issues/comments.jl") # export ------- @@ -116,8 +113,15 @@ export # issues.jl issue, issues, create_issue, - edit_issue, - issue_comments + edit_issue + +export # comments.jl + Comment, + comment, + comments, + create_comment, + edit_comment, + delete_comment ############ # Activity # diff --git a/src/issues/comments.jl b/src/issues/comments.jl new file mode 100644 index 0000000..5db84d6 --- /dev/null +++ b/src/issues/comments.jl @@ -0,0 +1,58 @@ +################ +# Comment Type # +################ + +type Comment <: GitHubType + body::Nullable{GitHubString} + path::Nullable{GitHubString} + diff_hunk::Nullable{GitHubString} + original_commit_id::Nullable{GitHubString} + commit_id::Nullable{GitHubString} + id::Nullable{Int} + original_position::Nullable{Int} + position::Nullable{Int} + line::Nullable{Int} + created_at::Nullable{Dates.DateTime} + updated_at::Nullable{Dates.DateTime} + url::Nullable{HttpCommon.URI} + html_url::Nullable{HttpCommon.URI} + issue_url::Nullable{HttpCommon.URI} + pull_request_url::Nullable{HttpCommon.URI} + user::Nullable{Owner} +end + +Comment(data::Dict) = json2github(Comment, data) +Comment(id::Real) = Comment(Dict("id" => id)) + +namefield(comment::Comment) = comment.id + +############### +# API Methods # +############### + +commentpath(review) = review ? "pulls" : "issues" + +function comment(repo, comment_obj, review = false; options...) + path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment_obj))" + return Comment(github_get_json(path; options...)) +end + +function comments(repo, issue_or_pr, review = false; options...) + path = "/repos/$(name(repo))/$(commentpath(review))/$(name(issue_or_pr))/comments" + return map(Comment, github_get_json(path; options...)) +end + +function create_comment(repo, issue_or_pr, review = false; options...) + path = "/repos/$(name(repo))/$(commentpath(review))/$(name(issue_or_pr))/comments" + return Comment(github_post_json(path; options...)) +end + +function edit_comment(repo, comment, review = false; options...) + path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment))" + return Comment(github_patch_json(path; options...)) +end + +function delete_comment(repo, comment, review = false; options...) + path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment))" + return github_delete(path; options...) +end diff --git a/src/issues/issues.jl b/src/issues/issues.jl index 3a3f82f..71fb211 100644 --- a/src/issues/issues.jl +++ b/src/issues/issues.jl @@ -35,8 +35,8 @@ namefield(issue::Issue) = issue.number # API Methods # ############### -function issue(repo, i; options...) - path = "/repos/$(name(repo))/issues/$(name(i))" +function issue(repo, issue_obj; options...) + path = "/repos/$(name(repo))/issues/$(name(issue_obj))" return Issue(github_get_json(path; options...)) end @@ -54,8 +54,3 @@ function edit_issue(repo, issue; options...) path = "/repos/$(name(repo))/issues/$(name(issue))" return Issue(github_patch_json(path; options...)) end - -function issue_comments(repo, issue; options...) - path = "/repos/$(name(repo))/issues/$(name(issue))/comments" - return map(Comment, github_get_json(path; options...)) -end diff --git a/src/repositories/comments.jl b/src/repositories/comments.jl deleted file mode 100644 index ff47de7..0000000 --- a/src/repositories/comments.jl +++ /dev/null @@ -1,27 +0,0 @@ -################ -# Comment Type # -################ - -type Comment <: GitHubType - body::Nullable{GitHubString} - path::Nullable{GitHubString} - diff_hunk::Nullable{GitHubString} - original_commit_id::Nullable{GitHubString} - commit_id::Nullable{GitHubString} - id::Nullable{Int} - original_position::Nullable{Int} - position::Nullable{Int} - line::Nullable{Int} - created_at::Nullable{Dates.DateTime} - updated_at::Nullable{Dates.DateTime} - url::Nullable{HttpCommon.URI} - html_url::Nullable{HttpCommon.URI} - issue_url::Nullable{HttpCommon.URI} - pull_request_url::Nullable{HttpCommon.URI} - user::Nullable{Owner} -end - -Comment(data::Dict) = json2github(Comment, data) -Comment(id::Real) = Comment(Dict("id" => id)) - -namefield(comment::Comment) = comment.id diff --git a/test/read_only_api_tests.jl b/test/read_only_api_tests.jl index 7a0df67..0dc65ea 100644 --- a/test/read_only_api_tests.jl +++ b/test/read_only_api_tests.jl @@ -27,28 +27,51 @@ auth = authenticate(string(circshift(["bcc", "3fc", "03a", "33e", # Owners # ########## +# test GitHub.owner @test name(owner(testuser; auth = auth)) == name(testuser) @test name(owner(julweb; auth = auth)) == name(julweb) + +# test GitHub.orgs @test hasghobj("JuliaWeb", orgs("jrevels"; auth = auth)) + +# test GitHub.followers, GitHub.following @test hasghobj("jrevels", followers(testuser; auth = auth)) @test hasghobj("jrevels", following(testuser; auth = auth)) + +# test GitHub.repos @test hasghobj(ghjl, repos(julweb; auth = auth)) ################ # Repositories # ################ +# test GitHub.repo @test name(repo(ghjl; auth = auth)) == name(ghjl) + +# test GitHub.forks @test length(forks(ghjl; auth = auth)) > 0 + +# test GitHub.contributors @test hasghobj("jrevels", map(x->x["contributor"], contributors(ghjl; auth = auth))) + +# test GitHub.stats @test stats(ghjl, "contributors"; auth = auth).status < 300 +# test GitHub.commit, GitHub.commits @test name(commit(ghjl, testcommit; auth = auth)) == name(testcommit) @test hasghobj(testcommit, commits(ghjl; auth = auth)) + +# test GitHub.file, GitHub.readme, GitHub.directory @test file(ghjl, "README.md"; auth = auth) == readme(ghjl; auth = auth) @test hasghobj("src/GitHub.jl", directory(ghjl, "src"; auth = auth)) -@test !(isempty(statuses(ghjl, testcommit; auth = auth))) + +# test GitHub.status, GitHub.statuses @test get(status(ghjl, testcommit; auth = auth).sha) == name(testcommit) +@test !(isempty(statuses(ghjl, testcommit; auth = auth))) + +# test GitHub.comment, GitHub.comments +@test name(comment(ghjl, 154431956; auth = auth)) == 154431956 +@test !(isempty(comments(ghjl, 40; auth = auth))) # These require `auth` to have push-access (it's currently a read-only token) # @test hasghobj("jrevels", collaborators(ghjl; auth = auth)) @@ -60,17 +83,22 @@ auth = authenticate(string(circshift(["bcc", "3fc", "03a", "33e", state_param = Dict("state" => "all") +# test GitHub.pull_request, GitHub.pull_requests @test get(pull_request(ghjl, 37; auth = auth).title) == "Fix dep warnings" @test hasghobj(37, pull_requests(ghjl; auth = auth, params = state_param)) + +# test GitHub.issue, GitHub.issues @test get(issue(ghjl, 40; auth = auth).title) == "Needs test" @test hasghobj(40, issues(ghjl; auth = auth, params = state_param)) -@test !(isempty(issue_comments(ghjl, 40; auth = auth))) ############ # Activity # ############ +# test GitHub.stargazers, GitHub.starred @test length(stargazers(ghjl; auth = auth)) > 10 # every package should fail tests if it's not popular enough :p @test hasghobj(ghjl, starred(testuser; auth = auth)) + +# test GitHub.watched, GitHub.watched @test hasghobj(testuser, watchers(ghjl; auth = auth)) @test hasghobj(ghjl, watched(testuser; auth = auth)) From 07415e323c8929be98187327cfe380b2b3f38e13 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Sun, 29 Nov 2015 06:28:44 -0500 Subject: [PATCH 08/12] alter authentication method to support kwargs like the other REST methods --- src/utils/auth.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/auth.jl b/src/utils/auth.jl index 3fa3245..8ebfdb3 100644 --- a/src/utils/auth.jl +++ b/src/utils/auth.jl @@ -14,10 +14,10 @@ immutable AnonymousAuth <: Authorization end # API Methods # ############### -function authenticate(token::AbstractString) +function authenticate(token::AbstractString; params = Dict(), options...) auth = OAuth2(token) - r = github_get("/"; params = Dict("access_token" => auth.token)) - handle_response_error(r) + params["access_token"] = auth.token + r = github_get("/"; params = params, options...) return auth end From 57773c09dc68a3b9c88173d11466bbd2dd134e2e Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Mon, 30 Nov 2015 10:07:20 -0500 Subject: [PATCH 09/12] refactor pagination structure to provide more feedback w.r.t. paginated results --- src/activity/activity.jl | 36 ++++-------- src/issues/comments.jl | 11 ++-- src/issues/issues.jl | 16 +++--- src/issues/pull_requests.jl | 8 +-- src/owners/owners.jl | 28 ++++++++-- src/repositories/commits.jl | 8 +-- src/repositories/contents.jl | 27 +++++---- src/repositories/repositories.jl | 31 ++++++----- src/repositories/statuses.jl | 12 ++-- src/utils/auth.jl | 2 +- src/utils/requests.jl | 94 ++++++++++++++++++++++---------- test/read_only_api_tests.jl | 34 ++++++------ 12 files changed, 179 insertions(+), 128 deletions(-) diff --git a/src/activity/activity.jl b/src/activity/activity.jl index e31414c..dbdac00 100644 --- a/src/activity/activity.jl +++ b/src/activity/activity.jl @@ -3,45 +3,33 @@ ############ function stargazers(repo; options...) - path = "/repos/$(name(repo))/stargazers" - return map(Owner, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/stargazers"; options...) + return map(Owner, results), page_data end function starred(user; options...) - path = "/users/$(name(user))/starred" - return map(Repo, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/users/$(name(user))/starred"; options...) + return map(Repo, results), page_data end -function star(repo; options...) - path = "/user/starred/$(name(repo))" - return github_put(path; options...) -end +star(repo; options...) = gh_put("/user/starred/$(name(repo))"; options...) -function unstar(repo; options...) - path = "/user/starred/$(name(repo))" - return github_delete(path; options...) -end +unstar(repo; options...) = gh_delete("/user/starred/$(name(repo))"; options...) ############ # Watching # ############ function watchers(repo; options...) - path = "/repos/$(name(repo))/subscribers" - return map(Owner, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/subscribers"; options...) + return map(Owner, results), page_data end function watched(owner; options...) - path = "/users/$(name(owner))/subscriptions" - return map(Repo, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/users/$(name(owner))/subscriptions"; options...) + return map(Repo, results), page_data end -function watch(repo; options...) - path = "/repos/$(name(repo))/subscription" - return github_put(path; options...) -end +watch(repo; options...) = gh_put("/repos/$(name(repo))/subscription"; options...) -function watch(repo; options...) - path = "/repos/$(name(repo))/subscription" - return github_delete(path; options...) -end +unwatch(repo; options...) = gh_delete("/repos/$(name(repo))/subscription"; options...) diff --git a/src/issues/comments.jl b/src/issues/comments.jl index 5db84d6..5b6460b 100644 --- a/src/issues/comments.jl +++ b/src/issues/comments.jl @@ -34,25 +34,26 @@ commentpath(review) = review ? "pulls" : "issues" function comment(repo, comment_obj, review = false; options...) path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment_obj))" - return Comment(github_get_json(path; options...)) + return Comment(gh_get_json(path; options...)) end function comments(repo, issue_or_pr, review = false; options...) path = "/repos/$(name(repo))/$(commentpath(review))/$(name(issue_or_pr))/comments" - return map(Comment, github_get_json(path; options...)) + results, page_data = gh_get_paged_json(path; options...) + return map(Comment, results), page_data end function create_comment(repo, issue_or_pr, review = false; options...) path = "/repos/$(name(repo))/$(commentpath(review))/$(name(issue_or_pr))/comments" - return Comment(github_post_json(path; options...)) + return Comment(gh_get_json(path; options...)) end function edit_comment(repo, comment, review = false; options...) path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment))" - return Comment(github_patch_json(path; options...)) + return Comment(gh_patch_json(path; options...)) end function delete_comment(repo, comment, review = false; options...) path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment))" - return github_delete(path; options...) + return gh_delete(path; options...) end diff --git a/src/issues/issues.jl b/src/issues/issues.jl index 71fb211..73e5825 100644 --- a/src/issues/issues.jl +++ b/src/issues/issues.jl @@ -36,21 +36,21 @@ namefield(issue::Issue) = issue.number ############### function issue(repo, issue_obj; options...) - path = "/repos/$(name(repo))/issues/$(name(issue_obj))" - return Issue(github_get_json(path; options...)) + result = gh_get_json("/repos/$(name(repo))/issues/$(name(issue_obj))"; options...) + return Issue(result) end function issues(repo; options...) - path = "/repos/$(name(repo))/issues" - return map(Issue, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/issues"; options...) + return map(Issue, results), page_data end function create_issue(repo; options...) - path = "/repos/$(name(repo))/issues" - return Issue(github_post_json(path; options...)) + result = gh_post_json("/repos/$(name(repo))/issues"; options...) + return Issue(result) end function edit_issue(repo, issue; options...) - path = "/repos/$(name(repo))/issues/$(name(issue))" - return Issue(github_patch_json(path; options...)) + result = gh_patch_json("/repos/$(name(repo))/issues/$(name(issue))"; options...) + return Issue(result) end diff --git a/src/issues/pull_requests.jl b/src/issues/pull_requests.jl index 27a1c70..580985d 100644 --- a/src/issues/pull_requests.jl +++ b/src/issues/pull_requests.jl @@ -59,11 +59,11 @@ namefield(pr::PullRequest) = pr.number ############### function pull_requests(repo; options...) - path = "/repos/$(name(repo))/pulls" - return map(PullRequest, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/pulls"; options...) + return map(PullRequest, results), page_data end function pull_request(repo, pr; options...) - path = "/repos/$(name(repo))/pulls/$(name(pr))" - return PullRequest(github_get_json(path; options...)) + result = gh_get_json("/repos/$(name(repo))/pulls/$(name(pr))"; options...) + return PullRequest(result) end diff --git a/src/owners/owners.jl b/src/owners/owners.jl index 9a13fff..b6f4296 100644 --- a/src/owners/owners.jl +++ b/src/owners/owners.jl @@ -44,12 +44,30 @@ typprefix(isorg) = isorg ? "orgs" : "users" isorg(owner::Owner) = get(owner.typ, "") == "Organization" owner(owner_obj::Owner; options...) = owner(name(owner_obj), isorg(owner_obj); options...) -owner(owner_obj, isorg = false; options...) = Owner(github_get_json("/$(typprefix(isorg))/$(name(owner_obj))"; options...)) -orgs(owner; options...) = map(Owner, github_get_json("/users/$(name(owner))/orgs"; options...)) +function owner(owner_obj, isorg = false; options...) + result = gh_get_json("/$(typprefix(isorg))/$(name(owner_obj))"; options...) + return Owner(result) +end + +function orgs(owner; options...) + results, page_data = gh_get_paged_json("/users/$(name(owner))/orgs"; options...) + return map(Owner, results), page_data +end + +function followers(owner; options...) + results, page_data = gh_get_paged_json("/users/$(name(owner))/followers"; options...) + return map(Owner, results), page_data +end -followers(owner; options...) = map(Owner, github_get_json("/users/$(name(owner))/followers"; options...)) -following(owner; options...) = map(Owner, github_get_json("/users/$(name(owner))/following"; options...)) +function following(owner; options...) + results, page_data = gh_get_paged_json("/users/$(name(owner))/following"; options...) + return map(Owner, results), page_data +end repos(owner::Owner; options...) = repos(name(owner), isorg(owner); options...) -repos(owner, isorg = false; options...) = map(Repo, github_get_json("/$(typprefix(isorg))/$(name(owner))/repos"; options...)) + +function repos(owner, isorg = false; options...) + results, page_data = gh_get_paged_json("/$(typprefix(isorg))/$(name(owner))/repos"; options...) + return map(Repo, results), page_data +end diff --git a/src/repositories/commits.jl b/src/repositories/commits.jl index cbc6f12..afc5fbf 100644 --- a/src/repositories/commits.jl +++ b/src/repositories/commits.jl @@ -27,11 +27,11 @@ namefield(commit::Commit) = commit.sha ############### function commits(repo; options...) - path = "/repos/$(name(repo))/commits" - return map(Commit, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/commits"; options...) + return map(Commit, results), page_data end function commit(repo, sha; options...) - path = "/repos/$(name(repo))/commits/$(name(sha))" - return Commit(github_get_json(path; options...)) + result = gh_get_json("/repos/$(name(repo))/commits/$(name(sha))"; options...) + return Commit(result) end diff --git a/src/repositories/contents.jl b/src/repositories/contents.jl index ff41054..459ee2e 100644 --- a/src/repositories/contents.jl +++ b/src/repositories/contents.jl @@ -27,27 +27,34 @@ namefield(content::Content) = content.path # API Methods # ############### -file(repo, path; options...) = Content(github_get_json(content_uri(repo, path); options...)) -directory(repo, path; options...) = map(Content, github_get_json(content_uri(repo, path); options...)) +function file(repo, path; options...) + result = gh_get_json(content_uri(repo, path); options...) + return Content(result) +end + +function directory(repo, path; options...) + results, page_data = gh_get_paged_json(content_uri(repo, path); options...) + return map(Content, results), page_data +end function create_file(repo, path; options...) - r = github_put_json(content_uri(repo, path); options...) - return build_content_response(r) + result = gh_put_json(content_uri(repo, path); options...) + return build_content_response(result) end function update_file(repo, path; options...) - r = github_put_json(content_uri(repo, path); options...) - return build_content_response(r) + result = gh_put_json(content_uri(repo, path); options...) + return build_content_response(result) end function delete_file(repo, path; options...) - r = github_delete_json(content_uri(repo, path); options...) - return build_content_response(r) + result = gh_delete_json(content_uri(repo, path); options...) + return build_content_response(result) end function readme(repo; options...) - path = "/repos/$(name(repo))/readme" - return Content(github_get_json(path; options...)) + result = gh_get_json("/repos/$(name(repo))/readme"; options...) + return Content(result) end ########################### diff --git a/src/repositories/repositories.jl b/src/repositories/repositories.jl index 9dbdf13..52973bb 100644 --- a/src/repositories/repositories.jl +++ b/src/repositories/repositories.jl @@ -45,38 +45,41 @@ namefield(repo::Repo) = repo.full_name # repos # #-------# -repo(repo_obj; options...) = Repo(github_get_json("/repos/$(name(repo_obj))"; options...)) +function repo(repo_obj; options...) + result = gh_get_json("/repos/$(name(repo_obj))"; options...) + return Repo(result) +end # forks # #-------# function forks(repo; options...) - path = "/repos/$(name(repo))/forks" - return map(Repo, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/forks"; options...) + return map(Repo, results), page_data end function create_fork(repo; options...) - path = "/repos/$(name(repo))/forks" - return Repo(github_post_json(path; options...)) + result = gh_post_json("/repos/$(name(repo))/forks"; options...) + return Repo(result) end # contributors/collaborators # #----------------------------# function contributors(repo; options...) - path = "/repos/$(name(repo))/contributors" - items = github_get_json(path; options...) - return [Dict("contributor" => Owner(i), "contributions" => i["contributions"]) for i in items] + results, page_data = gh_get_paged_json("/repos/$(name(repo))/contributors"; options...) + results = [Dict("contributor" => Owner(i), "contributions" => i["contributions"]) for i in results] + return results, page_data end function collaborators(repo; options...) - path = "/repos/$(name(repo))/collaborators" - return map(Owner, github_get_json(path; options...)) + results, page_data = gh_get_json("/repos/$(name(repo))/collaborators"; options...) + return map(Owner, results), page_data end function iscollaborator(repo, user; options...) path = "/repos/$(name(repo))/collaborators/$(name(user))" - r = github_get(path; handle_error = false, options...) + r = gh_get(path; handle_error = false, options...) r.status == 204 && return true r.status == 404 && return false handle_response_error(r) # 404 is not an error in this case @@ -85,12 +88,12 @@ end function add_collaborator(repo, user; options...) path = "/repos/$(name(repo))/collaborators/$(name(user))" - return github_put(path; options...) + return gh_put(path; options...) end function remove_collaborator(repo, user; options...) path = "/repos/$(name(repo))/collaborators/$(name(user))" - return github_delete(path; options...) + return gh_delete(path; options...) end # stats # @@ -100,7 +103,7 @@ function stats(repo, stat, attempts = 3; options...) path = "/repos/$(name(repo))/stats/$(name(stat))" local r for a in 1:attempts - r = github_get(path; handle_error = false, options...) + r = gh_get(path; handle_error = false, options...) r.status == 200 && return r sleep(2.0) end diff --git a/src/repositories/statuses.jl b/src/repositories/statuses.jl index d834748..6a6a33c 100644 --- a/src/repositories/statuses.jl +++ b/src/repositories/statuses.jl @@ -28,16 +28,16 @@ namefield(status::Status) = status.id ############### function create_status(repo, sha; options...) - path = "/repos/$(name(repo))/statuses/$(name(sha))" - return Status(github_post_json(path; options...)) + result = gh_post_json("/repos/$(name(repo))/statuses/$(name(sha))"; options...) + return Status(result) end function statuses(repo, ref; options...) - path = "/repos/$(name(repo))/commits/$(name(ref))/statuses" - return map(Status, github_get_json(path; options...)) + results, page_data = gh_get_paged_json("/repos/$(name(repo))/commits/$(name(ref))/statuses"; options...) + return map(Status, results), page_data end function status(repo, ref; options...) - path = "/repos/$(name(repo))/commits/$(name(ref))/status" - return Status(github_get_json(path; options...)) + result = gh_get_json("/repos/$(name(repo))/commits/$(name(ref))/status"; options...) + return Status(result) end diff --git a/src/utils/auth.jl b/src/utils/auth.jl index 8ebfdb3..20f1176 100644 --- a/src/utils/auth.jl +++ b/src/utils/auth.jl @@ -17,7 +17,7 @@ immutable AnonymousAuth <: Authorization end function authenticate(token::AbstractString; params = Dict(), options...) auth = OAuth2(token) params["access_token"] = auth.token - r = github_get("/"; params = params, options...) + gh_get("/"; params = params, options...) return auth end diff --git a/src/utils/requests.jl b/src/utils/requests.jl index 62db664..2ba9fc1 100644 --- a/src/utils/requests.jl +++ b/src/utils/requests.jl @@ -12,61 +12,95 @@ api_uri(path) = HttpCommon.URI(API_ENDPOINT, path = path) function github_request(request_method, endpoint; auth = AnonymousAuth(), handle_error = true, - headers = Dict(), params = Dict(), page_limit = Inf) + headers = Dict(), params = Dict()) authenticate_headers!(headers, auth) query = github2json(params) r = request_method(api_uri(endpoint); headers = headers, query = query) handle_error && handle_response_error(r) - - if ispaginated(r) - results = HttpCommon.Response[r] - page_count = 1 - while has_next_page(r) && page_count < page_limit - r = request_next_page(r, headers) - push!(results, r) - page_count += 1 - end - return results - end - return r end -github_get(endpoint = ""; options...) = github_request(Requests.get, endpoint; options...) -github_post(endpoint = ""; options...) = github_request(Requests.post, endpoint; options...) -github_put(endpoint = ""; options...) = github_request(Requests.put, endpoint; options...) -github_delete(endpoint = ""; options...) = github_request(Requests.delete, endpoint; options...) -github_patch(endpoint = ""; options...) = github_request(Requests.patch, endpoint; options...) - -github_get_json(endpoint = ""; options...) = jsonify(github_get(endpoint; options...)) -github_post_json(endpoint = ""; options...) = jsonify(github_post(endpoint; options...)) -github_put_json(endpoint = ""; options...) = jsonify(github_put(endpoint; options...)) -github_delete_json(endpoint = ""; options...) = jsonify(github_delete(endpoint; options...)) -github_patch_json(endpoint = ""; options...) = jsonify(github_patch(endpoint; options...)) +gh_get(endpoint = ""; options...) = github_request(Requests.get, endpoint; options...) +gh_post(endpoint = ""; options...) = github_request(Requests.post, endpoint; options...) +gh_put(endpoint = ""; options...) = github_request(Requests.put, endpoint; options...) +gh_delete(endpoint = ""; options...) = github_request(Requests.delete, endpoint; options...) +gh_patch(endpoint = ""; options...) = github_request(Requests.patch, endpoint; options...) -jsonify(r::HttpCommon.Response) = Requests.json(r) -jsonify(arr::Array) = mapreduce(jsonify, vcat, arr) +gh_get_json(endpoint = ""; options...) = Requests.json(gh_get(endpoint; options...)) +gh_post_json(endpoint = ""; options...) = Requests.json(gh_post(endpoint; options...)) +gh_put_json(endpoint = ""; options...) = Requests.json(gh_put(endpoint; options...)) +gh_delete_json(endpoint = ""; options...) = Requests.json(gh_delete(endpoint; options...)) +gh_patch_json(endpoint = ""; options...) = Requests.json(gh_patch(endpoint; options...)) ################# # Rate Limiting # ################# -rate_limit(; options...) = github_get_json("/rate_limit"; options...)::Dict +rate_limit(; options...) = gh_get_json("/rate_limit"; options...) ############## # Pagination # ############## ispaginated(r) = haskey(r.headers, "Link") -isnextlink(s) = contains(s, "rel=\"next\"") + +isnextlink(str) = contains(str, "rel=\"next\"") +islastlink(str) = contains(str, "rel=\"last\"") + has_next_page(r) = isnextlink(r.headers["Link"]) +has_last_page(r) = islastlink(r.headers["Link"]) + +split_links(r) = split(r.headers["Link"], ',') +get_link(pred, links) = match(r"<.*?>", links[findfirst(pred, links)]).match[2:end-1] + +get_page_number(link) = parse(Int, first(match(r"page=(\d+)", link).captures)) +get_next_page(r) = get_page_number(get_link(isnextlink, split_links(r))) +get_last_page(r) = get_page_number(get_link(islastlink, split_links(r))) function request_next_page(r, headers) - links = split(r.headers["Link"], ',') - nextlink = match(r"<.*?>", links[findfirst(isnextlink, links)]).match[2:end-1] + nextlink = get_link(isnextlink, split_links(r)) return Requests.get(nextlink, headers = headers) end +function github_paged_request(request_method, endpoint; page_limit = Inf, + auth = AnonymousAuth(), handle_error = true, + headers = Dict(), params = Dict()) + authenticate_headers!(headers, auth) + query = github2json(params) + r = request_method(api_uri(endpoint); headers = headers, query = query) + handle_error && handle_response_error(r) + + results = HttpCommon.Response[r] + init_page = get(query, "page", 1) + page_data = Dict{GitHubString, Int}() + + if ispaginated(r) + last_page = has_last_page(r) ? get_last_page(r) : init_page + next_page = has_next_page(r) ? get_next_page(r) : -1 + page_count = 1 + while next_page != -1 && page_count < page_limit + r = request_next_page(r, headers) + handle_error && handle_response_error(r) + push!(results, r) + page_count += 1 + next_page = has_next_page(r) ? get_next_page(r) : -1 + end + next_page != -1 && (page_data["next"] = next_page) + page_data["last"] = last_page + page_data["left"] = last_page - (init_page + page_count - 1) + else + page_data["last"] = init_page + page_data["left"] = 0 + end + + return results, page_data +end + +function gh_get_paged_json(endpoint = ""; options...) + results, page_data = github_paged_request(Requests.get, endpoint; options...) + return mapreduce(Requests.json, vcat, results), page_data +end + ################## # Error Handling # ################## diff --git a/test/read_only_api_tests.jl b/test/read_only_api_tests.jl index 0dc65ea..d0239b5 100644 --- a/test/read_only_api_tests.jl +++ b/test/read_only_api_tests.jl @@ -32,14 +32,14 @@ auth = authenticate(string(circshift(["bcc", "3fc", "03a", "33e", @test name(owner(julweb; auth = auth)) == name(julweb) # test GitHub.orgs -@test hasghobj("JuliaWeb", orgs("jrevels"; auth = auth)) +@test hasghobj("JuliaWeb", first(orgs("jrevels"; auth = auth))) # test GitHub.followers, GitHub.following -@test hasghobj("jrevels", followers(testuser; auth = auth)) -@test hasghobj("jrevels", following(testuser; auth = auth)) +@test hasghobj("jrevels", first(followers(testuser; auth = auth))) +@test hasghobj("jrevels", first(following(testuser; auth = auth))) # test GitHub.repos -@test hasghobj(ghjl, repos(julweb; auth = auth)) +@test hasghobj(ghjl, first(repos(julweb; auth = auth))) ################ # Repositories # @@ -49,32 +49,32 @@ auth = authenticate(string(circshift(["bcc", "3fc", "03a", "33e", @test name(repo(ghjl; auth = auth)) == name(ghjl) # test GitHub.forks -@test length(forks(ghjl; auth = auth)) > 0 +@test length(first(forks(ghjl; auth = auth))) > 0 # test GitHub.contributors -@test hasghobj("jrevels", map(x->x["contributor"], contributors(ghjl; auth = auth))) +@test hasghobj("jrevels", map(x->x["contributor"], first(contributors(ghjl; auth = auth)))) # test GitHub.stats @test stats(ghjl, "contributors"; auth = auth).status < 300 # test GitHub.commit, GitHub.commits @test name(commit(ghjl, testcommit; auth = auth)) == name(testcommit) -@test hasghobj(testcommit, commits(ghjl; auth = auth)) +@test hasghobj(testcommit, first(commits(ghjl; auth = auth))) # test GitHub.file, GitHub.readme, GitHub.directory @test file(ghjl, "README.md"; auth = auth) == readme(ghjl; auth = auth) -@test hasghobj("src/GitHub.jl", directory(ghjl, "src"; auth = auth)) +@test hasghobj("src/GitHub.jl", first(directory(ghjl, "src"; auth = auth))) # test GitHub.status, GitHub.statuses @test get(status(ghjl, testcommit; auth = auth).sha) == name(testcommit) -@test !(isempty(statuses(ghjl, testcommit; auth = auth))) +@test !(isempty(first(statuses(ghjl, testcommit; auth = auth)))) # test GitHub.comment, GitHub.comments @test name(comment(ghjl, 154431956; auth = auth)) == 154431956 -@test !(isempty(comments(ghjl, 40; auth = auth))) +@test !(isempty(first(comments(ghjl, 40; auth = auth)))) # These require `auth` to have push-access (it's currently a read-only token) -# @test hasghobj("jrevels", collaborators(ghjl; auth = auth)) +# @test hasghobj("jrevels", first(collaborators(ghjl; auth = auth))) # @test iscollaborator(ghjl, "jrevels"; auth = auth) ########## @@ -85,20 +85,20 @@ state_param = Dict("state" => "all") # test GitHub.pull_request, GitHub.pull_requests @test get(pull_request(ghjl, 37; auth = auth).title) == "Fix dep warnings" -@test hasghobj(37, pull_requests(ghjl; auth = auth, params = state_param)) +@test hasghobj(37, first(pull_requests(ghjl; auth = auth, params = state_param))) # test GitHub.issue, GitHub.issues @test get(issue(ghjl, 40; auth = auth).title) == "Needs test" -@test hasghobj(40, issues(ghjl; auth = auth, params = state_param)) +@test hasghobj(40, first(issues(ghjl; auth = auth, params = state_param))) ############ # Activity # ############ # test GitHub.stargazers, GitHub.starred -@test length(stargazers(ghjl; auth = auth)) > 10 # every package should fail tests if it's not popular enough :p -@test hasghobj(ghjl, starred(testuser; auth = auth)) +@test length(first(stargazers(ghjl; auth = auth))) > 10 # every package should fail tests if it's not popular enough :p +@test hasghobj(ghjl, first(starred(testuser; auth = auth))) # test GitHub.watched, GitHub.watched -@test hasghobj(testuser, watchers(ghjl; auth = auth)) -@test hasghobj(ghjl, watched(testuser; auth = auth)) +@test hasghobj(testuser, first(watchers(ghjl; auth = auth))) +@test hasghobj(ghjl, first(watched(testuser; auth = auth))) From 3b882b3c5c89a1eb9d48a3e572a75821da5210f0 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Mon, 30 Nov 2015 13:31:56 -0500 Subject: [PATCH 10/12] clean up event listener's repo validation and request forwarding mechanisms --- src/activity/events/events.jl | 22 +++++--- src/activity/events/listeners.jl | 92 +++++--------------------------- test/event_tests.jl | 10 ++-- 3 files changed, 36 insertions(+), 88 deletions(-) diff --git a/src/activity/events/events.jl b/src/activity/events/events.jl index e4e0143..7e4b7da 100644 --- a/src/activity/events/events.jl +++ b/src/activity/events/events.jl @@ -5,15 +5,25 @@ type WebhookEvent kind::GitHubString payload::Dict - repository::Nullable{Repo} - sender::Nullable{Owner} + repository::Repo + sender::Owner end function event_from_payload!(kind, data::Dict) - repository = extract_nullable(data, "repository", Repo) - sender = extract_nullable(data, "sender", Owner) - haskey(data, "repository") && delete!(data, "repository") - haskey(data, "sender") && delete!(data, "sender") + if haskey(data, "repository") + repository = Repo(data["repository"]) + elseif kind == "membership" + repository = Repo("") + else + error("event payload is missing repository field") + end + + if haskey(data, "sender") + sender = Owner(data["sender"]) + else + error("event payload is missing sender") + end + return WebhookEvent(kind, data, repository, sender) end diff --git a/src/activity/events/listeners.jl b/src/activity/events/listeners.jl index 2692875..520f2d6 100644 --- a/src/activity/events/listeners.jl +++ b/src/activity/events/listeners.jl @@ -8,7 +8,7 @@ event_header(request::HttpCommon.Request) = request.headers["X-GitHub-Event"] has_sig_header(request::HttpCommon.Request) = haskey(request.headers, "X-Hub-Signature") sig_header(request::HttpCommon.Request) = request.headers["X-Hub-Signature"] -function is_valid_secret(request::HttpCommon.Request, secret) +function has_valid_secret(request::HttpCommon.Request, secret) if has_sig_header(request) secret_sha = "sha1="*bytes2hex(MbedTLS.digest(MbedTLS.MD_SHA1, request.data, secret)) return sig_header(request) == secret_sha @@ -20,74 +20,14 @@ function is_valid_event(request::HttpCommon.Request, events) return (has_event_header(request) && in(event_header(request), events)) end -is_valid_repo(payload::Dict, repos) = in(payload["repository"]["full_name"], repos) +function from_valid_repo(event, repos) + return (name(event.repository) == "" || in(name(event.repository), repos)) +end ################# # EventListener # ################# -""" -A `GitHub.EventListener` is a server that handles events sent from a GitHub repo (usually via a webhook). When a `GitHub.EventListener` receives an event, it performs some basic validation and wraps the event payload in a `GitHub.WebhookEvent` type (use the REPL's `help` mode for more info on `GitHub.WebhookEvent`). This `GitHub.WebhookEvent` is then fed to the server's `handle` function, which defines how the server responds to the event. - -The `GitHub.EventListener` constructor takes in a handler function which should take in a `GitHub.WebhookEvent` and `GitHub.Authorization` and return an `HttpCommon.Response`. It also takes the following keyword arguments: - -- `auth`: GitHub authorization (usually with repo-level permissions) -- `secret`: A string used to verify the event source. If the event is from a GitHub webhook, it's the webhook's secret -- `repos`: A collection of fully qualified names of whitelisted repostories. All repostories are whitelisted by default -- `events`: A collection of webhook event name strings that contains all whitelisted events. All events are whitelisted by default -- `forwards`: A collection of address strings to which any incoming requests should be forwarded (after being validated by the listener) - -Here's an example that demonstrates how to construct and run a `GitHub.EventListener` that does some really basic benchmarking on every commit and PR: - - import GitHub - - # EventListener settings - myauth = GitHub.OAuth2(ENV["GITHUB_AUTH_TOKEN"]) - mysecret = ENV["MY_SECRET"] - myevents = ["pull_request", "push"] - myrepos = ["owner1/repo1", "owner2/repo2"] - myforwards = ["http://myforward1.com", "http://myforward2.com"] - - # Set up Status parameters - pending_params = Dict( - "state" => "pending", - "context" => "Benchmarker", - "description" => "Running benchmarks..." - ) - - success_params = Dict( - "state" => "success", - "context" => "Benchmarker", - "description" => "Benchmarks complete!" - ) - - listener = GitHub.EventListener(auth = myauth, - secret = mysecret, - repos = myrepos, - events = myevents, - forwards = myforwards) do event, auth - kind, payload = event.kind, event.payload - - if kind == "pull_request" && payload["action"] == "closed" - return HttpCommon.Response(200) - end - - sha = GitHub.most_recent_commit_sha(event) - - GitHub.create_status(event, sha; auth = auth, params = pending_params) - - # run_and_log_benchmarks isn't actually a defined function, but you get the point - run_and_log_benchmarks("\$(sha)-benchmarks.csv") - - GitHub.create_status(event, sha; auth = auth, params = success_params) - - return HttpCommon.Response(200) - end - - # Start the server on port 8000 - GitHub.run(listener, 8000) - -""" immutable EventListener server::HttpServer.Server function EventListener(handle; auth::Authorization = AnonymousAuth(), @@ -97,6 +37,10 @@ immutable EventListener forwards = map(HttpCommon.URI, forwards) end + if !(ias(repos, Void)) + repos = map(name, repos) + end + server = HttpServer.Server() do request, response try handle_event_request(request, handle; auth = auth, @@ -122,7 +66,7 @@ function handle_event_request(request, handle; auth::Authorization = AnonymousAuth(), secret = nothing, events = nothing, repos = nothing, forwards = nothing) - if !(isa(secret, Void)) && !(is_valid_secret(request, secret)) + if !(isa(secret, Void)) && !(has_valid_secret(request, secret)) return HttpCommon.Response(400, "invalid signature") end @@ -130,22 +74,18 @@ function handle_event_request(request, handle; return HttpCommon.Response(400, "invalid event") end - payload = Requests.json(request) + event = event_from_payload!(event_header(request), Requests.json(request)) - if !(isa(repos, Void)) && !(is_valid_repo(payload, repos)) + if !(isa(repos, Void)) && !(from_valid_repo(event, repos)) return HttpCommon.Response(400, "invalid repo") end if !(isa(forwards, Void)) for address in forwards - Requests.post(address, - UTF8String(request.data), - headers=request.headers) + Requests.post(address, request) end end - event = event_from_payload!(event_header(request), payload) - return handle(event, auth) end @@ -165,7 +105,7 @@ immutable CommentListener listener::EventListener function CommentListener(handle, trigger::AbstractString; auth::Authorization = AnonymousAuth(), - check_collab = true, + check_collab::Bool = true, secret = nothing, repos = nothing, forwards = nothing) @@ -194,11 +134,7 @@ function extract_trigger_string(event::WebhookEvent, trigger_regex = Regex("\`$trigger\(.*?\)\`") # extract repo/owner info from event - if isnull(event.repository) - return (false, "event is missing repo information") - end - - repo = get(event.repository) + repo = event.repository if isnull(repo.owner) return (false, "event repository is missing owner information") diff --git a/test/event_tests.jl b/test/event_tests.jl index 362d60e..e842f15 100644 --- a/test/event_tests.jl +++ b/test/event_tests.jl @@ -9,18 +9,20 @@ event = GitHub.event_from_payload!("commit_comment", event_json) # WebhookEvent # ################ -@test get(get(event.repository).name) == "BenchmarkTrackers.jl" -@test get(get(event.sender).login) == "jrevels" +@test get(event.repository.name) == "BenchmarkTrackers.jl" +@test get(event.sender.login) == "jrevels" @test most_recent_commit_sha(event) == "32d35f285777b077d8b6a2521309d1ab646d2379" ################# # EventListener # ################# -@test !(GitHub.is_valid_secret(event_request, "wrong")) -@test GitHub.is_valid_secret(event_request, "secret") +@test !(GitHub.has_valid_secret(event_request, "wrong")) +@test GitHub.has_valid_secret(event_request, "secret") @test !(GitHub.is_valid_event(event_request, ["wrong"])) @test GitHub.is_valid_event(event_request, ["commit_comment"]) +@test !(GitHub.from_valid_repo(event, ["JuliaWeb/GitHub.jl"])) +@test GitHub.from_valid_repo(event, ["JuliaCI/BenchmarkTrackers.jl"]) @test GitHub.handle_event_request(event_request, (args...) -> true, secret = "secret", events = ["commit_comment"], From 34f455a0b48dbbcdd47172833253ee41b5f42fd9 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Mon, 30 Nov 2015 19:30:26 -0500 Subject: [PATCH 11/12] reworked the comment-related methods to allow for interaction with commit comments --- src/issues/comments.jl | 66 +++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/src/issues/comments.jl b/src/issues/comments.jl index 5b6460b..6994af6 100644 --- a/src/issues/comments.jl +++ b/src/issues/comments.jl @@ -26,34 +26,74 @@ Comment(id::Real) = Comment(Dict("id" => id)) namefield(comment::Comment) = comment.id +kind_err_str(kind) = ("Error building comment request: :$kind is not a valid kind of comment.\n"* + "The only valid comment kinds are: :issue, :review, :commit") ############### # API Methods # ############### -commentpath(review) = review ? "pulls" : "issues" - -function comment(repo, comment_obj, review = false; options...) - path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment_obj))" +function comment(repo, item, kind = :issue; options...) + if kind == :issue + path = "/repos/$(name(repo))/issues/comments/$(name(item))" + elseif kind == :review + path = "/repos/$(name(repo))/pulls/comments/$(name(item))" + elseif kind == :commit + path = "/repos/$(name(repo))/comments/$(name(item))" + else + error(kind_err_str(kind)) + end return Comment(gh_get_json(path; options...)) end -function comments(repo, issue_or_pr, review = false; options...) - path = "/repos/$(name(repo))/$(commentpath(review))/$(name(issue_or_pr))/comments" +function comments(repo, item, kind = :issue; options...) + if kind == :issue + path = "/repos/$(name(repo))/issues/$(name(item))/comments" + elseif kind == :review + path = "/repos/$(name(repo))/pulls/$(name(item))/comments" + elseif kind == :commit + path = "/repos/$(name(repo))/commits/$(name(item))/comments" + else + error(kind_err_str(kind)) + end results, page_data = gh_get_paged_json(path; options...) return map(Comment, results), page_data end -function create_comment(repo, issue_or_pr, review = false; options...) - path = "/repos/$(name(repo))/$(commentpath(review))/$(name(issue_or_pr))/comments" - return Comment(gh_get_json(path; options...)) +function create_comment(repo, item, kind = :issue; options...) + if kind == :issue + path = "/repos/$(name(repo))/issues/$(name(item))/comments" + elseif kind == :review + path = "/repos/$(name(repo))/pulls/$(name(item))/comments" + elseif kind == :commit + path = "/repos/$(name(repo))/commits/$(name(item))/comments" + else + error(kind_err_str(kind)) + end + return Comment(gh_post_json(path; options...)) end -function edit_comment(repo, comment, review = false; options...) - path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment))" +function edit_comment(repo, item, kind = :issue; options...) + if kind == :issue + path = "/repos/$(name(repo))/issues/comments/$(name(item))" + elseif kind == :review + path = "/repos/$(name(repo))/pulls/comments/$(name(item))" + elseif kind == :commit + path = "/repos/$(name(repo))/comments/$(name(item))" + else + error(kind_err_str(kind)) + end return Comment(gh_patch_json(path; options...)) end -function delete_comment(repo, comment, review = false; options...) - path = "/repos/$(name(repo))/$(commentpath(review))/comments/$(name(comment))" +function delete_comment(repo, item, isreview = false; options...) + if kind == :issue + path = "/repos/$(name(repo))/issues/comments/$(name(item))" + elseif kind == :review + path = "/repos/$(name(repo))/pulls/comments/$(name(item))" + elseif kind == :commit + path = "/repos/$(name(repo))/comments/$(name(item))" + else + error(kind_err_str(kind)) + end return gh_delete(path; options...) end From 4dd2c39e70e58cc9c305869e139a66bdecc8dda3 Mon Sep 17 00:00:00 2001 From: Jarrett Revels Date: Mon, 30 Nov 2015 20:36:29 -0500 Subject: [PATCH 12/12] completed README documentation for new version of GitHub.jl --- README.md | 593 ++++++++++++++++++++++++++---------------------------- 1 file changed, 282 insertions(+), 311 deletions(-) diff --git a/README.md b/README.md index de4c090..ccdbdc4 100644 --- a/README.md +++ b/README.md @@ -6,390 +6,361 @@ [![Build status](https://ci.appveyor.com/api/projects/status/gmlm8snv03aw5pwq/branch/master?svg=true)](https://ci.appveyor.com/project/jrevels/github-jl-lj49i/branch/master) [![Coverage Status](https://coveralls.io/repos/JuliaWeb/GitHub.jl/badge.svg?branch=master&service=github)](https://coveralls.io/github/JuliaWeb/GitHub.jl?branch=master) -## Quick start +GitHub.jl provides a Julia interface to the [GitHub API v3](https://developer.github.com/v3/). Using GitHub.jl, you can do things like: -```julia -julia> Pkg.add("GitHub") +- query for basic repository, organization, and user information +- programmatically take user-level actions (e.g. starring a repository, commenting on an issue, etc.) +- set up listeners that can detect and respond to repository events +- create and retrieve commit statuses (i.e. report CI pending/failure/success statuses to GitHub) -julia> using GitHub +Here's a table of contents for this rather lengthy README: -julia> my_auth = authenticate("an_access_token_for_your_account") -GitHub Authorization (8caaff**********************************) +[1. Response Types](#response-types) -julia> star("JuliaWeb/GitHub.jl"; auth = my_auth) # :) -``` +[2. REST Methods](#rest-methods) + +[3. Authentication](#authentication) + +[4. Pagination](#pagination) + +[5. Handling Webhook Events](#handling-webhook-events) -## API +## Response Types -### Keyword Arguments +GitHub's JSON responses are parsed and returned to the caller as types of the form `G<:GitHub.GitHubType`. Here's some useful information about these types: -The following table describes the various keyword arguments accepted by API methods that make requests to GitHub: +- All fields are `Nullable`. +- Field names generally match the corresponding field in GitHub's JSON representation (the exception is `"type"`, which has the corresponding field name `typ` to avoid the obvious language conflict). +- `GitHubType`s can be passed as arguments to API methods in place of (and in combination with) regular identifying values. For example, `create_status(repo, commit)` could be called as: + + - `create_status(::GitHub.Repo, ::GitHub.Commit)` + - `create_status(::GitHub.Repo, ::AbstractString)` where the second argument is the SHA + - `create_status(::AbstractString, ::GitHub.Commit)` where the first argument is the full qualified repo name + - `create_status(::AbstractString, ::AbstractString)` where the first argument is the repo name, and the second is the SHA + +Here's a table that matches up the provided `GitHubType`s with their corresponding API documentation: + +| type | link(s) to documentation | +|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Owner` | [organizations](https://developer.github.com/v3/orgs/), [users](https://developer.github.com/v3/users/) | +| `Repo` | [repositories](https://developer.github.com/v3/repos/) | +| `Commit` | [repository commits](https://developer.github.com/v3/repos/commits/) | +| `Content` | [repository contents](https://developer.github.com/v3/repos/contents/) | +| `Comment` | [commit comments](https://developer.github.com/v3/repos/comments/), [issue comments](https://developer.github.com/v3/issues/comments/), [PR review comments](https://developer.github.com/v3/pulls/comments/) | +| `Status` | [commit statuses](https://developer.github.com/v3/repos/statuses/) | +| `PullRequest` | [pull requests](https://developer.github.com/v3/pulls/) | +| `Issue` | [issues](https://developer.github.com/v3/issues/) | + +You can inspect which fields are available for a type `G<:GitHubType` by calling `fieldnames(G)`. + +## REST Methods + +GitHub.jl implements a bunch of methods that make REST requests to GitHub's API. The below sections list these methods (note that a return type of `Tuple{Vector{T}, Dict}` means the result is [paginated](#pagination)). + +#### Users and Organizations + +| method | return type | documentation | +|------------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `owner(owner[, isorg = false])` | `Owner` | [get `owner`](https://developer.github.com/v3/users/#get-a-single-user)/[get an organization](https://developer.github.com/v3/orgs/#get-an-organization) | +| `orgs(owner)` | `Tuple{Vector{Owner}, Dict}` | [get the `owner`'s organizations](https://developer.github.com/v3/orgs/#list-user-organizations) | +| `followers(owner)` | `Tuple{Vector{Owner}, Dict}` | [get the `owner`'s followers](https://developer.github.com/v3/users/followers/#list-followers-of-a-user) | +| `following(owner)` | `Tuple{Vector{Owner}, Dict}` | [get the users followed by `owner`](https://developer.github.com/v3/users/followers/#list-users-followed-by-another-user) | +| `repos(owner[, isorg = false])` | `Tuple{Vector{Repo}, Dict}` | [get the `owner`'s repositories](https://developer.github.com/v3/repos/#list-user-repositories)/[get an organization's repositories](https://developer.github.com/v3/repos/#list-organization-repositories) | + +#### Repositories + +| method | return type | documentation | +|------------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `repo(repo)` | `Repo` | [get `repo`](https://developer.github.com/v3/repos/#get) | +| `create_fork(repo)` | `Repo` | [create a fork of `repo`](https://developer.github.com/v3/repos/forks/#create-a-fork) | +| `forks(repo)` | `Tuple{Vector{Repo}, Dict}` | [get `repo`'s forks](https://developer.github.com/v3/repos/forks/#list-forks) | +| `contributors(repo)` | `Dict` | [get `repo`'s contributors](https://developer.github.com/v3/repos/#list-contributors) | +| `collaborators(repo)` | `Tuple{Vector{Owner}, Dict}` | [get `repo`'s collaborators](https://developer.github.com/v3/repos/collaborators/#list) | +| `iscollaborator(repo, user)` | `Bool` | [check if `user` is a collaborator on `repo`](https://developer.github.com/v3/repos/collaborators/#get) | +| `add_collaborator(repo, user)` | `HttpCommon.Response` | [add `user` as a collaborator to `repo`](https://developer.github.com/v3/repos/collaborators/#add-collaborator) | +| `remove_collaborator(repo, user)` | `HttpCommon.Response` | [remove `user` as a collaborator from `repo`](https://developer.github.com/v3/repos/collaborators/#remove-collaborator) | +| `stats(repo, stat[, attempts = 3])` | `HttpCommon.Response` | [get information on `stat` (e.g. "contributors", "code_frequency", "commit_activity", etc.)](https://developer.github.com/v3/repos/statistics/) | +| `commit(repo, sha)` | `Commit` | [get the commit specified by `sha`](https://developer.github.com/v3/repos/commits/#get-a-single-commit) | +| `commits(repo)` | `Tuple{Vector{Commit}, Dict}` | [get `repo`'s commits](https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository) | +| `file(repo, path)` | `Content` | [get the file specified by `path`](https://developer.github.com/v3/repos/contents/#get-contents) | +| `directory(repo, path)` | `Tuple{Vector{Content}, Dict}` | [get the contents of the directory specified by `path`](https://developer.github.com/v3/repos/contents/#get-contents) | +| `create_file(repo, path)` | `Dict` | [create a file at `path` in `repo`](https://developer.github.com/v3/repos/contents/#create-a-file) | +| `update_file(repo, path)` | `Dict` | [update a file at `path` in `repo`](https://developer.github.com/v3/repos/contents/#update-a-file) | +| `delete_file(repo, path)` | `Dict` | [delete a file at `path` in `repo`](https://developer.github.com/v3/repos/contents/#delete-a-file) | +| `readme(repo)` | `Content` | [get `repo`'s README](https://developer.github.com/v3/repos/contents/#get-the-readme) | +| `create_status(repo, sha)` | `Status` | [create a status for the commit specified by `sha`](https://developer.github.com/v3/repos/statuses/#create-a-status) | +| `statuses(repo, ref)` | `Tuple{Vector{Status}, Dict}` | [get the statuses posted to `ref`](https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref) | +| `status(repo, ref)` | `Status` | [get the combined status for `ref`](https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref) | + +#### Pull Requests and Issues + +| method | return type | documentation | +|------------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `pull_request(repo, pr)` | `PullRequest` | [get the pull request specified by `pr`](https://developer.github.com/v3/pulls/#get-a-single-pull-request) | +| `pull_requests(repo)` | `Tuple{Vector{PullRequest}, Dict}` | [get `repo`'s pull requests](https://developer.github.com/v3/pulls/#list-pull-requests) | +| `issue(repo, issue)` | `Issue` | [get the issue specified by `issue`](https://developer.github.com/v3/issues/#get-a-single-issue) | +| `issues(repo)` | `Tuple{Vector{Issue}, Dict}` | [get `repo`'s issues](https://developer.github.com/v3/issues/#list-issues-for-a-repository) | +| `create_issue(repo)` | `Issue` | [create an issue in `repo`](https://developer.github.com/v3/issues/#create-an-issue) | +| `edit_issue(repo, issue)` | `Issue` | [edit `issue` in `repo`](https://developer.github.com/v3/issues/#edit-an-issue) | + +#### Comments + +| method | return type | documentation | +|------------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `comment(repo, comment, :issue)` | `Comment` | [get an issue `comment` from `repo`](https://developer.github.com/v3/issues/comments/#get-a-single-comment) | +| `comment(repo, comment, :review)` | `Comment` | [get an review `comment` from `repo`](https://developer.github.com/v3/pulls/comments/#get-a-single-comment) | +| `comment(repo, comment, :commit)` | `Comment` | [get a commit `comment` from `repo`](https://developer.github.com/v3/repos/comments/#get-a-single-commit-comment) | +| `comments(repo, issue, :issue)` | `Tuple{Vector{Comment}, Dict}` | [get the comments on `issue` in `repo`](https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue) | +| `comments(repo, pr, :review)` | `Tuple{Vector{Comment}, Dict}` | [get the review comments on `pr` in `repo`](https://developer.github.com/v3/pulls/comments/#list-comments-on-a-pull-request) | +| `comments(repo, commit, :commit)` | `Tuple{Vector{Comment}, Dict}` | [get the comments on `commit` in `repo`](https://developer.github.com/v3/repos/comments/#list-comments-for-a-single-commit) | +| `create_comment(repo, issue, :issue)` | `Comment` | [create a comment on `issue` in `repo`](https://developer.github.com/v3/issues/comments/#create-a-comment) | +| `create_comment(repo, pr, :review)` | `Comment` | [create a review comment on `pr` in `repo`](https://developer.github.com/v3/pulls/comments/#create-a-comment) | +| `create_comment(repo, commit, :commit)` | `Comment` | [create a comment on `commit` in `repo`](https://developer.github.com/v3/repos/comments/#create-a-commit-comment) | +| `edit_comment(repo, comment, :issue)` | `Comment` | [edit the issue `comment` in `repo`](https://developer.github.com/v3/issues/comments/#edit-a-comment) | +| `edit_comment(repo, comment, :review)` | `Comment` | [edit the review `comment` in `repo`](https://developer.github.com/v3/pulls/comments/#edit-a-comment) | +| `edit_comment(repo, comment, :commit)` | `Comment` | [edit the commit `comment` in `repo`](https://developer.github.com/v3/repos/comments/#update-a-commit-comment) | +| `delete_comment(repo, comment, :issue)` | `HttpCommon.Response` | [delete the issue `comment` from `repo`](https://developer.github.com/v3/issues/comments/#delete-a-comment) | +| `delete_comment(repo, comment, :review)` | `HttpCommon.Response` | [delete the review `comment` from `repo`](https://developer.github.com/v3/pulls/comments/#delete-a-comment) | +| `delete_comment(repo, comment, :commit)` | `HttpCommon.Response` | [delete the commit`comment` from `repo`](https://developer.github.com/v3/repos/comments/#delete-a-commit-comment) | + +#### Social Activity + +| method | return type | documentation | +|------------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `star(repo)` | `HttpCommon.Response` | [star `repo`](https://developer.github.com/v3/activity/starring/#star-a-repository) | +| `unstar(repo)` | `HttpCommon.Response` | [unstar `repo`](https://developer.github.com/v3/activity/starring/#unstar-a-repository) | +| `stargazers(repo)` | `Tuple{Vector{Owner}, Dict}` | [get `repo`'s stargazers](https://developer.github.com/v3/activity/starring/#list-stargazers) | +| `starred(user)` | `Tuple{Vector{Repo}, Dict}` | [get repositories starred by `user`](https://developer.github.com/v3/activity/starring/#list-repositories-being-starred) | +| `watchers(repo)` | `Tuple{Vector{Owner}, Dict}` | [get `repo`'s watchers](https://developer.github.com/v3/activity/watching/#list-watchers) | +| `watched(user)` | `Tuple{Vector{Repo}, Dict}` | [get repositories watched by `user`](https://developer.github.com/v3/activity/watching/#list-repositories-being-watched) | +| `watch(repo)` | `HttpCommon.Response` | [watch `repo`](https://developer.github.com/v3/activity/watching/#set-a-repository-subscription) | +| `unwatch(repo)` | `HttpCommon.Response` | [unwatch `repo`](https://developer.github.com/v3/activity/watching/#delete-a-repository-subscription) | + +#### Miscellaneous + +| method | return type | documentation | +|------------------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `rate_limit()` | `Dict` | [get your rate limit status](https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status) | +| `authenticate(token)` | `OAuth2` | [validate `token` and return an authentication object](https://developer.github.com/v3/#authentication) | + +#### Keyword Arguments + +All REST methods accept the following keyword arguments: | keyword | type | default value | description | |----------------|-------------------------|--------------------------|------------------------------------------------------------------------------------------------| | `auth` | `GitHub.Authorization` | `GitHub.AnonymousAuth()` | The request's authorization | | `params` | `Dict` | `Dict()` | The request's query parameters | -| `headers` | `Dict` | `Dict()` | The request's headers | +| `headers` | `Dict` | `Dict()` | The request's headers. Note that these headers will be mutated by GitHub.jl request methods. | | `handle_error` | `Bool` | `true` | If `true`, a Julia error will be thrown in the event that GitHub's response reports an error. | | `page_limit` | `Real` | `Inf` | The number of pages to return (only applies to paginated results, obviously) | -### `GitHubType`s +## Authentication -GitHub's JSON responses are parsed into types `G<:GitHub.GitHubType`. Here's some useful information about these types: +To authenticate your requests to GitHub, you'll need to generate an appropriate [access token](https://help.github.com/articles/creating-an-access-token-for-command-line-use). Then, you can do stuff like the following (this example assumes that you set an environmental variable `GITHUB_AUTH` containing the access token): -- All their fields are `Nullable`. -- Their field names generally match the corresponding field in GitHub's JSON representation (the exception is `"type"`, which has the corresponding field name `typ` to avoid the obvious language conflict). -- These types can be passed as arguments to API methods in place of (and in combination with) regular identifying values (e.g. `create_status(repo, sha)` could be called as `create_status(::GitHub.Repo, ::AbstractString)`, or `create_status(::GitHub.Repo, ::GitHub.Commit)`, etc.) +```julia +import GitHub +myauth = GitHub.authenticate(ENV["GITHUB_AUTH"]) # don't hardcode your access tokens! +GitHub.star("JuliaWeb/GitHub.jl"; auth = myauth) # star the GitHub.jl repo as the user identified by myauth +``` -Here's a table that matches up the provided `GitHubType`s with their corresponding API documentation: +As you can see, you can propagate the identity/permissions of the `myauth` token to GitHub.jl's methods by passing `auth = myauth` as a keyword argument. -| type | link(s) to documentation | -|---------------|---------------------------------------------------------------------------------------------------------| -| `Owner` | [organizations](https://developer.github.com/v3/orgs/), [users](https://developer.github.com/v3/users/) | -| `Repo` | [repositories](https://developer.github.com/v3/repos/) | -| `Commit` | [repository commits](https://developer.github.com/v3/repos/commits/) | -| `Content` | [repository contents](https://developer.github.com/v3/repos/contents/) | -| `Comment` | [repository comments](https://developer.github.com/v3/repos/comments/) | -| `Status` | [commit statuses](https://developer.github.com/v3/repos/statuses/) | -| `PullRequest` | [pull requests](https://developer.github.com/v3/pulls/) | -| `Issue` | [issues](https://developer.github.com/v3/issues/) | - -### Methods - -Here are the methods exported by GitHub.jl, along with their return types and links to the corresponding GitHub API requests: - -| signature | return type | documentation | -|-----------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| `owner(login, isorg = false)` | `Owner` | [get a user](https://developer.github.com/v3/users/#get-a-single-user), [get an org](https://developer.github.com/v3/orgs/#get-an-organization) | -| `orgs(owner)` | `Vector{Owner}` | -| `followers(owner)` | `Vector{Owner}` | -| `following(owner)` | `Vector{Owner}` | -| `repos(owner, isorg = false)` | `Vector{Repo}` | -| `repo(owner)` | `Repo` | -| `create_fork(repo)` | `Repo` | -| `forks(repo)` | `Vector{Repo}` | -| `contributors(repo)` | `Vector{Owner}` | -| `collaborators(repo)` | `Vector{Owner}` | -| `iscollaborator(repo, user)` | `Bool` | -| `add_collaborator(repo, user)` | `HttpCommon.Response` | -| `remove_collaborator(repo, user)` | `HttpCommon.Response` | -| `stats(repo, stat, attempts = 3)` | `HttpCommon.Response` | -| `commit(repo, sha)` | `Commit` | -| `commits(repo)` | `Vector{Commit}` | -| `file(repo, path)` | `Content` | -| `directory(repo, path)` | `Vector{Content}` | -| `create_file(repo, path)` | `Dict` | -| `update_file(repo, path)` | `Dict` | -| `delete_file(repo, path)` | `Dict` | -| `readme(repo)` | `Content` | -| `create_status(repo, sha)` | `Status` | -| `statuses(repo, ref)` | `Vector{Status}` | -| `pull_requests(repo)` | `Vector{PullRequest}` | -| `pull_request(repo, pr)` | `PullRequest` | -| `issue(repo, number)` | `Issue` | -| `issues(repo)` | `Vector{Issue}` | -| `create_issue(repo)` | `Issue` | -| `edit_issue(repo, number)` | `Issue` | -| `issue_comments(repo, number)` | `Vector{Comment}` | -| `star(repo)` | `HttpCommon.Response` | -| `unstar(repo)` | `HttpCommon.Response` | -| `stargazers(user)` | `Vector{Owner}` | -| `starred(user)` | `Vector{Repo}` | -| `watchers(repo)` | `Vector{Owner}` | -| `watched(owner)` | `Vector{Repo}` | -| `watch(repo)` | `HttpCommon.Response` | -| `unwatch(repo)` | `HttpCommon.Response` | - -All API methods accept a keyword `auth` of type `GitHub.Authorization`. By default, this parameter will be an instance of `AnonymousAuth`, and the API request will be made without any privileges. - -If you would like to make requests as an authorized user, you need to `authenticate`. +Note that if authentication is not provided, they'll be subject to the restrictions GitHub imposes on unauthenticated requests (such as [stricter rate limiting](https://developer.github.com/v3/#rate-limiting)) -```julia -authenticate(token::String) -``` -- `token` is an "access token" which you can [read about generating here](https://help.github.com/articles/creating-an-access-token-for-command-line-use) +## Pagination +GitHub will often [paginate](https://developer.github.com/v3/#pagination) results for requests that return multiple items. On the GitHub.jl side of things, it's pretty easy to see which methods return paginated results by referring to the [REST Methods documentation](#rest-methods); if a method returns a `Tuple{Vector{T}, Dict}`, that means its results are paginated. -### Users +Paginated methods return both the response values, and some pagination metadata. You can use the `per_page`/`page` query parameters and the `page_limit` keyword argument to configure result pagination. -The `User` type is used to represent GitHub accounts. It contains lots of interesting information about an account, and can be used in other API requests. +For example, let's request a couple pages of GitHub.jl's PRs, and configure our result pagination to see how it works: ```julia -user(username; auth = AnonymousAuth()) -``` -- `username` is the GitHub login -- if you provide `auth` potentially much more user information will be returned +julia> myparams = Dict("state" => "all", "per_page" => 3, "page" => 2); # show all PRs (both open and closed), and give me 3 items per page starting at page 2 -```julia -followers(user::String) -followers(user::User) -``` -```julia -following(user::String) -following(user::User) +julia> prs, page_data = pull_requests("JuliaWeb/GitHub.jl"; params = myparams, page_limit = 2); + +julia> prs # 3 items per page * 2 page limit == 6 items, as expected +6-element Array{GitHub.PullRequest,1}: + GitHub.PullRequest(39) + GitHub.PullRequest(38) + GitHub.PullRequest(37) + GitHub.PullRequest(34) + GitHub.PullRequest(32) + GitHub.PullRequest(30) + +julia> page_data +Dict{UTF8String,Int64} with 3 entries: + "last" => 5 + "left" => 2 + "next" => 4 ``` -- `user` is either a GitHub login or `User` type -- the returned data will be an array of `User` types +In the above, `prs` contains the results from page 2 and 3. We know this because we specified page 2 as our starting page (`"page" => 2`), and limited the response to 2 pages max (`page_limit = 2`). In addition, we know that exactly 2 pages were actually retrieved, since there are 6 items and we said each page should only contain 3 items (`"per_page" => 3`). -### Organizations +The values provided by `page_data` are calculated by assuming the same `per_page` value given in the original request. Here's a description of each key in `page_data`: -Organizations let multiple users manage repositories together. +- `page_data["last"]`: The last page of results available to be queried. In our example, the final page we could query for is page 5. +- `page_data["left"]`: The number of pages left between the final page delivered in our result and `page_data["last"]`. Our final page was page 3, and the last page is page 5, so we have 2 pages of results left to retrieve. +- `page_data["next"]`: The index of the next page after the final page delivered in our result. In the example, our final page was page 3, so the next page will be 4. -```julia -org(name; auth = AnonymousAuth()) -``` -- `name` is the GitHub organization login name +## Handling Webhook Events -```julia -orgs(user::String; auth = AnonymousAuth()) -``` -- `user` is the GitHub account about which you are curious +GitHub.jl comes with configurable `EventListener` and `CommentListener` types that can be used as basic servers for parsing and responding to events delivered by [GitHub's repository Webhooks](https://developer.github.com/webhooks/). +#### `EventListener` -### Repos +When an `EventListener` receives an event, it performs some basic validation and wraps the event payload (and some other data) in [a `WebhookEvent` type](https://github.com/JuliaWeb/GitHub.jl/blob/master/src/activity/events/events.jl). This `WebhookEvent` instance, along with the provided `Authorization`, is then fed to the server's handler function, which the user defines to determine the server's response behavior. The handler function is expected to return an `HttpCommon.Response` that is then sent back to GitHub. -The `Repo` type is used to represent a repository hosted by GitHub. It contains all sorts of useful information about a repositories usage and history. +The `EventListener` constructor takes the following keyword arguments: -```julia -repo(owner, repo_name; auth = AnonymousAuth()) -``` -- `owner` is the GitHub login of the `User` or `Organization` that manages the repo -- `repo_name` is the repositories name on GitHub +- `auth`: GitHub authorization (usually with repo-level permissions). This is passed as the second argument to the server's handler function. +- `secret`: A string used to verify the event source. If the event is from a GitHub Webhook, it's the Webhook's secret. If a secret is not provided, the server won't validate the secret signature of incoming requests. +- `repos`: A vector of `Repo`s (or fully qualified repository names) listing all acceptable repositories. All repositories are whitelisted by default. +- `events`: A vector of [event names](https://developer.github.com/webhooks/#events) listing all acceptable events (e.g. ["commit_comment", "pull_request"]). All events are whitelisted by default. +- `forwards`: A vector of `HttpCommon.URI`s (or URI strings) to which any incoming requests should be forwarded (after being validated by the listener) +Here's an example that demonstrates how to construct and run an `EventListener` that does benchmarking on every commit and PR: ```julia -repos(owner::Owner; auth = AnonymousAuth(), - typ = nothing, - sort = nothing, - direction = nothing) -``` -- `owner` is a `User` or `Organization` -- `typ` is "all", "member", or "owner" (the default) for User -- `typ` is "all" (the default), "public", "private", "forks", "sources", or "member". -- `sort` is "created", "updated", "pushed", or "full_name" (the default). -- `direction` is "asc" or "desc" (the default). +import GitHub +# EventListener settings +myauth = GitHub.authenticate(ENV["GITHUB_AUTH"]) +mysecret = ENV["MY_SECRET"] +myevents = ["pull_request", "push"] +myrepos = [GitHub.Repo("owner1/repo1"), "owner2/repo2"] # can be Repos or repo names +myforwards = [HttpCommon.URI("http://myforward1.com"), "http://myforward2.com"] # can be HttpCommon.URIs or URI strings -```julia -contributors(owner, repo; auth = AnonymousAuth() - include_anon = false) -``` -- `owner` is the GitHub login of the `User` or `Organization` that manages the repo -- `repo` is the repositories name on GitHub -- `include_anon` will tell GitHub to include anonymous contributions +# Set up Status parameters +pending_params = Dict( + "state" => "pending", + "context" => "Benchmarker", + "description" => "Running benchmarks..." +) +success_params = Dict( + "state" => "success", + "context" => "Benchmarker", + "description" => "Benchmarks complete!" +) -### Statistics +error_params(err) = Dict( + "state" => "error", + "context" => "Benchmarker", + "description" => "Error: $err" +) -Repository statistics are interesting bits of information about activity. GitHub caches this data when possible, but sometimes a request will trigger regeneration and come back empty. For this reason all statistics functions have an argument `attempts` which will be the number of tries made before admitting defeat. +# We can use Julia's `do` notation to set up the listener's handler function +listener = GitHub.EventListener(auth = myauth, + secret = mysecret, + repos = myrepos, + events = myevents, + forwards = myforwards) do event, auth + kind, payload, repo = event.kind, event.payload, event.repository -```julia -contributor_stats(owner, repo, attempts = 3; auth = AnonymousAuth()) -``` -```julia -commit_activity(owner, repo, attempts = 3; auth = AnonymousAuth()) -``` -```julia -code_frequency(owner, repo, attempts = 3; auth = AnonymousAuth()) -``` -```julia -participation(owner, repo, attempts = 3; auth = AnonymousAuth()) -``` -```julia -punch_card(owner, repo, attempts = 3; auth = AnonymousAuth()) -``` -- `owner` is a GitHub login -- `repo` is a repository name -- `attempts` is the number of tries made before admitting defeat + if kind == "pull_request" && payload["action"] == "closed" + return HttpCommon.Response(200) + end + sha = GitHub.most_recent_commit_sha(event) -### Forks + GitHub.create_status(repo, sha; auth = auth, params = pending_params) -```julia -forks(owner, repo; auth = AnonymousAuth()) -``` -```julia -fork(owner, repo, organization = ""; auth = AnonymousAuth()) -``` -- `owner` is a GitHub login -- `repo` is a repository name + try + # run_and_log_benchmarks isn't actually a defined function, but you get the point + run_and_log_benchmarks(event, "\$(sha)-benchmarks.csv") + catch err + GitHub.create_status(repo, sha; auth = auth, params = error_params(err)) + return HttpCommon.Response(500) + end + GitHub.create_status(repo, sha; auth = auth, params = success_params) -### Starring + return HttpCommon.Response(200) +end -```julia -stargazers(owner, repo; auth = AnonymousAuth()) -``` -```julia -starred(user; auth = AnonymousAuth()) -``` -```julia -star(owner, repo; auth = AnonymousAuth()) -``` -```julia -unstar(owner, repo; auth = AnonymousAuth()) +# Start the listener on localhost at port 8000 +GitHub.run(listener, host=IPv4(127,0,0,1), port=8000) ``` -- `owner` is a GitHub login -- `repo` is a repository name -- `user` is a GitHub login +#### `CommentListener` -### Watching +A `CommentListener` is a special kind of `EventListener` that allows users to pass data to the listener's handler function via commenting. This is useful for triggering events on repositories that require configuration settings. + +A `CommentListener` automatically filters out all non-comment events, and then checks each comment event it receives for a specific, user-defined "trigger phrase". The trigger phrase has the following structure: -```julia -watchers(owner, repo; auth = AnonymousAuth()) -``` -```julia -watched(user; auth = AnonymousAuth()) -``` -```julia -watching(owner, repo; auth = AnonymousAuth()) ``` -```julia -watch(owner, repo; auth = AnonymousAuth()) +`$trigger(args...)` ``` -```julia -unwatch(owner, repo; auth = AnonymousAuth()) -``` -- `owner` is a GitHub login -- `repo` is a repository name -- `user` is a GitHub login +...where `args` can be anything. If the trigger phrase is found in a comment, then the `CommentListener` calls its handler function, passing it the event, the provided authentication, and `"(args...)"`. -### Collaborators +The `CommentListener` constructor takes the following keyword arguments: -Collaborators are users that work together and share access to a repository. +- `auth`: same as `EventListener` +- `secret`: same as `EventListener` +- `repos`: same as `EventListener` +- `forwards`: same as `EventListener` +- `check_collab`: If `true`, only acknowledge comments made by repository collaborators. Note that, if `check_collab` is `true`, `auth` must have the appropriate permissions to query the comment's repository for the collaborator status of the commenter. `check_collab` is `true` by default. + +For example, let's set up a silly `CommentListener` that responds to the commenter with a greeting. To give a demonstration of the desired behavior, if a collaborator makes a comment like: -```julia -collaborators(owner, repo; auth = AnonymousAuth()) -``` -```julia -iscollaborator(owner, repo, user; auth = AnonymousAuth()) -``` -```julia -add_collaborator(owner, repo, user; auth = AnonymousAuth() -``` -```julia -remove_collaborator(owner, repo, user; auth = AnonymousAuth()) ``` -- `owner` is a GitHub login -- `repo` is a repository name -- `user` is the GitHub login being inspected, added, or removed +Man, I really would like to be greeted today. -#### Examples -```julia -julia> using GitHub - -julia> collaborators("JuliaLang","Julia") -26-element Array{Any,1}: - User - amitmurthy - User - andreasnoackjensen - ⋮ - User - tshort - User - vtjnash - -julia> o = org("JuliaLang") -User - JuliaLang (The Julia Language, http://julialang.org/) - -julia> collaborators(o,"julia") -26-element Array{Any,1}: - User - amitmurthy - User - andreasnoackjensen - ⋮ - User - tshort - User - vtjnash - -julia> r = repo("JuliaLang","julia") -Repo - JuliaLang/julia (http://julialang.org/) -"The Julia Language: A fresh approach to technical computing." - -julia> collaborators(r) -26-element Array{Any,1}: - User - amitmurthy - User - andreasnoackjensen - ⋮ - User - tshort - User - vtjnash +`sayhello("Bob", "outgoing")` ``` +We want the `CommentLister` to reply (using the provided `auth`): -### Issues +``` +Hello, Bob, you look very outgoing today! +``` -The `Issue` type is used to represent issues and pull requests made against repositories. +Here's the code that will make this happen: ```julia -issue(owner, repo, num; auth = AnonymousAuth()) -``` -- `owner` is a GitHub login or `User` type -- `repo` is the name of a repository -- `num` is the issue numer +import GitHub -```julia -issues(owner, repo; auth = AnonymousAuth(), - milestone = nothing, - state = nothing, - assignee = nothing, - creator = nothing, - mentioned = nothing, - labels = nothing, - sort = nothing, - direction = nothing, - since = nothing) -``` -- `owner` is a GitHub login or `User` type -- `repo` is a repository name -- `milestone` can be an int or string ("*" matches all milestones, "none" returns issues with no milestone) -- `state` can be "open" or "closed" -- `assignee` can be the name of a user ("*" matches all users, "none" returns issues with no assignee) -- `creator` can be the user that created the issue -- `mentioned` is for any user mentioned in the issue -- `labels` is an array of labels to match -- `sort` can be "created", "updated", or "comments" (defaults to "created") -- `direction` can be "asc" or "desc" (defaults to "desc") -- `since` can be an ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) string +# CommentListener settings +trigger = "sayhello" +myauth = GitHub.authenticate(ENV["GITHUB_AUTH"]) +mysecret = ENV["MY_SECRET"] -```julia -create_issue(owner, repo, title; auth = AnonymousAuth(), - body = nothing, - assignee = nothing, - milestone = nothing, - labels = nothing) -``` -- `owner` is a GitHub login or `User` type -- `repo` is a repository name -- `title` is the title of your new issue -- `body` can be a text description of your issue -- `assignee` is a GitHub login -- `milestone` is the milestone number -- `labels` is an array of label strings +# We can use Julia's `do` notation to set up the listener's handler function. +# Note that, in our example case, argstring will equal "(\"Bob\", \"outgoing\")". +listener = GitHub.CommentListener(trigger; auth = myauth, secret = mysecret) do event, auth, argstring + # In our example case, this code sets name to "Bob" and adjective to "outgoing" + name, adjective = map(s -> strip(s)[2:(end-1)], split(argstring[2:(end-1)], ',')) + comment_params = Dict("body" => "Hello, $name, you look very $adjective today!") -```julia -edit_issue(owner, repo, num; auth = AnonymousAuth(), - title = nothing, - body = nothing, - assignee = nothing, - state = nothing, - milestone = nothing, - labels = nothing) -``` -- `owner` is a GitHub login or `User` type -- `repo` is a repository name -- `num` is the issue number -- `title` can be a new title for the issue -- `body` can be a new body for the issue -- `assignee` can be the new assignee -- `state` can be "open" or "closed" -- `milestone` can be the milestone number -- `labels` can be an array of label strings + # Parse the original comment event for all the necessary reply info + comment = GitHub.Comment(event.payload["comment"]) -### Comments + if event.kind == "issue_comment" + comment_kind = :issue + reply_to = event.payload["issue"]["number"] + elseif event.kind == "commit_comment" + comment_kind = :commit + reply_to = get(comment.commit_id) + elseif event.kind == "pull_request_review_comment" + comment_kind = :review + reply_to = event.payload["pull_request"]["number"] + # load required query params for review comment creation + comment_params["commit_id"] = get(comment.commit_id) + comment_params["path"] = get(comment.path) + comment_params["position"] = get(comment.position) + end -The `Comment` type is used to represent comments on Github issues. + # send the comment creation request to GitHub + GitHub.create_comment(event.repository, reply_to, comment_kind; auth = auth, params = comment_params) -```julia -comments(owner, repo, num; auth = AnonymousAuth()) + return HttpCommon.Response(200) +end + +# Start the listener on localhost at port 8000 +GitHub.run(listener, host=IPv4(127,0,0,1), port=8000) ``` -- `owner` is a GitHub login or `User` type -- `repo` is a repository name -- `num` is the issue number