github/api: return an empty list instead of crashing for issues without labels
[nit.git] / lib / github / api.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 # Nit object oriented interface to Github api.
16 #
17 # This modules reifies Github API elements as Nit classes.
18 #
19 # For most use-cases you need to use the `GithubAPI` client.
20 module api
21
22 import github_curl
23
24 # Interface to Github REST API.
25 #
26 # Used by all `GithubEntity` to perform requests.
27 #
28 # Usage:
29 #
30 # ~~~
31 # # Get Github authentification token.
32 # var token = get_github_oauth
33 # assert not token.is_empty
34 #
35 # # Init the client.
36 # var api = new GithubAPI(token)
37 # ~~~
38 #
39 # The API client allows to get Github API entities:
40 #
41 # ~~~
42 # var repo = api.load_repo("privat/nit")
43 # assert repo != null
44 # assert repo.name == "nit"
45 #
46 # var user = api.load_user("Morriar")
47 # assert user != null
48 # assert user.login == "Morriar"
49 # ~~~
50 class GithubAPI
51
52 # Github API OAuth token.
53 #
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.
57 var auth: String
58
59 # Github API base url.
60 #
61 # Default is `https://api.github.com` and should not be changed.
62 var api_url = "https://api.github.com"
63
64 # User agent used for HTTP requests.
65 #
66 # Default is `nit_github_api`.
67 #
68 # See <https://developer.github.com/v3/#user-agent-required>
69 var user_agent = "nit_github_api"
70
71 # Curl instance.
72 #
73 # Internal Curl instance used to perform API calls.
74 private var ghcurl: GithubCurl is noinit
75
76 # Verbosity level.
77 #
78 # * `0`: only errors (default)
79 # * `1`: verbose
80 var verbose_lvl = 0 is public writable
81
82 init do
83 ghcurl = new GithubCurl(auth, user_agent)
84 end
85
86 # Execute a GET request on Github API.
87 #
88 # This method returns raw json data.
89 # See other `load_*` methods to use more expressive types.
90 #
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"
95 #
96 # Returns `null` in case of `error`.
97 #
98 # obj = api.get("foo/bar/baz")
99 # assert obj == null
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
109 last_error = res
110 was_error = true
111 return null
112 end
113 was_error = false
114 return res
115 end
116
117 # Display a message depending on `verbose_lvl`.
118 fun message(lvl: Int, message: String) do
119 if lvl <= verbose_lvl then print message
120 end
121
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")
126 end
127
128 # Last error occured during Github API communications.
129 var last_error: nullable Error = null is public writable
130
131 # Does the last request provoqued an error?
132 var was_error = false is protected writable
133
134 # Load the json object from Github.
135 # See `GithubEntity::load_from_github`.
136 protected fun load_from_github(key: String): JsonObject do
137 message(1, "Get {key} (github)")
138 var res = get(key)
139 if was_error then return new JsonObject
140 return res.as(JsonObject)
141 end
142
143 # Get the Github user with `login`.
144 #
145 # Returns `null` if the user cannot be found.
146 #
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
153 end
154
155 # Get the Github repo with `full_name`.
156 #
157 # Returns `null` if the repo cannot be found.
158 #
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
167 end
168
169 # Get the Github branch with `name`.
170 #
171 # Returns `null` if the branch cannot be found.
172 #
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
182 end
183
184 # Get the Github commit with `sha`.
185 #
186 # Returns `null` if the commit cannot be found.
187 #
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
196 end
197
198 # Get the Github issue #`number`.
199 #
200 # Returns `null` if the issue cannot be found.
201 #
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
210 end
211
212 # Get the Github pull request #`number`.
213 #
214 # Returns `null` if the pull request cannot be found.
215 #
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
225 end
226
227 # Get the Github label with `name`.
228 #
229 # Returns `null` if the label cannot be found.
230 #
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
239 end
240
241 # Get the Github milestone with `id`.
242 #
243 # Returns `null` if the milestone cannot be found.
244 #
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
253 end
254
255 # Get the Github issue event with `id`.
256 #
257 # Returns `null` if the event cannot be found.
258 #
259 # var api = new GithubAPI(get_github_oauth)
260 # var repo = api.load_repo("privat/nit")
261 # assert repo isa Repo
262 # var event = api.load_issue_event(repo, 199674194)
263 # assert event.actor.login == "privat"
264 # assert event.event == "labeled"
265 # assert event.labl.name == "need_review"
266 # assert event.issue.number == 945
267 fun load_issue_event(repo: Repo, id: Int): nullable IssueEvent do
268 var event = new IssueEvent(self, repo, id)
269 return event.load_from_github
270 end
271
272 # Get the Github commit comment with `id`.
273 #
274 # Returns `null` if the comment cannot be found.
275 #
276 # var api = new GithubAPI(get_github_oauth)
277 # var repo = api.load_repo("privat/nit")
278 # assert repo != null
279 # var comment = api.load_commit_comment(repo, 8982707)
280 # assert comment.user.login == "Morriar"
281 # assert comment.body == "For testing purposes..."
282 # assert comment.commit.sha == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
283 fun load_commit_comment(repo: Repo, id: Int): nullable CommitComment do
284 var comment = new CommitComment(self, repo, id)
285 return comment.load_from_github
286 end
287
288 # Get the Github issue comment with `id`.
289 #
290 # Returns `null` if the comment cannot be found.
291 #
292 # var api = new GithubAPI(get_github_oauth)
293 # var repo = api.load_repo("privat/nit")
294 # assert repo != null
295 # var comment = api.load_issue_comment(repo, 6020149)
296 # assert comment.user.login == "privat"
297 # assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
298 # assert comment.issue.number == 10
299 fun load_issue_comment(repo: Repo, id: Int): nullable IssueComment do
300 var comment = new IssueComment(self, repo, id)
301 return comment.load_from_github
302 end
303
304 # Get the Github diff comment with `id`.
305 #
306 # Returns `null` if the comment cannot be found.
307 #
308 # var api = new GithubAPI(get_github_oauth)
309 # var repo = api.load_repo("privat/nit")
310 # assert repo != null
311 # var comment = api.load_review_comment(repo, 21010363)
312 # assert comment.path == "src/modelize/modelize_property.nit"
313 # assert comment.original_position == 26
314 # assert comment.pull.number == 945
315 fun load_review_comment(repo: Repo, id: Int): nullable ReviewComment do
316 var comment = new ReviewComment(self, repo, id)
317 return comment.load_from_github
318 end
319 end
320
321 # Something returned by the Github API.
322 #
323 # Mainly a Nit wrapper around a JSON objet.
324 abstract class GithubEntity
325
326 # Github API instance.
327 var api: GithubAPI
328
329 # FIXME constructor should be private
330
331 # Key used to access this entity from Github api base.
332 fun key: String is abstract
333
334 # JSON representation of `self`.
335 #
336 # This is the same json structure than used by Github API.
337 var json: JsonObject is noinit, protected writable
338
339 # Load `json` from Github API.
340 private fun load_from_github: nullable SELF do
341 json = api.load_from_github(key)
342 if api.was_error then return null
343 return self
344 end
345
346 redef fun to_s do return json.to_json
347 end
348
349 # A Github user.
350 #
351 # Should be accessed from `GithubAPI::load_user`.
352 #
353 # See <https://developer.github.com/v3/users/>.
354 class User
355 super GithubEntity
356
357 redef var key is lazy do return "users/{login}"
358
359 # Github login.
360 var login: String
361
362 # Init `self` from a `json` object.
363 init from_json(api: GithubAPI, json: JsonObject) do
364 init(api, json["login"].to_s)
365 self.json = json
366 end
367
368 # Github User page url.
369 fun html_url: String do return json["html_url"].to_s
370
371 # Avatar image url for this user.
372 fun avatar_url: String do return json["avatar_url"].to_s
373 end
374
375 # A Github repository.
376 #
377 # Should be accessed from `GithubAPI::load_repo`.
378 #
379 # See <https://developer.github.com/v3/repos/>.
380 class Repo
381 super GithubEntity
382
383 redef var key is lazy do return "repos/{full_name}"
384
385 # Repo full name on Github.
386 var full_name: String
387
388 # Init `self` from a `json` object.
389 init from_json(api: GithubAPI, json: JsonObject) do
390 init(api, json["full_name"].to_s)
391 self.json = json
392 end
393
394 # Repo short name on Github.
395 fun name: String do return json["name"].to_s
396
397 # Github User page url.
398 fun html_url: String do return json["html_url"].to_s
399
400 # Get the repo owner.
401 fun owner: User do
402 return new User.from_json(api, json["owner"].as(JsonObject))
403 end
404
405 # List of branches associated with their names.
406 fun branches: Map[String, Branch] do
407 api.message(1, "Get branches for {full_name}")
408 var array = api.get("repos/{full_name}/branches")
409 var res = new HashMap[String, Branch]
410 if not array isa JsonArray then return res
411 for obj in array do
412 if not obj isa JsonObject then continue
413 var name = obj["name"].to_s
414 res[name] = new Branch.from_json(api, self, obj)
415 end
416 return res
417 end
418
419 # List of issues associated with their ids.
420 fun issues: Map[Int, Issue] do
421 api.message(1, "Get issues for {full_name}")
422 var res = new HashMap[Int, Issue]
423 var issue = last_issue
424 if issue == null then return res
425 res[issue.number] = issue
426 while issue.number > 1 do
427 issue = api.load_issue(self, issue.number - 1)
428 assert issue isa Issue
429 res[issue.number] = issue
430 end
431 return res
432 end
433
434 # Get the last published issue.
435 fun last_issue: nullable Issue do
436 var array = api.get("repos/{full_name}/issues")
437 if not array isa JsonArray then return null
438 if array.is_empty then return null
439 var obj = array.first
440 if not obj isa JsonObject then return null
441 return new Issue.from_json(api, self, obj)
442 end
443
444 # List of labels associated with their names.
445 fun labels: Map[String, Label] do
446 api.message(1, "Get labels for {full_name}")
447 var array = api.get("repos/{full_name}/labels")
448 var res = new HashMap[String, Label]
449 if not array isa JsonArray then return res
450 for obj in array do
451 if not obj isa JsonObject then continue
452 var name = obj["name"].to_s
453 res[name] = new Label.from_json(api, self, obj)
454 end
455 return res
456 end
457
458 # List of milestones associated with their ids.
459 fun milestones: Map[Int, Milestone] do
460 api.message(1, "Get milestones for {full_name}")
461 var array = api.get("repos/{full_name}/milestones")
462 var res = new HashMap[Int, Milestone]
463 if array isa JsonArray then
464 for obj in array do
465 if not obj isa JsonObject then continue
466 var number = obj["number"].as(Int)
467 res[number] = new Milestone.from_json(api, self, obj)
468 end
469 end
470 return res
471 end
472
473 # List of pull-requests associated with their ids.
474 #
475 # Implementation notes: because PR numbers are not consecutive,
476 # PR are loaded from pages.
477 # See: https://developer.github.com/v3/pulls/#list-pull-requests
478 fun pulls: Map[Int, PullRequest] do
479 api.message(1, "Get pulls for {full_name}")
480 var res = new HashMap[Int, PullRequest]
481 var page = 1
482 var array = api.get("{key}/pulls?page={page}").as(JsonArray)
483 while not array.is_empty do
484 for obj in array do
485 if not obj isa JsonObject then continue
486 var number = obj["number"].as(Int)
487 res[number] = new PullRequest.from_json(api, self, obj)
488 end
489 page += 1
490 array = api.get("{key}/pulls?page={page}").as(JsonArray)
491 end
492 return res
493 end
494
495 # List of contributor related statistics.
496 fun contrib_stats: Array[ContributorStats] do
497 api.message(1, "Get contributor stats for {full_name}")
498 var res = new Array[ContributorStats]
499 var array = api.get("{key}/stats/contributors")
500 if array isa JsonArray then
501 for obj in array do
502 res.add new ContributorStats.from_json(api, obj.as(JsonObject))
503 end
504 end
505 return res
506 end
507
508 # Repo default branch.
509 fun default_branch: Branch do
510 var name = json["default_branch"].to_s
511 var branch = api.load_branch(self, name)
512 assert branch isa Branch
513 return branch
514 end
515 end
516
517 # A `RepoEntity` is something contained in a `Repo`.
518 abstract class RepoEntity
519 super GithubEntity
520
521 # Repo that contains `self`.
522 var repo: Repo
523
524 # Init `self` from a `json` object.
525 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
526 self.api = api
527 self.repo = repo
528 self.json = json
529 end
530 end
531
532 # A Github branch.
533 #
534 # Should be accessed from `GithubAPI::load_branch`.
535 #
536 # See <https://developer.github.com/v3/repos/#list-branches>.
537 class Branch
538 super RepoEntity
539
540 redef var key is lazy do return "{repo.key}/branches/{name}"
541
542 # Branch name.
543 var name: String
544
545 redef init from_json(api, repo, json) do
546 self.name = json["name"].to_s
547 super
548 end
549
550 # Get the last commit of `self`.
551 fun commit: Commit do
552 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
553 end
554
555 # List all commits in `self`.
556 #
557 # This can be long depending on the branch size.
558 # Commit are returned in an unspecified order.
559 fun commits: Array[Commit] do
560 var res = new Array[Commit]
561 var done = new HashSet[String]
562 var todos = new Array[Commit]
563 todos.add commit
564 while not todos.is_empty do
565 var commit = todos.pop
566 if done.has(commit.sha) then continue
567 done.add commit.sha
568 res.add commit
569 for parent in commit.parents do
570 todos.add parent
571 end
572 end
573 return res
574 end
575 end
576
577 # A Github commit.
578 #
579 # Should be accessed from `GithubAPI::load_commit`.
580 #
581 # See <https://developer.github.com/v3/commits/>.
582 class Commit
583 super RepoEntity
584
585 redef var key is lazy do return "{repo.key}/commits/{sha}"
586
587 # Commit SHA.
588 var sha: String
589
590 redef init from_json(api, repo, json) do
591 self.sha = json["sha"].to_s
592 super
593 end
594
595 # Parent commits of `self`.
596 fun parents: Array[Commit] do
597 var res = new Array[Commit]
598 var parents = json["parents"]
599 if not parents isa JsonArray then return res
600 for obj in parents do
601 if not obj isa JsonObject then continue
602 res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
603 end
604 return res
605 end
606
607 # Author of the commit.
608 fun author: nullable User do
609 if not json.has_key("author") then return null
610 var user = json["author"]
611 if not user isa JsonObject then return null
612 return new User.from_json(api, user)
613 end
614
615 # Committer of the commit.
616 fun committer: nullable User do
617 if not json.has_key("committer") then return null
618 var user = json["author"]
619 if not user isa JsonObject then return null
620 return new User.from_json(api, user)
621 end
622
623 # Authoring date as ISODate.
624 fun author_date: ISODate do
625 var commit = json["commit"].as(JsonObject)
626 var author = commit["author"].as(JsonObject)
627 return new ISODate.from_string(author["date"].to_s)
628 end
629
630 # Commit date as ISODate.
631 fun commit_date: ISODate do
632 var commit = json["commit"].as(JsonObject)
633 var author = commit["committer"].as(JsonObject)
634 return new ISODate.from_string(author["date"].to_s)
635 end
636
637 # List files staged in this commit.
638 fun files: Array[GithubFile] do
639 var res = new Array[GithubFile]
640 var files = json["files"]
641 if not files isa JsonArray then return res
642 for obj in files do
643 res.add(new GithubFile(obj.as(JsonObject)))
644 end
645 return res
646 end
647
648 # Commit message.
649 fun message: String do return json["commit"].as(JsonObject)["message"].to_s
650 end
651
652 # A Github issue.
653 #
654 # Should be accessed from `GithubAPI::load_issue`.
655 #
656 # See <https://developer.github.com/v3/issues/>.
657 class Issue
658 super RepoEntity
659
660 redef var key is lazy do return "{repo.key}/issues/{number}"
661
662 # Issue Github ID.
663 var number: Int
664
665 redef init from_json(api, repo, json) do
666 self.number = json["number"].as(Int)
667 super
668 end
669
670 # Issue title.
671 fun title: String do return json["title"].to_s
672
673 # User that created this issue.
674 fun user: User do
675 return new User.from_json(api, json["user"].as(JsonObject))
676 end
677
678 # List of labels on this issue associated to their names.
679 fun labels: Map[String, Label] do
680 var res = new HashMap[String, Label]
681 if not json.has_key("labels") then return res
682 for obj in json["labels"].as(JsonArray) do
683 if not obj isa JsonObject then continue
684 var name = obj["name"].to_s
685 res[name] = new Label.from_json(api, repo, obj)
686 end
687 return res
688 end
689
690 # State of the issue on Github.
691 fun state: String do return json["state"].to_s
692
693 # Is the issue locked?
694 fun locked: Bool do return json["locked"].as(Bool)
695
696 # Assigned `User` (if any).
697 fun assignee: nullable User do
698 var assignee = json["assignee"]
699 if not assignee isa JsonObject then return null
700 return new User.from_json(api, assignee)
701 end
702
703 # `Milestone` (if any).
704 fun milestone: nullable Milestone do
705 var milestone = json["milestone"]
706 if not milestone isa JsonObject then return null
707 return new Milestone.from_json(api, repo, milestone)
708 end
709
710 # List of comments made on this issue.
711 fun comments: Array[IssueComment] do
712 var res = new Array[IssueComment]
713 var count = comments_count
714 var page = 1
715 var array = api.get("{key}/comments?page={page}")
716 if not array isa JsonArray then
717 return res
718 end
719 while not array.is_empty and res.length < count do
720 for obj in array do
721 if not obj isa JsonObject then continue
722 var id = obj["id"].as(Int)
723 res.add(api.load_issue_comment(repo, id).as(not null))
724 end
725 page += 1
726 array = api.get("{key}/comments?page={page}").as(JsonArray)
727 end
728 return res
729 end
730
731 # Number of comments on this issue.
732 fun comments_count: Int do return json["comments"].to_s.to_i
733
734 # Creation time in ISODate format.
735 fun created_at: ISODate do
736 return new ISODate.from_string(json["created_at"].to_s)
737 end
738
739 # Last update time in ISODate format (if any).
740 fun updated_at: nullable ISODate do
741 var res = json["updated_at"]
742 if res == null then return null
743 return new ISODate.from_string(res.to_s)
744 end
745
746 # Close time in ISODate format (if any).
747 fun closed_at: nullable ISODate do
748 var res = json["closed_at"]
749 if res == null then return null
750 return new ISODate.from_string(res.to_s)
751 end
752
753 # TODO link to pull request
754
755 # Full description of the issue.
756 fun body: String do return json["body"].to_s
757
758 # List of events on this issue.
759 fun events: Array[IssueEvent] do
760 var res = new Array[IssueEvent]
761 var page = 1
762 var array = api.get("{key}/events?page={page}").as(JsonArray)
763 while not array.is_empty do
764 for obj in array do
765 if not obj isa JsonObject then continue
766 res.add new IssueEvent.from_json(api, repo, obj)
767 end
768 page += 1
769 array = api.get("{key}/events?page={page}").as(JsonArray)
770 end
771 return res
772 end
773
774 # User that closed this issue (if any).
775 fun closed_by: nullable User do
776 var closer = json["closed_by"]
777 if not closer isa JsonObject then return null
778 return new User.from_json(api, closer)
779 end
780 end
781
782 # A Github pull request.
783 #
784 # Should be accessed from `GithubAPI::load_pull`.
785 #
786 # PullRequest are basically Issues with more data.
787 # See <https://developer.github.com/v3/pulls/>.
788 class PullRequest
789 super Issue
790
791 redef var key is lazy do return "{repo.key}/pulls/{number}"
792
793 # Merge time in ISODate format (if any).
794 fun merged_at: nullable ISODate do
795 var res = json["merged_at"]
796 if res == null then return null
797 return new ISODate.from_string(res.to_s)
798 end
799
800 # Merge commit SHA.
801 fun merge_commit_sha: String do return json["merge_commit_sha"].to_s
802
803 # Count of comments made on the pull request diff.
804 fun review_comments: Int do return json["review_comments"].to_s.to_i
805
806 # Pull request head (can be a commit SHA or a branch name).
807 fun head: PullRef do
808 var json = json["head"].as(JsonObject)
809 return new PullRef(api, json)
810 end
811
812 # Pull request base (can be a commit SHA or a branch name).
813 fun base: PullRef do
814 var json = json["base"].as(JsonObject)
815 return new PullRef(api, json)
816 end
817
818 # Is this pull request merged?
819 fun merged: Bool do return json["merged"].as(Bool)
820
821 # Is this pull request mergeable?
822 fun mergeable: Bool do return json["mergeable"].as(Bool)
823
824 # Mergeable state of this pull request.
825 #
826 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
827 fun mergeable_state: Int do return json["mergeable_state"].to_s.to_i
828
829 # User that merged this pull request (if any).
830 fun merged_by: nullable User do
831 var merger = json["merged_by"]
832 if not merger isa JsonObject then return null
833 return new User.from_json(api, merger)
834 end
835
836 # Count of commits in this pull request.
837 fun commits: Int do return json["commits"].to_s.to_i
838
839 # Added line count.
840 fun additions: Int do return json["additions"].to_s.to_i
841
842 # Deleted line count.
843 fun deletions: Int do return json["deletions"].to_s.to_i
844
845 # Changed files count.
846 fun changed_files: Int do return json["changed_files"].to_s.to_i
847 end
848
849 # A pull request reference (used for head and base).
850 class PullRef
851
852 # Api instance that maintains self.
853 var api: GithubAPI
854
855 # JSON representation.
856 var json: JsonObject
857
858 # Label pointed by `self`.
859 fun labl: String do return json["label"].to_s
860
861 # Reference pointed by `self`.
862 fun ref: String do return json["ref"].to_s
863
864 # Commit SHA pointed by `self`.
865 fun sha: String do return json["sha"].to_s
866
867 # User pointed by `self`.
868 fun user: User do
869 return new User.from_json(api, json["user"].as(JsonObject))
870 end
871
872 # Repo pointed by `self`.
873 fun repo: Repo do
874 return new Repo.from_json(api, json["repo"].as(JsonObject))
875 end
876 end
877
878 # A Github label.
879 #
880 # Should be accessed from `GithubAPI::load_label`.
881 #
882 # See <https://developer.github.com/v3/issues/labels/>.
883 class Label
884 super RepoEntity
885
886 redef var key is lazy do return "{repo.key}/labels/{name}"
887
888 # Label name.
889 var name: String
890
891 redef init from_json(api, repo, json) do
892 self.name = json["name"].to_s
893 super
894 end
895
896 # Label color code.
897 fun color: String do return json["color"].to_s
898 end
899
900 # A Github milestone.
901 #
902 # Should be accessed from `GithubAPI::load_milestone`.
903 #
904 # See <https://developer.github.com/v3/issues/milestones/>.
905 class Milestone
906 super RepoEntity
907
908 redef var key is lazy do return "{repo.key}/milestones/{number}"
909
910 # The milestone id on Github.
911 var number: Int
912
913 redef init from_json(api, repo, json) do
914 super
915 self.number = json["number"].as(Int)
916 end
917
918 # Milestone title.
919 fun title: String do return json["title"].to_s
920
921 # Milestone long description.
922 fun description: String do return json["description"].to_s
923
924 # Count of opened issues linked to this milestone.
925 fun open_issues: Int do return json["open_issues"].to_s.to_i
926
927 # Count of closed issues linked to this milestone.
928 fun closed_issues: Int do return json["closed_issues"].to_s.to_i
929
930 # Milestone state.
931 fun state: String do return json["state"].to_s
932
933 # Creation time in ISODate format.
934 fun created_at: ISODate do
935 return new ISODate.from_string(json["created_at"].to_s)
936 end
937
938 # User that created this milestone.
939 fun creator: User do
940 return new User.from_json(api, json["creator"].as(JsonObject))
941 end
942
943 # Due time in ISODate format (if any).
944 fun due_on: nullable ISODate do
945 var res = json["updated_at"]
946 if res == null then return null
947 return new ISODate.from_string(res.to_s)
948 end
949
950 # Update time in ISODate format (if any).
951 fun updated_at: nullable ISODate do
952 var res = json["updated_at"]
953 if res == null then return null
954 return new ISODate.from_string(res.to_s)
955 end
956
957 # Close time in ISODate format (if any).
958 fun closed_at: nullable ISODate do
959 var res = json["closed_at"]
960 if res == null then return null
961 return new ISODate.from_string(res.to_s)
962 end
963 end
964
965 # A Github comment
966 #
967 # There is two kinds of comments:
968 #
969 # * `CommitComment` are made on a commit page.
970 # * `IssueComment` are made on an issue or pull request page.
971 # * `ReviewComment` are made on the diff associated to a pull request.
972 abstract class Comment
973 super RepoEntity
974
975 # Identifier of this comment.
976 var id: Int
977
978 redef init from_json(api, repo, json) do
979 self.id = json["id"].as(Int)
980 super
981 end
982
983 # User that made this comment.
984 fun user: User do
985 return new User.from_json(api, json["user"].as(JsonObject))
986 end
987
988 # Creation time in ISODate format.
989 fun created_at: ISODate do
990 return new ISODate.from_string(json["created_at"].to_s)
991 end
992
993 # Last update time in ISODate format (if any).
994 fun updated_at: nullable ISODate do
995 if not json.has_key("updated_at") then return null
996 return new ISODate.from_string(json["updated_at"].to_s)
997 end
998
999 # Comment body text.
1000 fun body: String do return json["body"].to_s
1001 end
1002
1003 # A comment made on a commit.
1004 class CommitComment
1005 super Comment
1006
1007 redef var key is lazy do return "{repo.key}/comments/{id}"
1008
1009 # Commented commit.
1010 fun commit: Commit do
1011 return api.load_commit(repo, json["commit_id"].to_s).as(not null)
1012 end
1013
1014 # Position of the comment on the line.
1015 fun position: nullable String do
1016 if not json.has_key("position") then return null
1017 var res = json["position"]
1018 if res == null then return null
1019 return res.to_s
1020 end
1021
1022 # Line of the comment.
1023 fun line: nullable String do
1024 if not json.has_key("line") then return null
1025 var res = json["line"]
1026 if res == null then return null
1027 return res.to_s
1028 end
1029
1030 # Path of the commented file.
1031 fun path: String do return json["path"].to_s
1032 end
1033
1034 # Comments made on Github issue and pull request pages.
1035 #
1036 # Should be accessed from `GithubAPI::load_issue_comment`.
1037 #
1038 # See <https://developer.github.com/v3/issues/comments/>.
1039 class IssueComment
1040 super Comment
1041
1042 redef var key is lazy do return "{repo.key}/issues/comments/{id}"
1043
1044 # Issue that contains `self`.
1045 fun issue: Issue do
1046 var number = issue_url.split("/").last.to_i
1047 return api.load_issue(repo, number).as(not null)
1048 end
1049
1050 # Link to the issue document on API.
1051 fun issue_url: String do return json["issue_url"].to_s
1052 end
1053
1054 # Comments made on Github pull request diffs.
1055 #
1056 # Should be accessed from `GithubAPI::load_diff_comment`.
1057 #
1058 # See <https://developer.github.com/v3/pulls/comments/>.
1059 class ReviewComment
1060 super Comment
1061
1062 redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
1063
1064 # Pull request that contains `self`.
1065 fun pull: PullRequest do
1066 var number = pull_request_url.split("/").last.to_i
1067 return api.load_pull(repo, number).as(not null)
1068 end
1069
1070 # Link to the pull request on API.
1071 fun pull_request_url: String do return json["pull_request_url"].to_s
1072
1073 # Diff hunk.
1074 fun diff_hunk: String do return json["diff_hunk"].to_s
1075
1076 # Path of commented file.
1077 fun path: String do return json["path"].to_s
1078
1079 # Position of the comment on the file.
1080 fun position: Int do return json["position"].to_s.to_i
1081
1082 # Original position in the diff.
1083 fun original_position: Int do return json["original_position"].to_s.to_i
1084
1085 # Commit referenced by this comment.
1086 fun commit_id: String do return json["commit_id"].to_s
1087
1088 # Original commit id.
1089 fun original_commit_id: String do return json["original_commit_id"].to_s
1090 end
1091
1092 # An event that occurs on a Github `Issue`.
1093 #
1094 # Should be accessed from `GithubAPI::load_issue_event`.
1095 #
1096 # See <https://developer.github.com/v3/issues/events/>.
1097 class IssueEvent
1098 super RepoEntity
1099
1100 redef var key is lazy do return "{repo.key}/issues/events/{id}"
1101
1102 # Event id on Github.
1103 var id: Int
1104
1105 redef init from_json(api, repo, json) do
1106 self.id = json["id"].as(Int)
1107 super
1108 end
1109
1110 # Issue that contains `self`.
1111 fun issue: Issue do
1112 return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
1113 end
1114
1115 # User that initiated the event.
1116 fun actor: User do
1117 return new User.from_json(api, json["actor"].as(JsonObject))
1118 end
1119
1120 # Creation time in ISODate format.
1121 fun created_at: ISODate do
1122 return new ISODate.from_string(json["created_at"].to_s)
1123 end
1124
1125 # Event descriptor.
1126 fun event: String do return json["event"].to_s
1127
1128 # Commit linked to this event (if any).
1129 fun commit_id: nullable String do
1130 var res = json["commit_id"]
1131 if res == null then return null
1132 return res.to_s
1133 end
1134
1135 # Label linked to this event (if any).
1136 fun labl: nullable Label do
1137 var res = json["label"]
1138 if not res isa JsonObject then return null
1139 return new Label.from_json(api, repo, res)
1140 end
1141
1142 # User linked to this event (if any).
1143 fun assignee: nullable User do
1144 var res = json["assignee"]
1145 if not res isa JsonObject then return null
1146 return new User.from_json(api, res)
1147 end
1148
1149 # Milestone linked to this event (if any).
1150 fun milestone: nullable Milestone do
1151 var res = json["milestone"]
1152 if not res isa JsonObject then return null
1153 return new Milestone.from_json(api, repo, res)
1154 end
1155
1156 # Rename linked to this event (if any).
1157 fun rename: nullable RenameAction do
1158 var res = json["rename"]
1159 if res == null then return null
1160 return new RenameAction(res.as(JsonObject))
1161 end
1162 end
1163
1164 # A rename action maintains the name before and after a renaming action.
1165 class RenameAction
1166
1167 # JSON content.
1168 var json: JsonObject
1169
1170 # Name before renaming.
1171 fun from: String do return json["from"].to_s
1172
1173 # Name after renaming.
1174 fun to: String do return json["to"].to_s
1175 end
1176
1177 # Contributors list with additions, deletions, and commit counts.
1178 #
1179 # Should be accessed from `Repo::contrib_stats`.
1180 #
1181 # See <https://developer.github.com/v3/repos/statistics/>.
1182 class ContributorStats
1183 super Comparable
1184
1185 redef type OTHER: ContributorStats
1186
1187 # Github API client.
1188 var api: GithubAPI
1189
1190 # Json content.
1191 var json: JsonObject
1192
1193 # Init `self` from a `json` object.
1194 init from_json(api: GithubAPI, json: JsonObject) do
1195 self.api = api
1196 self.json = json
1197 end
1198
1199 # User these statistics are about.
1200 fun author: User do
1201 return new User.from_json(api, json["author"].as(JsonObject))
1202 end
1203
1204 # Total number of commit.
1205 fun total: Int do return json["total"].to_s.to_i
1206
1207 # Are of weeks of activity with detailed statistics.
1208 fun weeks: JsonArray do return json["weeks"].as(JsonArray)
1209
1210 # ContributorStats can be compared on the total amount of commits.
1211 redef fun <(o) do return total < o.total
1212 end
1213
1214 # A Github file representation.
1215 #
1216 # Mostly a wrapper around a json object.
1217 class GithubFile
1218
1219 # Json content.
1220 var json: JsonObject
1221
1222 # File name.
1223 fun filename: String do return json["filename"].to_s
1224 end