github/api: move `html_url` to GithubEntity since it's shared by all of them
[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
348 # Github page url.
349 fun html_url: String do return json["html_url"].to_s
350 end
351
352 # A Github user.
353 #
354 # Should be accessed from `GithubAPI::load_user`.
355 #
356 # See <https://developer.github.com/v3/users/>.
357 class User
358 super GithubEntity
359
360 redef var key is lazy do return "users/{login}"
361
362 # Github login.
363 var login: String
364
365 # Init `self` from a `json` object.
366 init from_json(api: GithubAPI, json: JsonObject) do
367 init(api, json["login"].to_s)
368 self.json = json
369 end
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 # Get the repo owner.
398 fun owner: User do
399 return new User.from_json(api, json["owner"].as(JsonObject))
400 end
401
402 # List of branches associated with their names.
403 fun branches: Map[String, Branch] do
404 api.message(1, "Get branches for {full_name}")
405 var array = api.get("repos/{full_name}/branches")
406 var res = new HashMap[String, Branch]
407 if not array isa JsonArray then return res
408 for obj in array do
409 if not obj isa JsonObject then continue
410 var name = obj["name"].to_s
411 res[name] = new Branch.from_json(api, self, obj)
412 end
413 return res
414 end
415
416 # List of issues associated with their ids.
417 fun issues: Map[Int, Issue] do
418 api.message(1, "Get issues for {full_name}")
419 var res = new HashMap[Int, Issue]
420 var issue = last_issue
421 if issue == null then return res
422 res[issue.number] = issue
423 while issue.number > 1 do
424 issue = api.load_issue(self, issue.number - 1)
425 assert issue isa Issue
426 res[issue.number] = issue
427 end
428 return res
429 end
430
431 # Get the last published issue.
432 fun last_issue: nullable Issue do
433 var array = api.get("repos/{full_name}/issues")
434 if not array isa JsonArray then return null
435 if array.is_empty then return null
436 var obj = array.first
437 if not obj isa JsonObject then return null
438 return new Issue.from_json(api, self, obj)
439 end
440
441 # List of labels associated with their names.
442 fun labels: Map[String, Label] do
443 api.message(1, "Get labels for {full_name}")
444 var array = api.get("repos/{full_name}/labels")
445 var res = new HashMap[String, Label]
446 if not array isa JsonArray then return res
447 for obj in array do
448 if not obj isa JsonObject then continue
449 var name = obj["name"].to_s
450 res[name] = new Label.from_json(api, self, obj)
451 end
452 return res
453 end
454
455 # List of milestones associated with their ids.
456 fun milestones: Map[Int, Milestone] do
457 api.message(1, "Get milestones for {full_name}")
458 var array = api.get("repos/{full_name}/milestones")
459 var res = new HashMap[Int, Milestone]
460 if array isa JsonArray then
461 for obj in array do
462 if not obj isa JsonObject then continue
463 var number = obj["number"].as(Int)
464 res[number] = new Milestone.from_json(api, self, obj)
465 end
466 end
467 return res
468 end
469
470 # List of pull-requests associated with their ids.
471 #
472 # Implementation notes: because PR numbers are not consecutive,
473 # PR are loaded from pages.
474 # See: https://developer.github.com/v3/pulls/#list-pull-requests
475 fun pulls: Map[Int, PullRequest] do
476 api.message(1, "Get pulls for {full_name}")
477 var res = new HashMap[Int, PullRequest]
478 var page = 1
479 var array = api.get("{key}/pulls?page={page}").as(JsonArray)
480 while not array.is_empty do
481 for obj in array do
482 if not obj isa JsonObject then continue
483 var number = obj["number"].as(Int)
484 res[number] = new PullRequest.from_json(api, self, obj)
485 end
486 page += 1
487 array = api.get("{key}/pulls?page={page}").as(JsonArray)
488 end
489 return res
490 end
491
492 # List of contributor related statistics.
493 fun contrib_stats: Array[ContributorStats] do
494 api.message(1, "Get contributor stats for {full_name}")
495 var res = new Array[ContributorStats]
496 var array = api.get("{key}/stats/contributors")
497 if array isa JsonArray then
498 for obj in array do
499 res.add new ContributorStats.from_json(api, obj.as(JsonObject))
500 end
501 end
502 return res
503 end
504
505 # Repo default branch.
506 fun default_branch: Branch do
507 var name = json["default_branch"].to_s
508 var branch = api.load_branch(self, name)
509 assert branch isa Branch
510 return branch
511 end
512 end
513
514 # A `RepoEntity` is something contained in a `Repo`.
515 abstract class RepoEntity
516 super GithubEntity
517
518 # Repo that contains `self`.
519 var repo: Repo
520
521 # Init `self` from a `json` object.
522 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
523 self.api = api
524 self.repo = repo
525 self.json = json
526 end
527 end
528
529 # A Github branch.
530 #
531 # Should be accessed from `GithubAPI::load_branch`.
532 #
533 # See <https://developer.github.com/v3/repos/#list-branches>.
534 class Branch
535 super RepoEntity
536
537 redef var key is lazy do return "{repo.key}/branches/{name}"
538
539 # Branch name.
540 var name: String
541
542 redef init from_json(api, repo, json) do
543 self.name = json["name"].to_s
544 super
545 end
546
547 # Get the last commit of `self`.
548 fun commit: Commit do
549 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
550 end
551
552 # List all commits in `self`.
553 #
554 # This can be long depending on the branch size.
555 # Commit are returned in an unspecified order.
556 fun commits: Array[Commit] do
557 var res = new Array[Commit]
558 var done = new HashSet[String]
559 var todos = new Array[Commit]
560 todos.add commit
561 while not todos.is_empty do
562 var commit = todos.pop
563 if done.has(commit.sha) then continue
564 done.add commit.sha
565 res.add commit
566 for parent in commit.parents do
567 todos.add parent
568 end
569 end
570 return res
571 end
572 end
573
574 # A Github commit.
575 #
576 # Should be accessed from `GithubAPI::load_commit`.
577 #
578 # See <https://developer.github.com/v3/commits/>.
579 class Commit
580 super RepoEntity
581
582 redef var key is lazy do return "{repo.key}/commits/{sha}"
583
584 # Commit SHA.
585 var sha: String
586
587 redef init from_json(api, repo, json) do
588 self.sha = json["sha"].to_s
589 super
590 end
591
592 # Parent commits of `self`.
593 fun parents: Array[Commit] do
594 var res = new Array[Commit]
595 var parents = json["parents"]
596 if not parents isa JsonArray then return res
597 for obj in parents do
598 if not obj isa JsonObject then continue
599 res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
600 end
601 return res
602 end
603
604 # Author of the commit.
605 fun author: nullable User do
606 if not json.has_key("author") then return null
607 var user = json["author"]
608 if not user isa JsonObject then return null
609 return new User.from_json(api, user)
610 end
611
612 # Committer of the commit.
613 fun committer: nullable User do
614 if not json.has_key("committer") then return null
615 var user = json["author"]
616 if not user isa JsonObject then return null
617 return new User.from_json(api, user)
618 end
619
620 # Authoring date as ISODate.
621 fun author_date: ISODate do
622 var commit = json["commit"].as(JsonObject)
623 var author = commit["author"].as(JsonObject)
624 return new ISODate.from_string(author["date"].to_s)
625 end
626
627 # Commit date as ISODate.
628 fun commit_date: ISODate do
629 var commit = json["commit"].as(JsonObject)
630 var author = commit["committer"].as(JsonObject)
631 return new ISODate.from_string(author["date"].to_s)
632 end
633
634 # List files staged in this commit.
635 fun files: Array[GithubFile] do
636 var res = new Array[GithubFile]
637 var files = json["files"]
638 if not files isa JsonArray then return res
639 for obj in files do
640 res.add(new GithubFile(obj.as(JsonObject)))
641 end
642 return res
643 end
644
645 # Commit message.
646 fun message: String do return json["commit"].as(JsonObject)["message"].to_s
647 end
648
649 # A Github issue.
650 #
651 # Should be accessed from `GithubAPI::load_issue`.
652 #
653 # See <https://developer.github.com/v3/issues/>.
654 class Issue
655 super RepoEntity
656
657 redef var key is lazy do return "{repo.key}/issues/{number}"
658
659 # Issue Github ID.
660 var number: Int
661
662 redef init from_json(api, repo, json) do
663 self.number = json["number"].as(Int)
664 super
665 end
666
667 # Issue title.
668 fun title: String do return json["title"].to_s
669
670 # User that created this issue.
671 fun user: User do
672 return new User.from_json(api, json["user"].as(JsonObject))
673 end
674
675 # List of labels on this issue associated to their names.
676 fun labels: Map[String, Label] do
677 var res = new HashMap[String, Label]
678 if not json.has_key("labels") then return res
679 for obj in json["labels"].as(JsonArray) do
680 if not obj isa JsonObject then continue
681 var name = obj["name"].to_s
682 res[name] = new Label.from_json(api, repo, obj)
683 end
684 return res
685 end
686
687 # State of the issue on Github.
688 fun state: String do return json["state"].to_s
689
690 # Is the issue locked?
691 fun locked: Bool do return json["locked"].as(Bool)
692
693 # Assigned `User` (if any).
694 fun assignee: nullable User do
695 var assignee = json["assignee"]
696 if not assignee isa JsonObject then return null
697 return new User.from_json(api, assignee)
698 end
699
700 # `Milestone` (if any).
701 fun milestone: nullable Milestone do
702 var milestone = json["milestone"]
703 if not milestone isa JsonObject then return null
704 return new Milestone.from_json(api, repo, milestone)
705 end
706
707 # List of comments made on this issue.
708 fun comments: Array[IssueComment] do
709 var res = new Array[IssueComment]
710 var count = comments_count
711 var page = 1
712 var array = api.get("{key}/comments?page={page}")
713 if not array isa JsonArray then
714 return res
715 end
716 while not array.is_empty and res.length < count do
717 for obj in array do
718 if not obj isa JsonObject then continue
719 var id = obj["id"].as(Int)
720 res.add(api.load_issue_comment(repo, id).as(not null))
721 end
722 page += 1
723 array = api.get("{key}/comments?page={page}").as(JsonArray)
724 end
725 return res
726 end
727
728 # Number of comments on this issue.
729 fun comments_count: Int do return json["comments"].to_s.to_i
730
731 # Creation time in ISODate format.
732 fun created_at: ISODate do
733 return new ISODate.from_string(json["created_at"].to_s)
734 end
735
736 # Last update time in ISODate format (if any).
737 fun updated_at: nullable ISODate do
738 var res = json["updated_at"]
739 if res == null then return null
740 return new ISODate.from_string(res.to_s)
741 end
742
743 # Close time in ISODate format (if any).
744 fun closed_at: nullable ISODate do
745 var res = json["closed_at"]
746 if res == null then return null
747 return new ISODate.from_string(res.to_s)
748 end
749
750 # TODO link to pull request
751
752 # Full description of the issue.
753 fun body: String do return json["body"].to_s
754
755 # List of events on this issue.
756 fun events: Array[IssueEvent] do
757 var res = new Array[IssueEvent]
758 var page = 1
759 var array = api.get("{key}/events?page={page}").as(JsonArray)
760 while not array.is_empty do
761 for obj in array do
762 if not obj isa JsonObject then continue
763 res.add new IssueEvent.from_json(api, repo, obj)
764 end
765 page += 1
766 array = api.get("{key}/events?page={page}").as(JsonArray)
767 end
768 return res
769 end
770
771 # User that closed this issue (if any).
772 fun closed_by: nullable User do
773 var closer = json["closed_by"]
774 if not closer isa JsonObject then return null
775 return new User.from_json(api, closer)
776 end
777 end
778
779 # A Github pull request.
780 #
781 # Should be accessed from `GithubAPI::load_pull`.
782 #
783 # PullRequest are basically Issues with more data.
784 # See <https://developer.github.com/v3/pulls/>.
785 class PullRequest
786 super Issue
787
788 redef var key is lazy do return "{repo.key}/pulls/{number}"
789
790 # Merge time in ISODate format (if any).
791 fun merged_at: nullable ISODate do
792 var res = json["merged_at"]
793 if res == null then return null
794 return new ISODate.from_string(res.to_s)
795 end
796
797 # Merge commit SHA.
798 fun merge_commit_sha: String do return json["merge_commit_sha"].to_s
799
800 # Count of comments made on the pull request diff.
801 fun review_comments: Int do return json["review_comments"].to_s.to_i
802
803 # Pull request head (can be a commit SHA or a branch name).
804 fun head: PullRef do
805 var json = json["head"].as(JsonObject)
806 return new PullRef(api, json)
807 end
808
809 # Pull request base (can be a commit SHA or a branch name).
810 fun base: PullRef do
811 var json = json["base"].as(JsonObject)
812 return new PullRef(api, json)
813 end
814
815 # Is this pull request merged?
816 fun merged: Bool do return json["merged"].as(Bool)
817
818 # Is this pull request mergeable?
819 fun mergeable: Bool do return json["mergeable"].as(Bool)
820
821 # Mergeable state of this pull request.
822 #
823 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
824 fun mergeable_state: Int do return json["mergeable_state"].to_s.to_i
825
826 # User that merged this pull request (if any).
827 fun merged_by: nullable User do
828 var merger = json["merged_by"]
829 if not merger isa JsonObject then return null
830 return new User.from_json(api, merger)
831 end
832
833 # Count of commits in this pull request.
834 fun commits: Int do return json["commits"].to_s.to_i
835
836 # Added line count.
837 fun additions: Int do return json["additions"].to_s.to_i
838
839 # Deleted line count.
840 fun deletions: Int do return json["deletions"].to_s.to_i
841
842 # Changed files count.
843 fun changed_files: Int do return json["changed_files"].to_s.to_i
844 end
845
846 # A pull request reference (used for head and base).
847 class PullRef
848
849 # Api instance that maintains self.
850 var api: GithubAPI
851
852 # JSON representation.
853 var json: JsonObject
854
855 # Label pointed by `self`.
856 fun labl: String do return json["label"].to_s
857
858 # Reference pointed by `self`.
859 fun ref: String do return json["ref"].to_s
860
861 # Commit SHA pointed by `self`.
862 fun sha: String do return json["sha"].to_s
863
864 # User pointed by `self`.
865 fun user: User do
866 return new User.from_json(api, json["user"].as(JsonObject))
867 end
868
869 # Repo pointed by `self`.
870 fun repo: Repo do
871 return new Repo.from_json(api, json["repo"].as(JsonObject))
872 end
873 end
874
875 # A Github label.
876 #
877 # Should be accessed from `GithubAPI::load_label`.
878 #
879 # See <https://developer.github.com/v3/issues/labels/>.
880 class Label
881 super RepoEntity
882
883 redef var key is lazy do return "{repo.key}/labels/{name}"
884
885 # Label name.
886 var name: String
887
888 redef init from_json(api, repo, json) do
889 self.name = json["name"].to_s
890 super
891 end
892
893 # Label color code.
894 fun color: String do return json["color"].to_s
895 end
896
897 # A Github milestone.
898 #
899 # Should be accessed from `GithubAPI::load_milestone`.
900 #
901 # See <https://developer.github.com/v3/issues/milestones/>.
902 class Milestone
903 super RepoEntity
904
905 redef var key is lazy do return "{repo.key}/milestones/{number}"
906
907 # The milestone id on Github.
908 var number: Int
909
910 redef init from_json(api, repo, json) do
911 super
912 self.number = json["number"].as(Int)
913 end
914
915 # Milestone title.
916 fun title: String do return json["title"].to_s
917
918 # Milestone long description.
919 fun description: String do return json["description"].to_s
920
921 # Count of opened issues linked to this milestone.
922 fun open_issues: Int do return json["open_issues"].to_s.to_i
923
924 # Count of closed issues linked to this milestone.
925 fun closed_issues: Int do return json["closed_issues"].to_s.to_i
926
927 # Milestone state.
928 fun state: String do return json["state"].to_s
929
930 # Creation time in ISODate format.
931 fun created_at: ISODate do
932 return new ISODate.from_string(json["created_at"].to_s)
933 end
934
935 # User that created this milestone.
936 fun creator: User do
937 return new User.from_json(api, json["creator"].as(JsonObject))
938 end
939
940 # Due time in ISODate format (if any).
941 fun due_on: nullable ISODate do
942 var res = json["updated_at"]
943 if res == null then return null
944 return new ISODate.from_string(res.to_s)
945 end
946
947 # Update time in ISODate format (if any).
948 fun updated_at: nullable ISODate do
949 var res = json["updated_at"]
950 if res == null then return null
951 return new ISODate.from_string(res.to_s)
952 end
953
954 # Close time in ISODate format (if any).
955 fun closed_at: nullable ISODate do
956 var res = json["closed_at"]
957 if res == null then return null
958 return new ISODate.from_string(res.to_s)
959 end
960 end
961
962 # A Github comment
963 #
964 # There is two kinds of comments:
965 #
966 # * `CommitComment` are made on a commit page.
967 # * `IssueComment` are made on an issue or pull request page.
968 # * `ReviewComment` are made on the diff associated to a pull request.
969 abstract class Comment
970 super RepoEntity
971
972 # Identifier of this comment.
973 var id: Int
974
975 redef init from_json(api, repo, json) do
976 self.id = json["id"].as(Int)
977 super
978 end
979
980 # User that made this comment.
981 fun user: User do
982 return new User.from_json(api, json["user"].as(JsonObject))
983 end
984
985 # Creation time in ISODate format.
986 fun created_at: ISODate do
987 return new ISODate.from_string(json["created_at"].to_s)
988 end
989
990 # Last update time in ISODate format (if any).
991 fun updated_at: nullable ISODate do
992 if not json.has_key("updated_at") then return null
993 return new ISODate.from_string(json["updated_at"].to_s)
994 end
995
996 # Comment body text.
997 fun body: String do return json["body"].to_s
998 end
999
1000 # A comment made on a commit.
1001 class CommitComment
1002 super Comment
1003
1004 redef var key is lazy do return "{repo.key}/comments/{id}"
1005
1006 # Commented commit.
1007 fun commit: Commit do
1008 return api.load_commit(repo, json["commit_id"].to_s).as(not null)
1009 end
1010
1011 # Position of the comment on the line.
1012 fun position: nullable String do
1013 if not json.has_key("position") then return null
1014 var res = json["position"]
1015 if res == null then return null
1016 return res.to_s
1017 end
1018
1019 # Line of the comment.
1020 fun line: nullable String do
1021 if not json.has_key("line") then return null
1022 var res = json["line"]
1023 if res == null then return null
1024 return res.to_s
1025 end
1026
1027 # Path of the commented file.
1028 fun path: String do return json["path"].to_s
1029 end
1030
1031 # Comments made on Github issue and pull request pages.
1032 #
1033 # Should be accessed from `GithubAPI::load_issue_comment`.
1034 #
1035 # See <https://developer.github.com/v3/issues/comments/>.
1036 class IssueComment
1037 super Comment
1038
1039 redef var key is lazy do return "{repo.key}/issues/comments/{id}"
1040
1041 # Issue that contains `self`.
1042 fun issue: Issue do
1043 var number = issue_url.split("/").last.to_i
1044 return api.load_issue(repo, number).as(not null)
1045 end
1046
1047 # Link to the issue document on API.
1048 fun issue_url: String do return json["issue_url"].to_s
1049 end
1050
1051 # Comments made on Github pull request diffs.
1052 #
1053 # Should be accessed from `GithubAPI::load_diff_comment`.
1054 #
1055 # See <https://developer.github.com/v3/pulls/comments/>.
1056 class ReviewComment
1057 super Comment
1058
1059 redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
1060
1061 # Pull request that contains `self`.
1062 fun pull: PullRequest do
1063 var number = pull_request_url.split("/").last.to_i
1064 return api.load_pull(repo, number).as(not null)
1065 end
1066
1067 # Link to the pull request on API.
1068 fun pull_request_url: String do return json["pull_request_url"].to_s
1069
1070 # Diff hunk.
1071 fun diff_hunk: String do return json["diff_hunk"].to_s
1072
1073 # Path of commented file.
1074 fun path: String do return json["path"].to_s
1075
1076 # Position of the comment on the file.
1077 fun position: Int do return json["position"].to_s.to_i
1078
1079 # Original position in the diff.
1080 fun original_position: Int do return json["original_position"].to_s.to_i
1081
1082 # Commit referenced by this comment.
1083 fun commit_id: String do return json["commit_id"].to_s
1084
1085 # Original commit id.
1086 fun original_commit_id: String do return json["original_commit_id"].to_s
1087 end
1088
1089 # An event that occurs on a Github `Issue`.
1090 #
1091 # Should be accessed from `GithubAPI::load_issue_event`.
1092 #
1093 # See <https://developer.github.com/v3/issues/events/>.
1094 class IssueEvent
1095 super RepoEntity
1096
1097 redef var key is lazy do return "{repo.key}/issues/events/{id}"
1098
1099 # Event id on Github.
1100 var id: Int
1101
1102 redef init from_json(api, repo, json) do
1103 self.id = json["id"].as(Int)
1104 super
1105 end
1106
1107 # Issue that contains `self`.
1108 fun issue: Issue do
1109 return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
1110 end
1111
1112 # User that initiated the event.
1113 fun actor: User do
1114 return new User.from_json(api, json["actor"].as(JsonObject))
1115 end
1116
1117 # Creation time in ISODate format.
1118 fun created_at: ISODate do
1119 return new ISODate.from_string(json["created_at"].to_s)
1120 end
1121
1122 # Event descriptor.
1123 fun event: String do return json["event"].to_s
1124
1125 # Commit linked to this event (if any).
1126 fun commit_id: nullable String do
1127 var res = json["commit_id"]
1128 if res == null then return null
1129 return res.to_s
1130 end
1131
1132 # Label linked to this event (if any).
1133 fun labl: nullable Label do
1134 var res = json["label"]
1135 if not res isa JsonObject then return null
1136 return new Label.from_json(api, repo, res)
1137 end
1138
1139 # User linked to this event (if any).
1140 fun assignee: nullable User do
1141 var res = json["assignee"]
1142 if not res isa JsonObject then return null
1143 return new User.from_json(api, res)
1144 end
1145
1146 # Milestone linked to this event (if any).
1147 fun milestone: nullable Milestone do
1148 var res = json["milestone"]
1149 if not res isa JsonObject then return null
1150 return new Milestone.from_json(api, repo, res)
1151 end
1152
1153 # Rename linked to this event (if any).
1154 fun rename: nullable RenameAction do
1155 var res = json["rename"]
1156 if res == null then return null
1157 return new RenameAction(res.as(JsonObject))
1158 end
1159 end
1160
1161 # A rename action maintains the name before and after a renaming action.
1162 class RenameAction
1163
1164 # JSON content.
1165 var json: JsonObject
1166
1167 # Name before renaming.
1168 fun from: String do return json["from"].to_s
1169
1170 # Name after renaming.
1171 fun to: String do return json["to"].to_s
1172 end
1173
1174 # Contributors list with additions, deletions, and commit counts.
1175 #
1176 # Should be accessed from `Repo::contrib_stats`.
1177 #
1178 # See <https://developer.github.com/v3/repos/statistics/>.
1179 class ContributorStats
1180 super Comparable
1181
1182 redef type OTHER: ContributorStats
1183
1184 # Github API client.
1185 var api: GithubAPI
1186
1187 # Json content.
1188 var json: JsonObject
1189
1190 # Init `self` from a `json` object.
1191 init from_json(api: GithubAPI, json: JsonObject) do
1192 self.api = api
1193 self.json = json
1194 end
1195
1196 # User these statistics are about.
1197 fun author: User do
1198 return new User.from_json(api, json["author"].as(JsonObject))
1199 end
1200
1201 # Total number of commit.
1202 fun total: Int do return json["total"].to_s.to_i
1203
1204 # Are of weeks of activity with detailed statistics.
1205 fun weeks: JsonArray do return json["weeks"].as(JsonArray)
1206
1207 # ContributorStats can be compared on the total amount of commits.
1208 redef fun <(o) do return total < o.total
1209 end
1210
1211 # A Github file representation.
1212 #
1213 # Mostly a wrapper around a json object.
1214 class GithubFile
1215
1216 # Json content.
1217 var json: JsonObject
1218
1219 # File name.
1220 fun filename: String do return json["filename"].to_s
1221 end