1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Nit object oriented interface to Github api.
17 # This modules reifies Github API elements as Nit classes.
19 # For most use-cases you need to use the `GithubAPI` client.
24 # Interface to Github REST API.
26 # Used by all `GithubEntity` to perform requests.
31 # # Get Github authentification token.
32 # var token = get_github_oauth
33 # assert not token.is_empty
36 # var api = new GithubAPI(token)
39 # The API client allows to get Github API entities:
42 # var repo = api.load_repo("privat/nit")
44 # assert repo.name == "nit"
46 # var user = api.load_user("Morriar")
48 # assert user.login == "Morriar"
52 # Github API OAuth token.
54 # This token is used to authenticate the application on Github API.
55 # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
56 # associated to the key.
59 # Github API base url.
61 # Default is `https://api.github.com` and should not be changed.
62 var api_url
= "https://api.github.com"
64 # User agent used for HTTP requests.
66 # Default is `nit_github_api`.
68 # See <https://developer.github.com/v3/#user-agent-required>
69 var user_agent
= "nit_github_api"
73 # Internal Curl instance used to perform API calls.
74 private var ghcurl
: GithubCurl is noinit
78 # * `0`: only errors (default)
80 var verbose_lvl
= 0 is public
writable
83 ghcurl
= new GithubCurl(auth
, user_agent
)
86 # Execute a GET request on Github API.
88 # This method returns raw json data.
89 # See other `load_*` methods to use more expressive types.
91 # var api = new GithubAPI(get_github_oauth)
92 # var obj = api.get("repos/privat/nit")
93 # assert obj isa JsonObject
94 # assert obj["name"] == "nit"
96 # Returns `null` in case of `error`.
98 # obj = api.get("foo/bar/baz")
100 # assert api.was_error
101 # var err = api.last_error
102 # assert err isa GithubError
103 # assert err.name == "GithubAPIError"
104 # assert err.message == "Not Found"
105 fun get
(path
: String): nullable Jsonable do
106 path
= sanitize_uri
(path
)
107 var res
= ghcurl
.get_and_parse
("{api_url}/{path}")
108 if res
isa Error then
117 # Display a message depending on `verbose_lvl`.
118 fun message
(lvl
: Int, message
: String) do
119 if lvl
<= verbose_lvl
then print message
122 # Escape `uri` in an acceptable format for Github.
123 private fun sanitize_uri
(uri
: String): String do
124 # TODO better URI escape.
125 return uri
.replace
(" ", "%20")
128 # Last error occured during Github API communications.
129 var last_error
: nullable Error = null is public
writable
131 # Does the last request provoqued an error?
132 var was_error
= false is protected writable
134 # Load the json object from Github.
135 # See `GithubEntity::load_from_github`.
136 private fun load_from_github
(key
: String): JsonObject do
137 message
(1, "Get {key} (github)")
139 if was_error
then return new JsonObject
140 return res
.as(JsonObject)
143 # Get the Github user with `login`.
145 # Returns `null` if the user cannot be found.
147 # var api = new GithubAPI(get_github_oauth)
148 # var user = api.load_user("Morriar")
149 # assert user.login == "Morriar"
150 fun load_user
(login
: String): nullable User do
151 var user
= new User(self, login
)
152 return user
.load_from_github
155 # Get the Github repo with `full_name`.
157 # Returns `null` if the repo cannot be found.
159 # var api = new GithubAPI(get_github_oauth)
160 # var repo = api.load_repo("privat/nit")
161 # assert repo.name == "nit"
162 # assert repo.owner.login == "privat"
163 # assert repo.default_branch.name == "master"
164 fun load_repo
(full_name
: String): nullable Repo do
165 var repo
= new Repo(self, full_name
)
166 return repo
.load_from_github
169 # Get the Github branch with `name`.
171 # Returns `null` if the branch cannot be found.
173 # var api = new GithubAPI(get_github_oauth)
174 # var repo = api.load_repo("privat/nit")
175 # assert repo != null
176 # var branch = api.load_branch(repo, "master")
177 # assert branch.name == "master"
178 # assert branch.commit isa Commit
179 fun load_branch
(repo
: Repo, name
: String): nullable Branch do
180 var branch
= new Branch(self, repo
, name
)
181 return branch
.load_from_github
184 # Get the Github commit with `sha`.
186 # Returns `null` if the commit cannot be found.
188 # var api = new GithubAPI(get_github_oauth)
189 # var repo = api.load_repo("privat/nit")
190 # assert repo != null
191 # var commit = api.load_commit(repo, "64ce1f")
192 # assert commit isa Commit
193 fun load_commit
(repo
: Repo, sha
: String): nullable Commit do
194 var commit
= new Commit(self, repo
, sha
)
195 return commit
.load_from_github
198 # Get the Github issue #`number`.
200 # Returns `null` if the issue cannot be found.
202 # var api = new GithubAPI(get_github_oauth)
203 # var repo = api.load_repo("privat/nit")
204 # assert repo != null
205 # var issue = api.load_issue(repo, 1)
206 # assert issue.title == "Doc"
207 fun load_issue
(repo
: Repo, number
: Int): nullable Issue do
208 var issue
= new Issue(self, repo
, number
)
209 return issue
.load_from_github
212 # Get the Github pull request #`number`.
214 # Returns `null` if the pull request cannot be found.
216 # var api = new GithubAPI(get_github_oauth)
217 # var repo = api.load_repo("privat/nit")
218 # assert repo != null
219 # var pull = api.load_pull(repo, 1)
220 # assert pull.title == "Doc"
221 # assert pull.user.login == "Morriar"
222 fun load_pull
(repo
: Repo, number
: Int): nullable PullRequest do
223 var pull
= new PullRequest(self, repo
, number
)
224 return pull
.load_from_github
227 # Get the Github label with `name`.
229 # Returns `null` if the label cannot be found.
231 # var api = new GithubAPI(get_github_oauth)
232 # var repo = api.load_repo("privat/nit")
233 # assert repo != null
234 # var labl = api.load_label(repo, "ok_will_merge")
235 # assert labl != null
236 fun load_label
(repo
: Repo, name
: String): nullable Label do
237 var labl
= new Label(self, repo
, name
)
238 return labl
.load_from_github
241 # Get the Github milestone with `id`.
243 # Returns `null` if the milestone cannot be found.
245 # var api = new GithubAPI(get_github_oauth)
246 # var repo = api.load_repo("privat/nit")
247 # assert repo != null
248 # var stone = api.load_milestone(repo, 4)
249 # assert stone.title == "v1.0prealpha"
250 fun load_milestone
(repo
: Repo, id
: Int): nullable Milestone do
251 var milestone
= new Milestone(self, repo
, id
)
252 return milestone
.load_from_github
256 # Something returned by the Github API.
258 # Mainly a Nit wrapper around a JSON objet.
259 abstract class GithubEntity
261 # Github API instance.
264 # FIXME constructor should be private
266 # Key used to access this entity from Github api base.
267 fun key
: String is abstract
269 # JSON representation of `self`.
271 # This is the same json structure than used by Github API.
272 var json
: JsonObject is noinit
, protected writable
274 # Load `json` from Github API.
275 private fun load_from_github
: nullable SELF do
276 json
= api
.load_from_github
(key
)
277 if api
.was_error
then return null
281 redef fun to_s
do return json
.to_json
286 # Should be accessed from `GithubAPI::load_user`.
288 # See <https://developer.github.com/v3/users/>.
292 redef var key
is lazy
do return "users/{login}"
297 # Init `self` from a `json` object.
298 init from_json
(api
: GithubAPI, json
: JsonObject) do
299 init(api
, json
["login"].to_s
)
303 # Github User page url.
304 fun html_url
: String do return json
["html_url"].to_s
306 # Avatar image url for this user.
307 fun avatar_url
: String do return json
["avatar_url"].to_s
310 # A Github repository.
312 # Should be accessed from `GithubAPI::load_repo`.
314 # See <https://developer.github.com/v3/repos/>.
318 redef var key
is lazy
do return "repos/{full_name}"
320 # Repo full name on Github.
321 var full_name
: String
323 # Init `self` from a `json` object.
324 init from_json
(api
: GithubAPI, json
: JsonObject) do
325 init(api
, json
["full_name"].to_s
)
329 # Repo short name on Github.
330 fun name
: String do return json
["name"].to_s
332 # Github User page url.
333 fun html_url
: String do return json
["html_url"].to_s
335 # Get the repo owner.
337 return new User.from_json
(api
, json
["owner"].as(JsonObject))
340 # List of branches associated with their names.
341 fun branches
: Map[String, Branch] do
342 api
.message
(1, "Get branches for {full_name}")
343 var array
= api
.get
("repos/{full_name}/branches")
344 var res
= new HashMap[String, Branch]
345 if not array
isa JsonArray then return res
347 if not obj
isa JsonObject then continue
348 var name
= obj
["name"].to_s
349 res
[name
] = new Branch.from_json
(api
, self, obj
)
354 # List of issues associated with their ids.
355 fun issues
: Map[Int, Issue] do
356 api
.message
(1, "Get issues for {full_name}")
357 var res
= new HashMap[Int, Issue]
358 var issue
= last_issue
359 if issue
== null then return res
360 res
[issue
.number
] = issue
361 while issue
.number
> 1 do
362 issue
= api
.load_issue
(self, issue
.number
- 1)
363 assert issue
isa Issue
364 res
[issue
.number
] = issue
369 # Get the last published issue.
370 fun last_issue
: nullable Issue do
371 var array
= api
.get
("repos/{full_name}/issues")
372 if not array
isa JsonArray then return null
373 if array
.is_empty
then return null
374 var obj
= array
.first
375 if not obj
isa JsonObject then return null
376 return new Issue.from_json
(api
, self, obj
)
379 # List of labels associated with their names.
380 fun labels
: Map[String, Label] do
381 api
.message
(1, "Get labels for {full_name}")
382 var array
= api
.get
("repos/{full_name}/labels")
383 var res
= new HashMap[String, Label]
384 if not array
isa JsonArray then return res
386 if not obj
isa JsonObject then continue
387 var name
= obj
["name"].to_s
388 res
[name
] = new Label.from_json
(api
, self, obj
)
393 # List of milestones associated with their ids.
394 fun milestones
: Map[Int, Milestone] do
395 api
.message
(1, "Get milestones for {full_name}")
396 var array
= api
.get
("repos/{full_name}/milestones")
397 var res
= new HashMap[Int, Milestone]
398 if array
isa JsonArray then
400 if not obj
isa JsonObject then continue
401 var number
= obj
["number"].as(Int)
402 res
[number
] = new Milestone.from_json
(api
, self, obj
)
408 # List of pull-requests associated with their ids.
410 # Implementation notes: because PR numbers are not consecutive,
411 # PR are loaded from pages.
412 # See: https://developer.github.com/v3/pulls/#list-pull-requests
413 fun pulls
: Map[Int, PullRequest] do
414 api
.message
(1, "Get pulls for {full_name}")
415 var res
= new HashMap[Int, PullRequest]
417 var array
= api
.get
("{key}/pulls?page={page}").as(JsonArray)
418 while not array
.is_empty
do
420 if not obj
isa JsonObject then continue
421 var number
= obj
["number"].as(Int)
422 res
[number
] = new PullRequest.from_json
(api
, self, obj
)
425 array
= api
.get
("{key}/pulls?page={page}").as(JsonArray)
430 # Repo default branch.
431 fun default_branch
: Branch do
432 var name
= json
["default_branch"].to_s
433 var branch
= api
.load_branch
(self, name
)
434 assert branch
isa Branch
439 # A `RepoEntity` is something contained in a `Repo`.
440 abstract class RepoEntity
443 # Repo that contains `self`.
446 # Init `self` from a `json` object.
447 init from_json
(api
: GithubAPI, repo
: Repo, json
: JsonObject) do
456 # Should be accessed from `GithubAPI::load_branch`.
458 # See <https://developer.github.com/v3/repos/#list-branches>.
462 redef var key
is lazy
do return "{repo.key}/branches/{name}"
467 redef init from_json
(api
, repo
, json
) do
468 self.name
= json
["name"].to_s
472 # Get the last commit of `self`.
473 fun commit
: Commit do
474 return new Commit.from_json
(api
, repo
, json
["commit"].as(JsonObject))
477 # List all commits in `self`.
479 # This can be long depending on the branch size.
480 # Commit are returned in an unspecified order.
481 fun commits
: Array[Commit] do
482 var res
= new Array[Commit]
483 var done
= new HashSet[String]
484 var todos
= new Array[Commit]
486 while not todos
.is_empty
do
487 var commit
= todos
.pop
488 if done
.has
(commit
.sha
) then continue
491 for parent
in commit
.parents
do
501 # Should be accessed from `GithubAPI::load_commit`.
503 # See <https://developer.github.com/v3/commits/>.
507 redef var key
is lazy
do return "{repo.key}/commits/{sha}"
512 redef init from_json
(api
, repo
, json
) do
513 self.sha
= json
["sha"].to_s
517 # Parent commits of `self`.
518 fun parents
: Array[Commit] do
519 var res
= new Array[Commit]
520 var parents
= json
["parents"]
521 if not parents
isa JsonArray then return res
522 for obj
in parents
do
523 if not obj
isa JsonObject then continue
524 res
.add
(api
.load_commit
(repo
, obj
["sha"].to_s
).as(not null))
529 # Author of the commit.
530 fun author
: nullable User do
531 if not json
.has_key
("author") then return null
532 var user
= json
["author"]
533 if not user
isa JsonObject then return null
534 return new User.from_json
(api
, user
)
537 # Committer of the commit.
538 fun committer
: nullable User do
539 if not json
.has_key
("committer") then return null
540 var user
= json
["author"]
541 if not user
isa JsonObject then return null
542 return new User.from_json
(api
, user
)
545 # Authoring date as ISODate.
546 fun author_date
: ISODate do
547 var commit
= json
["commit"].as(JsonObject)
548 var author
= commit
["author"].as(JsonObject)
549 return new ISODate.from_string
(author
["date"].to_s
)
552 # Commit date as ISODate.
553 fun commit_date
: ISODate do
554 var commit
= json
["commit"].as(JsonObject)
555 var author
= commit
["committer"].as(JsonObject)
556 return new ISODate.from_string
(author
["date"].to_s
)
560 fun message
: String do return json
["commit"].as(JsonObject)["message"].to_s
565 # Should be accessed from `GithubAPI::load_issue`.
567 # See <https://developer.github.com/v3/issues/>.
571 redef var key
is lazy
do return "{repo.key}/issues/{number}"
576 redef init from_json
(api
, repo
, json
) do
577 self.number
= json
["number"].as(Int)
582 fun title
: String do return json
["title"].to_s
584 # User that created this issue.
586 return new User.from_json
(api
, json
["user"].as(JsonObject))
589 # List of labels on this issue associated to their names.
590 fun labels
: Map[String, Label] do
591 var res
= new HashMap[String, Label]
592 for obj
in json
["labels"].as(JsonArray) do
593 if not obj
isa JsonObject then continue
594 var name
= obj
["name"].to_s
595 res
[name
] = new Label.from_json
(api
, repo
, obj
)
600 # State of the issue on Github.
601 fun state
: String do return json
["state"].to_s
603 # Is the issue locked?
604 fun locked
: Bool do return json
["locked"].as(Bool)
606 # Assigned `User` (if any).
607 fun assignee
: nullable User do
608 var assignee
= json
["assignee"]
609 if not assignee
isa JsonObject then return null
610 return new User.from_json
(api
, assignee
)
613 # `Milestone` (if any).
614 fun milestone
: nullable Milestone do
615 var milestone
= json
["milestone"]
616 if not milestone
isa JsonObject then return null
617 return new Milestone.from_json
(api
, repo
, milestone
)
620 # Number of comments on this issue.
621 fun comments_count
: Int do return json
["comments"].to_s
.to_i
623 # Creation time in ISODate format.
624 fun created_at
: ISODate do
625 return new ISODate.from_string
(json
["created_at"].to_s
)
628 # Last update time in ISODate format (if any).
629 fun updated_at
: nullable ISODate do
630 var res
= json
["updated_at"]
631 if res
== null then return null
632 return new ISODate.from_string
(res
.to_s
)
635 # Close time in ISODate format (if any).
636 fun closed_at
: nullable ISODate do
637 var res
= json
["closed_at"]
638 if res
== null then return null
639 return new ISODate.from_string
(res
.to_s
)
642 # TODO link to pull request
644 # Full description of the issue.
645 fun body
: String do return json
["body"].to_s
647 # User that closed this issue (if any).
648 fun closed_by
: nullable User do
649 var closer
= json
["closed_by"]
650 if not closer
isa JsonObject then return null
651 return new User.from_json
(api
, closer
)
655 # A Github pull request.
657 # Should be accessed from `GithubAPI::load_pull`.
659 # PullRequest are basically Issues with more data.
660 # See <https://developer.github.com/v3/pulls/>.
664 redef var key
is lazy
do return "{repo.key}/pulls/{number}"
666 # Merge time in ISODate format (if any).
667 fun merged_at
: nullable ISODate do
668 var res
= json
["merged_at"]
669 if res
== null then return null
670 return new ISODate.from_string
(res
.to_s
)
674 fun merge_commit_sha
: String do return json
["merge_commit_sha"].to_s
676 # Count of comments made on the pull request diff.
677 fun review_comments
: Int do return json
["review_comments"].to_s
.to_i
679 # Pull request head (can be a commit SHA or a branch name).
681 var json
= json
["head"].as(JsonObject)
682 return new PullRef(api
, json
)
685 # Pull request base (can be a commit SHA or a branch name).
687 var json
= json
["base"].as(JsonObject)
688 return new PullRef(api
, json
)
691 # Is this pull request merged?
692 fun merged
: Bool do return json
["merged"].as(Bool)
694 # Is this pull request mergeable?
695 fun mergeable
: Bool do return json
["mergeable"].as(Bool)
697 # Mergeable state of this pull request.
699 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
700 fun mergeable_state
: Int do return json
["mergeable_state"].to_s
.to_i
702 # User that merged this pull request (if any).
703 fun merged_by
: nullable User do
704 var merger
= json
["merged_by"]
705 if not merger
isa JsonObject then return null
706 return new User.from_json
(api
, merger
)
709 # Count of commits in this pull request.
710 fun commits
: Int do return json
["commits"].to_s
.to_i
713 fun additions
: Int do return json
["additions"].to_s
.to_i
715 # Deleted line count.
716 fun deletions
: Int do return json
["deletions"].to_s
.to_i
718 # Changed files count.
719 fun changed_files
: Int do return json
["changed_files"].to_s
.to_i
722 # A pull request reference (used for head and base).
725 # Api instance that maintains self.
728 # JSON representation.
731 # Label pointed by `self`.
732 fun labl
: String do return json
["label"].to_s
734 # Reference pointed by `self`.
735 fun ref
: String do return json
["ref"].to_s
737 # Commit SHA pointed by `self`.
738 fun sha
: String do return json
["sha"].to_s
740 # User pointed by `self`.
742 return new User.from_json
(api
, json
["user"].as(JsonObject))
745 # Repo pointed by `self`.
747 return new Repo.from_json
(api
, json
["repo"].as(JsonObject))
753 # Should be accessed from `GithubAPI::load_label`.
755 # See <https://developer.github.com/v3/issues/labels/>.
759 redef var key
is lazy
do return "{repo.key}/labels/{name}"
764 redef init from_json
(api
, repo
, json
) do
765 self.name
= json
["name"].to_s
770 fun color
: String do return json
["color"].to_s
773 # A Github milestone.
775 # Should be accessed from `GithubAPI::load_milestone`.
777 # See <https://developer.github.com/v3/issues/milestones/>.
781 redef var key
is lazy
do return "{repo.key}/milestones/{number}"
783 # The milestone id on Github.
786 redef init from_json
(api
, repo
, json
) do
788 self.number
= json
["number"].as(Int)
792 fun title
: String do return json
["title"].to_s
794 # Milestone long description.
795 fun description
: String do return json
["description"].to_s
797 # Count of opened issues linked to this milestone.
798 fun open_issues
: Int do return json
["open_issues"].to_s
.to_i
800 # Count of closed issues linked to this milestone.
801 fun closed_issues
: Int do return json
["closed_issues"].to_s
.to_i
804 fun state
: String do return json
["state"].to_s
806 # Creation time in ISODate format.
807 fun created_at
: ISODate do
808 return new ISODate.from_string
(json
["created_at"].to_s
)
811 # User that created this milestone.
813 return new User.from_json
(api
, json
["creator"].as(JsonObject))
816 # Due time in ISODate format (if any).
817 fun due_on
: nullable ISODate do
818 var res
= json
["updated_at"]
819 if res
== null then return null
820 return new ISODate.from_string
(res
.to_s
)
823 # Update time in ISODate format (if any).
824 fun updated_at
: nullable ISODate do
825 var res
= json
["updated_at"]
826 if res
== null then return null
827 return new ISODate.from_string
(res
.to_s
)
830 # Close time in ISODate format (if any).
831 fun closed_at
: nullable ISODate do
832 var res
= json
["closed_at"]
833 if res
== null then return null
834 return new ISODate.from_string
(res
.to_s
)