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