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