lib/github: add setters to all github api entities and events
[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
356 # Set page url.
357 fun html_url=(url: String) do json["html_url"] = url
358 end
359
360 # A Github user
361 #
362 # Provides access to [Github user data](https://developer.github.com/v3/users/).
363 # Should be accessed from `GithubAPI::load_user`.
364 class User
365 super GithubEntity
366
367 redef var key is lazy do return "users/{login}"
368
369 # Github login.
370 var login: String
371
372 # Init `self` from a `json` object.
373 init from_json(api: GithubAPI, json: JsonObject) do
374 init(api, json["login"].as(String))
375 self.json = json
376 end
377
378 # Avatar image url for this user.
379 fun avatar_url: String do return json["avatar_url"].as(String)
380
381 # Set avatar url.
382 fun avatar_url=(url: String) do json["avatar_url"] = url
383 end
384
385 # A Github repository.
386 #
387 # Provides access to [Github repo data](https://developer.github.com/v3/repos/).
388 # Should be accessed from `GithubAPI::load_repo`.
389 class Repo
390 super GithubEntity
391
392 redef var key is lazy do return "repos/{full_name}"
393
394 # Repo full name on Github.
395 var full_name: String
396
397 # Init `self` from a `json` object.
398 init from_json(api: GithubAPI, json: JsonObject) do
399 init(api, json["full_name"].as(String))
400 self.json = json
401 end
402
403 # Repo short name on Github.
404 fun name: String do return json["name"].as(String)
405
406 # Set repo full name
407 fun name=(name: String) do json["name"] = name
408
409 # Get the repo owner.
410 fun owner: User do return new User.from_json(api, json["owner"].as(JsonObject))
411
412 # Set repo owner
413 fun owner=(owner: User) do json["owner"] = owner.json
414
415 # List of branches associated with their names.
416 fun branches: Map[String, Branch] do
417 api.message(1, "Get branches for {full_name}")
418 var array = api.get("repos/{full_name}/branches")
419 var res = new HashMap[String, Branch]
420 if not array isa JsonArray then return res
421 for obj in array do
422 if not obj isa JsonObject then continue
423 var name = obj["name"].as(String)
424 res[name] = new Branch.from_json(api, self, obj)
425 end
426 return res
427 end
428
429 # List of issues associated with their ids.
430 fun issues: Map[Int, Issue] do
431 api.message(1, "Get issues for {full_name}")
432 var res = new HashMap[Int, Issue]
433 var issue = last_issue
434 if issue == null then return res
435 res[issue.number] = issue
436 while issue.number > 1 do
437 issue = api.load_issue(self, issue.number - 1)
438 assert issue isa Issue
439 res[issue.number] = issue
440 end
441 return res
442 end
443
444 # Search issues in this repo form an advanced query.
445 #
446 # Example:
447 #
448 # ~~~nitish
449 # var issues = repo.search_issues("is:open label:need_review")
450 # ~~~
451 #
452 # See <https://developer.github.com/v3/search/#search-issues>.
453 fun search_issues(query: String): nullable Array[Issue] do
454 query = "search/issues?q={query} repo:{full_name}"
455 var response = api.get(query)
456 if api.was_error then return null
457 var arr = response.as(JsonObject)["items"].as(JsonArray)
458 var res = new Array[Issue]
459 for obj in arr do
460 res.add new Issue.from_json(api, self, obj.as(JsonObject))
461 end
462 return res
463 end
464
465 # Get the last published issue.
466 fun last_issue: nullable Issue do
467 var array = api.get("repos/{full_name}/issues")
468 if not array isa JsonArray then return null
469 if array.is_empty then return null
470 var obj = array.first
471 if not obj isa JsonObject then return null
472 return new Issue.from_json(api, self, obj)
473 end
474
475 # List of labels associated with their names.
476 fun labels: Map[String, Label] do
477 api.message(1, "Get labels for {full_name}")
478 var array = api.get("repos/{full_name}/labels")
479 var res = new HashMap[String, Label]
480 if not array isa JsonArray then return res
481 for obj in array do
482 if not obj isa JsonObject then continue
483 var name = obj["name"].as(String)
484 res[name] = new Label.from_json(api, self, obj)
485 end
486 return res
487 end
488
489 # List of milestones associated with their ids.
490 fun milestones: Map[Int, Milestone] do
491 api.message(1, "Get milestones for {full_name}")
492 var array = api.get("repos/{full_name}/milestones")
493 var res = new HashMap[Int, Milestone]
494 if array isa JsonArray then
495 for obj in array do
496 if not obj isa JsonObject then continue
497 var number = obj["number"].as(Int)
498 res[number] = new Milestone.from_json(api, self, obj)
499 end
500 end
501 return res
502 end
503
504 # List of pull-requests associated with their ids.
505 #
506 # Implementation notes: because PR numbers are not consecutive,
507 # PR are loaded from pages.
508 # See: https://developer.github.com/v3/pulls/#list-pull-requests
509 fun pulls: Map[Int, PullRequest] do
510 api.message(1, "Get pulls for {full_name}")
511 var res = new HashMap[Int, PullRequest]
512 var page = 1
513 var array = api.get("{key}/pulls?page={page}").as(JsonArray)
514 while not array.is_empty do
515 for obj in array do
516 if not obj isa JsonObject then continue
517 var number = obj["number"].as(Int)
518 res[number] = new PullRequest.from_json(api, self, obj)
519 end
520 page += 1
521 array = api.get("{key}/pulls?page={page}").as(JsonArray)
522 end
523 return res
524 end
525
526 # List of contributor related statistics.
527 fun contrib_stats: Array[ContributorStats] do
528 api.message(1, "Get contributor stats for {full_name}")
529 var res = new Array[ContributorStats]
530 var array = api.get("{key}/stats/contributors")
531 if array isa JsonArray then
532 for obj in array do
533 res.add new ContributorStats.from_json(api, obj.as(JsonObject))
534 end
535 end
536 return res
537 end
538
539 # Repo default branch.
540 fun default_branch: Branch do
541 var name = json["default_branch"].as(String)
542 var branch = api.load_branch(self, name)
543 assert branch isa Branch
544 return branch
545 end
546
547 # Set the default branch
548 fun default_branch=(branch: Branch) do json["default_branch"] = branch.json
549 end
550
551 # A `RepoEntity` is something contained in a `Repo`.
552 abstract class RepoEntity
553 super GithubEntity
554
555 # Repo that contains `self`.
556 var repo: Repo
557
558 # Init `self` from a `json` object.
559 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
560 self.api = api
561 self.repo = repo
562 self.json = json
563 end
564 end
565
566 # A Github branch.
567 #
568 # Should be accessed from `GithubAPI::load_branch`.
569 #
570 # See <https://developer.github.com/v3/repos/#list-branches>.
571 class Branch
572 super RepoEntity
573
574 redef var key is lazy do return "{repo.key}/branches/{name}"
575
576 # Branch name.
577 var name: String
578
579 redef init from_json(api, repo, json) do
580 self.name = json["name"].as(String)
581 super
582 end
583
584 # Get the last commit of `self`.
585 fun commit: Commit do return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
586
587 # Set the last commit
588 fun commit=(commit: Commit) do json["commit"] = commit.json
589
590 # List all commits in `self`.
591 #
592 # This can be long depending on the branch size.
593 # Commit are returned in an unspecified order.
594 fun commits: Array[Commit] do
595 var res = new Array[Commit]
596 var done = new HashSet[String]
597 var todos = new Array[Commit]
598 todos.add commit
599 while not todos.is_empty do
600 var commit = todos.pop
601 if done.has(commit.sha) then continue
602 done.add commit.sha
603 res.add commit
604 for parent in commit.parents do
605 todos.add parent
606 end
607 end
608 return res
609 end
610 end
611
612 # A Github commit.
613 #
614 # Should be accessed from `GithubAPI::load_commit`.
615 #
616 # See <https://developer.github.com/v3/repos/commits/>.
617 class Commit
618 super RepoEntity
619
620 redef var key is lazy do return "{repo.key}/commits/{sha}"
621
622 # Commit SHA.
623 var sha: String
624
625 redef init from_json(api, repo, json) do
626 self.sha = json["sha"].as(String)
627 super
628 end
629
630 # Parent commits of `self`.
631 fun parents: Array[Commit] do
632 var res = new Array[Commit]
633 var parents = json.get_or_null("parents")
634 if not parents isa JsonArray then return res
635 for obj in parents do
636 if not obj isa JsonObject then continue
637 res.add(api.load_commit(repo, obj["sha"].as(String)).as(not null))
638 end
639 return res
640 end
641
642 # Set parent commits.
643 fun parents=(parents: Array[Commit]) do
644 var res = new JsonArray
645 for parent in parents do res.add parent.json
646 json["parents"] = res
647 end
648
649 # Author of the commit.
650 fun author: nullable User do
651 var user = json.get_or_null("author")
652 if user isa JsonObject then return new User.from_json(api, user)
653 return null
654 end
655
656 # Set commit author.
657 fun author=(user: nullable User) do
658 if user == null then
659 json["author"] = null
660 else
661 json["author"] = user.json
662 end
663 end
664
665 # Committer of the commit.
666 fun committer: nullable User do
667 var user = json.get_or_null("author")
668 if user isa JsonObject then return new User.from_json(api, user)
669 return null
670 end
671
672 # Set commit committer.
673 fun committer=(user: nullable User) do
674 if user == null then
675 json["committer"] = null
676 else
677 json["committer"] = user.json
678 end
679 end
680
681 # Authoring date as ISODate.
682 fun author_date: ISODate do
683 var commit = json["commit"].as(JsonObject)
684 var author = commit["author"].as(JsonObject)
685 return new ISODate.from_string(author["date"].as(String))
686 end
687
688 # Commit date as ISODate.
689 fun commit_date: ISODate do
690 var commit = json["commit"].as(JsonObject)
691 var author = commit["committer"].as(JsonObject)
692 return new ISODate.from_string(author["date"].as(String))
693 end
694
695 # List files staged in this commit.
696 fun files: Array[GithubFile] do
697 var res = new Array[GithubFile]
698 var files = json.get_or_null("files")
699 if not files isa JsonArray then return res
700 for obj in files do
701 res.add(new GithubFile(obj.as(JsonObject)))
702 end
703 return res
704 end
705
706 # Set commit files.
707 fun files=(files: Array[GithubFile]) do
708 var res = new JsonArray
709 for file in files do res.add file.json
710 json["files"] = res
711 end
712
713 # Commit message.
714 fun message: String do return json["commit"].as(JsonObject)["message"].as(String)
715 end
716
717 # A Github issue.
718 #
719 # Should be accessed from `GithubAPI::load_issue`.
720 #
721 # See <https://developer.github.com/v3/issues/>.
722 class Issue
723 super RepoEntity
724
725 redef var key is lazy do return "{repo.key}/issues/{number}"
726
727 # Issue Github ID.
728 var number: Int
729
730 redef init from_json(api, repo, json) do
731 self.number = json["number"].as(Int)
732 super
733 end
734
735 # Issue title.
736 fun title: String do return json["title"].as(String)
737
738 # Set issue title
739 fun title=(title: String) do json["title"] = title
740
741 # User that created this issue.
742 fun user: User do return new User.from_json(api, json["user"].as(JsonObject))
743
744 # Set issue creator.
745 fun user=(user: User) do json["user"] = user.json
746
747 # List of labels on this issue associated to their names.
748 fun labels: Map[String, Label] do
749 var res = new HashMap[String, Label]
750 var lbls = json.get_or_null("labels")
751 if not lbls isa JsonArray then return res
752 for obj in lbls do
753 if not obj isa JsonObject then continue
754 var name = obj["name"].as(String)
755 res[name] = new Label.from_json(api, repo, obj)
756 end
757 return res
758 end
759
760 # State of the issue on Github.
761 fun state: String do return json["state"].as(String)
762
763 # Set the state of this issue.
764 fun state=(state: String) do json["state"] = state
765
766 # Is the issue locked?
767 fun locked: Bool do return json["locked"].as(Bool)
768
769 # Set issue locked state.
770 fun locked=(locked: Bool) do json["locked"] = locked
771
772 # Assigned `User` (if any).
773 fun assignee: nullable User do
774 var assignee = json.get_or_null("assignee")
775 if assignee isa JsonObject then return new User.from_json(api, assignee)
776 return null
777 end
778
779 # Set issue assignee.
780 fun assignee=(user: nullable User) do
781 if user == null then
782 json["assignee"] = null
783 else
784 json["assignee"] = user.json
785 end
786 end
787
788 # `Milestone` (if any).
789 fun milestone: nullable Milestone do
790 var milestone = json.get_or_null("milestone")
791 if milestone isa JsonObject then return new Milestone.from_json(api, repo, milestone)
792 return null
793 end
794
795 # Set issue milestone.
796 fun milestone=(milestone: nullable Milestone) do
797 if milestone == null then
798 json["milestone"] = null
799 else
800 json["milestone"] = milestone.json
801 end
802 end
803
804 # List of comments made on this issue.
805 fun comments: Array[IssueComment] do
806 var res = new Array[IssueComment]
807 var count = comments_count
808 var page = 1
809 var array = api.get("{key}/comments?page={page}")
810 if not array isa JsonArray then
811 return res
812 end
813 while not array.is_empty and res.length < count do
814 for obj in array do
815 if not obj isa JsonObject then continue
816 var id = obj["id"].as(Int)
817 res.add(api.load_issue_comment(repo, id).as(not null))
818 end
819 page += 1
820 array = api.get("{key}/comments?page={page}").as(JsonArray)
821 end
822 return res
823 end
824
825 # Number of comments on this issue.
826 fun comments_count: Int do return json["comments"].as(Int)
827
828 # Creation time in ISODate format.
829 fun created_at: ISODate do return new ISODate.from_string(json["created_at"].as(String))
830
831 # Set issue creation time.
832 fun created_at=(created_at: nullable ISODate) do
833 if created_at == null then
834 json["created_at"] = null
835 else
836 json["created_at"] = created_at.to_s
837 end
838 end
839
840 # Last update time in ISODate format (if any).
841 fun updated_at: nullable ISODate do
842 var res = json.get_or_null("updated_at")
843 if res isa String then return new ISODate.from_string(res)
844 return null
845 end
846
847 # Set issue last update time.
848 fun updated_at=(updated_at: nullable ISODate) do
849 if updated_at == null then
850 json["updated_at"] = null
851 else
852 json["updated_at"] = updated_at.to_s
853 end
854 end
855
856 # Close time in ISODate format (if any).
857 fun closed_at: nullable ISODate do
858 var res = json.get_or_null("closed_at")
859 if res isa String then return new ISODate.from_string(res)
860 return null
861 end
862
863 # Set issue close time.
864 fun closed_at=(closed_at: nullable ISODate) do
865 if closed_at == null then
866 json["closed_at"] = null
867 else
868 json["closed_at"] = closed_at.to_s
869 end
870 end
871
872 # TODO link to pull request
873
874 # Full description of the issue.
875 fun body: String do return json["body"].as(String)
876
877 # Set description body
878 fun body=(body: String) do json["body"] = body
879
880 # List of events on this issue.
881 fun events: Array[IssueEvent] do
882 var res = new Array[IssueEvent]
883 var page = 1
884 var array = api.get("{key}/events?page={page}")
885 if not array isa JsonArray then return res
886 while not array.is_empty do
887 for obj in array do
888 if not obj isa JsonObject then continue
889 res.add new IssueEvent.from_json(api, repo, obj)
890 end
891 page += 1
892 array = api.get("{key}/events?page={page}").as(JsonArray)
893 end
894 return res
895 end
896
897 # User that closed this issue (if any).
898 fun closed_by: nullable User do
899 var closer = json.get_or_null("closed_by")
900 if closer isa JsonObject then return new User.from_json(api, closer)
901 return null
902 end
903
904 # Set user that closed the issue.
905 fun closed_by=(user: nullable User) do
906 if user == null then
907 json["closed_by"] = null
908 else
909 json["closed_by"] = user.json
910 end
911 end
912 end
913
914 # A Github pull request.
915 #
916 # Should be accessed from `GithubAPI::load_pull`.
917 #
918 # PullRequest are basically Issues with more data.
919 # See <https://developer.github.com/v3/pulls/>.
920 class PullRequest
921 super Issue
922
923 redef var key is lazy do return "{repo.key}/pulls/{number}"
924
925 # Merge time in ISODate format (if any).
926 fun merged_at: nullable ISODate do
927 var res = json.get_or_null("merged_at")
928 if res isa String then return new ISODate.from_string(res)
929 return null
930 end
931
932 # Set pull request merge time.
933 fun merged_at=(merged_at: nullable ISODate) do
934 if merged_at == null then
935 json["merged_at"] = null
936 else
937 json["merged_at"] = merged_at.to_s
938 end
939 end
940
941 # Merge commit SHA.
942 fun merge_commit_sha: String do return json["merge_commit_sha"].as(String)
943
944 # Set merge_commit_sha
945 fun merge_commit_sha=(sha: String) do json["merge_commit_sha"] = sha
946
947 # Count of comments made on the pull request diff.
948 fun review_comments: Int do return json["review_comments"].as(Int)
949
950 # Set review_comments
951 fun review_comments=(count: Int) do json["review_comments"] = count
952
953 # Pull request head (can be a commit SHA or a branch name).
954 fun head: PullRef do
955 var json = json["head"].as(JsonObject)
956 return new PullRef(api, json)
957 end
958
959 # Set head
960 fun head=(head: PullRef) do json["head"] = head.json
961
962 # Pull request base (can be a commit SHA or a branch name).
963 fun base: PullRef do
964 var json = json["base"].as(JsonObject)
965 return new PullRef(api, json)
966 end
967
968 # Set base
969 fun base=(base: PullRef) do json["base"] = base.json
970
971 # Is this pull request merged?
972 fun merged: Bool do return json["merged"].as(Bool)
973
974 # Set merged
975 fun merged=(merged: Bool) do json["merged"] = merged
976
977 # Is this pull request mergeable?
978 fun mergeable: Bool do return json["mergeable"].as(Bool)
979
980 # Set mergeable
981 fun mergeable=(mergeable: Bool) do json["mergeable"] = mergeable
982
983 # Mergeable state of this pull request.
984 #
985 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
986 fun mergeable_state: Int do return json["mergeable_state"].as(Int)
987
988 # Set mergeable_state
989 fun mergeable_state=(mergeable_state: Int) do json["mergeable_state"] = mergeable_state
990
991 # User that merged this pull request (if any).
992 fun merged_by: nullable User do
993 var merger = json.get_or_null("merged_by")
994 if merger isa JsonObject then return new User.from_json(api, merger)
995 return null
996 end
997
998 # Set merged_by.
999 fun merged_by=(merged_by: nullable User) do
1000 if merged_by == null then
1001 json["merged_by"] = null
1002 else
1003 json["merged_by"] = merged_by.json
1004 end
1005 end
1006
1007 # Count of commits in this pull request.
1008 fun commits: Int do return json["commits"].as(Int)
1009
1010 # Set commits
1011 fun commits=(commits: Int) do json["commits"] = commits
1012
1013 # Added line count.
1014 fun additions: Int do return json["additions"].as(Int)
1015
1016 # Set additions
1017 fun additions=(additions: Int) do json["additions"] = additions
1018
1019 # Deleted line count.
1020 fun deletions: Int do return json["deletions"].as(Int)
1021
1022 # Set deletions
1023 fun deletions=(deletions: Int) do json["deletions"] = deletions
1024
1025 # Changed files count.
1026 fun changed_files: Int do return json["changed_files"].as(Int)
1027
1028 # Set changed_files
1029 fun changed_files=(changed_files: Int) do json["changed_files"] = changed_files
1030 end
1031
1032 # A pull request reference (used for head and base).
1033 class PullRef
1034
1035 # Api instance that maintains self.
1036 var api: GithubAPI
1037
1038 # JSON representation.
1039 var json: JsonObject
1040
1041 # Label pointed by `self`.
1042 fun labl: String do return json["label"].as(String)
1043
1044 # Set labl
1045 fun labl=(labl: String) do json["label"] = labl
1046
1047 # Reference pointed by `self`.
1048 fun ref: String do return json["ref"].as(String)
1049
1050 # Set ref
1051 fun ref=(ref: String) do json["ref"] = ref
1052
1053 # Commit SHA pointed by `self`.
1054 fun sha: String do return json["sha"].as(String)
1055
1056 # Set sha
1057 fun sha=(sha: String) do json["sha"] = sha
1058
1059 # User pointed by `self`.
1060 fun user: User do
1061 return new User.from_json(api, json["user"].as(JsonObject))
1062 end
1063
1064 # Set user
1065 fun user=(user: User) do json["user"] = user.json
1066
1067 # Repo pointed by `self`.
1068 fun repo: Repo do
1069 return new Repo.from_json(api, json["repo"].as(JsonObject))
1070 end
1071
1072 # Set repo
1073 fun repo=(repo: Repo) do json["repo"] = repo.json
1074 end
1075
1076 # A Github label.
1077 #
1078 # Should be accessed from `GithubAPI::load_label`.
1079 #
1080 # See <https://developer.github.com/v3/issues/labels/>.
1081 class Label
1082 super RepoEntity
1083
1084 redef var key is lazy do return "{repo.key}/labels/{name}"
1085
1086 # Label name.
1087 var name: String
1088
1089 redef init from_json(api, repo, json) do
1090 self.name = json["name"].as(String)
1091 super
1092 end
1093
1094 # Label color code.
1095 fun color: String do return json["color"].as(String)
1096
1097 # Set color
1098 fun color=(color: String) do json["color"] = color
1099 end
1100
1101 # A Github milestone.
1102 #
1103 # Should be accessed from `GithubAPI::load_milestone`.
1104 #
1105 # See <https://developer.github.com/v3/issues/milestones/>.
1106 class Milestone
1107 super RepoEntity
1108
1109 redef var key is lazy do return "{repo.key}/milestones/{number}"
1110
1111 # The milestone id on Github.
1112 var number: Int
1113
1114 redef init from_json(api, repo, json) do
1115 super
1116 self.number = json["number"].as(Int)
1117 end
1118
1119 # Milestone title.
1120 fun title: String do return json["title"].as(String)
1121
1122 # Set title
1123 fun title=(title: String) do json["title"] = title
1124
1125 # Milestone long description.
1126 fun description: String do return json["description"].as(String)
1127
1128 # Set description
1129 fun description=(description: String) do json["description"] = description
1130
1131 # Count of opened issues linked to this milestone.
1132 fun open_issues: Int do return json["open_issues"].as(Int)
1133
1134 # Set open_issues
1135 fun open_issues=(open_issues: Int) do json["open_issues"] = open_issues
1136
1137 # Count of closed issues linked to this milestone.
1138 fun closed_issues: Int do return json["closed_issues"].as(Int)
1139
1140 # Set closed_issues
1141 fun closed_issues=(closed_issues: Int) do json["closed_issues"] = closed_issues
1142
1143 # Milestone state.
1144 fun state: String do return json["state"].as(String)
1145
1146 # Set state
1147 fun state=(state: String) do json["state"] = state
1148
1149 # Creation time in ISODate format.
1150 fun created_at: ISODate do
1151 return new ISODate.from_string(json["created_at"].as(String))
1152 end
1153
1154 # Set created_at
1155 fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
1156
1157 # User that created this milestone.
1158 fun creator: User do
1159 return new User.from_json(api, json["creator"].as(JsonObject))
1160 end
1161
1162 # Set creator
1163 fun creator=(creator: User) do json["creator"] = creator.json
1164
1165 # Due time in ISODate format (if any).
1166 fun due_on: nullable ISODate do
1167 var res = json.get_or_null("updated_at")
1168 if res isa String then return new ISODate.from_string(res)
1169 return null
1170 end
1171
1172 # Set due_on.
1173 fun due_on=(due_on: nullable ISODate) do
1174 if due_on == null then
1175 json["due_on"] = null
1176 else
1177 json["due_on"] = due_on.to_s
1178 end
1179 end
1180
1181 # Update time in ISODate format (if any).
1182 fun updated_at: nullable ISODate do
1183 var res = json.get_or_null("updated_at")
1184 if res isa String then return new ISODate.from_string(res)
1185 return null
1186 end
1187
1188 # Set updated_at.
1189 fun updated_at=(updated_at: nullable ISODate) do
1190 if updated_at == null then
1191 json["updated_at"] = null
1192 else
1193 json["updated_at"] = updated_at.to_s
1194 end
1195 end
1196
1197 # Close time in ISODate format (if any).
1198 fun closed_at: nullable ISODate do
1199 var res = json.get_or_null("closed_at")
1200 if res isa String then return new ISODate.from_string(res)
1201 return null
1202 end
1203
1204 # Set closed_at.
1205 fun closed_at=(closed_at: nullable ISODate) do
1206 if closed_at == null then
1207 json["closed_at"] = null
1208 else
1209 json["closed_at"] = closed_at.to_s
1210 end
1211 end
1212 end
1213
1214 # A Github comment
1215 #
1216 # There is two kinds of comments:
1217 #
1218 # * `CommitComment` are made on a commit page.
1219 # * `IssueComment` are made on an issue or pull request page.
1220 # * `ReviewComment` are made on the diff associated to a pull request.
1221 abstract class Comment
1222 super RepoEntity
1223
1224 # Identifier of this comment.
1225 var id: Int
1226
1227 redef init from_json(api, repo, json) do
1228 self.id = json["id"].as(Int)
1229 super
1230 end
1231
1232 # User that made this comment.
1233 fun user: User do
1234 return new User.from_json(api, json["user"].as(JsonObject))
1235 end
1236
1237 # Set user
1238 fun user=(user: User) do json["user"] = user.json
1239
1240 # Creation time in ISODate format.
1241 fun created_at: ISODate do
1242 return new ISODate.from_string(json["created_at"].as(String))
1243 end
1244
1245 # Set created_at
1246 fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
1247
1248 # Last update time in ISODate format (if any).
1249 fun updated_at: nullable ISODate do
1250 var res = json.get_or_null("updated_at")
1251 if res isa String then return new ISODate.from_string(res)
1252 return null
1253 end
1254
1255 # Set updated_at.
1256 fun updated_at=(updated_at: nullable ISODate) do
1257 if updated_at == null then
1258 json["updated_at"] = null
1259 else
1260 json["updated_at"] = updated_at.to_s
1261 end
1262 end
1263
1264 # Comment body text.
1265 fun body: String do return json["body"].as(String)
1266
1267 # Set body
1268 fun body=(body: String) do json["body"] = body
1269
1270 # Does the comment contain an acknowledgement (+1)
1271 fun is_ack: Bool
1272 do
1273 return body.has("\\+1\\b".to_re) or body.has(":+1:") or body.has(":shipit:")
1274 end
1275 end
1276
1277 # A comment made on a commit.
1278 class CommitComment
1279 super Comment
1280
1281 redef var key is lazy do return "{repo.key}/comments/{id}"
1282
1283 # Commented commit.
1284 fun commit: Commit do
1285 return api.load_commit(repo, json["commit_id"].as(String)).as(not null)
1286 end
1287
1288 # Set commit
1289 fun commit=(commit: Commit) do json["commit_id"] = commit.json
1290
1291 # Position of the comment on the line.
1292 fun position: nullable String do
1293 var res = json.get_or_null("position")
1294 if res isa String then return res
1295 return null
1296 end
1297
1298 # Set position.
1299 fun position=(position: nullable String) do json["position"] = position
1300
1301 # Line of the comment.
1302 fun line: nullable String do
1303 var res = json.get_or_null("line")
1304 if res isa String then return res
1305 return null
1306 end
1307
1308 # Set line.
1309 fun line=(line: nullable String) do json["line"] = line
1310
1311 # Path of the commented file.
1312 fun path: String do return json["path"].as(String)
1313
1314 # Set path.
1315 fun path=(path: String) do json["path"] = path
1316 end
1317
1318 # Comments made on Github issue and pull request pages.
1319 #
1320 # Should be accessed from `GithubAPI::load_issue_comment`.
1321 #
1322 # See <https://developer.github.com/v3/issues/comments/>.
1323 class IssueComment
1324 super Comment
1325
1326 redef var key is lazy do return "{repo.key}/issues/comments/{id}"
1327
1328 # Issue that contains `self`.
1329 fun issue: Issue do
1330 var number = issue_url.split("/").last.to_i
1331 return api.load_issue(repo, number).as(not null)
1332 end
1333
1334 # Link to the issue document on API.
1335 fun issue_url: String do return json["issue_url"].as(String)
1336
1337 # Set issue_url.
1338 fun issue_url=(issue_url: String) do json["issue_url"] = issue_url
1339 end
1340
1341 # Comments made on Github pull request diffs.
1342 #
1343 # Should be accessed from `GithubAPI::load_diff_comment`.
1344 #
1345 # See <https://developer.github.com/v3/pulls/comments/>.
1346 class ReviewComment
1347 super Comment
1348
1349 redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
1350
1351 # Pull request that contains `self`.
1352 fun pull: PullRequest do
1353 var number = pull_request_url.split("/").last.to_i
1354 return api.load_pull(repo, number).as(not null)
1355 end
1356
1357 # Link to the pull request on API.
1358 fun pull_request_url: String do return json["pull_request_url"].as(String)
1359
1360 # Set pull_request_url.
1361 fun pull_request_url=(pull_request_url: String) do json["pull_request_url"] = pull_request_url
1362
1363 # Diff hunk.
1364 fun diff_hunk: String do return json["diff_hunk"].as(String)
1365
1366 # Set diff_hunk.
1367 fun diff_hunk=(diff_hunk: String) do json["diff_hunk"] = diff_hunk
1368
1369 # Path of commented file.
1370 fun path: String do return json["path"].as(String)
1371
1372 # Set path.
1373 fun path=(path: String) do json["path"] = path
1374
1375 # Position of the comment on the file.
1376 fun position: Int do return json["position"].as(Int)
1377
1378 # Set position.
1379 fun position=(position: Int) do json["position"] = position
1380
1381 # Original position in the diff.
1382 fun original_position: Int do return json["original_position"].as(Int)
1383
1384 # Set original_position.
1385 fun original_position=(original_position: Int) do json["original_position"] = original_position
1386
1387 # Commit referenced by this comment.
1388 fun commit_id: String do return json["commit_id"].as(String)
1389
1390 # Set commit_id.
1391 fun commit_id=(commit_id: String) do json["commit_id"] = commit_id
1392
1393 # Original commit id.
1394 fun original_commit_id: String do return json["original_commit_id"].as(String)
1395
1396 # Set original_commit_id.
1397 fun original_commit_id=(commit_id: String) do json["original_commit_id"] = commit_id
1398 end
1399
1400 # An event that occurs on a Github `Issue`.
1401 #
1402 # Should be accessed from `GithubAPI::load_issue_event`.
1403 #
1404 # See <https://developer.github.com/v3/issues/events/>.
1405 class IssueEvent
1406 super RepoEntity
1407
1408 redef var key is lazy do return "{repo.key}/issues/events/{id}"
1409
1410 # Event id on Github.
1411 var id: Int
1412
1413 redef init from_json(api, repo, json) do
1414 self.id = json["id"].as(Int)
1415 super
1416 end
1417
1418 # Issue that contains `self`.
1419 fun issue: Issue do
1420 return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
1421 end
1422
1423 # Set issue.
1424 fun issue=(issue: Issue) do json["issue"] = issue.json
1425
1426 # User that initiated the event.
1427 fun actor: User do
1428 return new User.from_json(api, json["actor"].as(JsonObject))
1429 end
1430
1431 # Set actor.
1432 fun actor=(actor: User) do json["actor"] = actor.json
1433
1434 # Creation time in ISODate format.
1435 fun created_at: ISODate do
1436 return new ISODate.from_string(json["created_at"].as(String))
1437 end
1438
1439 # Set created_at.
1440 fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
1441
1442 # Event descriptor.
1443 fun event: String do return json["event"].as(String)
1444
1445 # Set event.
1446 fun event=(event: String) do json["event"] = event
1447
1448 # Commit linked to this event (if any).
1449 fun commit_id: nullable String do
1450 var res = json.get_or_null("commit_id")
1451 if res isa String then return res
1452 return null
1453 end
1454
1455 # Set commit_id.
1456 fun commit_id=(commit_id: nullable String) do json["commit_id"] = commit_id
1457
1458 # Label linked to this event (if any).
1459 fun labl: nullable Label do
1460 var res = json.get_or_null("label")
1461 if res isa JsonObject then return new Label.from_json(api, repo, res)
1462 return null
1463 end
1464
1465 # Set labl.
1466 fun labl=(labl: nullable Label) do
1467 if labl == null then
1468 json["labl"] = null
1469 else
1470 json["labl"] = labl.json
1471 end
1472 end
1473
1474 # User linked to this event (if any).
1475 fun assignee: nullable User do
1476 var res = json.get_or_null("assignee")
1477 if res isa JsonObject then return new User.from_json(api, res)
1478 return null
1479 end
1480
1481 # Set assignee.
1482 fun assignee=(assignee: nullable User) do
1483 if assignee == null then
1484 json["assignee"] = null
1485 else
1486 json["assignee"] = assignee.json
1487 end
1488 end
1489
1490 # Milestone linked to this event (if any).
1491 fun milestone: nullable Milestone do
1492 var res = json.get_or_null("milestone")
1493 if res isa JsonObject then return new Milestone.from_json(api, repo, res)
1494 return null
1495 end
1496
1497 # Set milestone.
1498 fun milestone=(milestone: nullable User) do
1499 if milestone == null then
1500 json["milestone"] = null
1501 else
1502 json["milestone"] = milestone.json
1503 end
1504 end
1505
1506 # Rename linked to this event (if any).
1507 fun rename: nullable RenameAction do
1508 var res = json.get_or_null("rename")
1509 if res isa JsonObject then return new RenameAction(res)
1510 return null
1511 end
1512
1513 # Set rename.
1514 fun rename=(rename: nullable User) do
1515 if rename == null then
1516 json["rename"] = null
1517 else
1518 json["rename"] = rename.json
1519 end
1520 end
1521 end
1522
1523 # A rename action maintains the name before and after a renaming action.
1524 class RenameAction
1525
1526 # JSON content.
1527 var json: JsonObject
1528
1529 # Name before renaming.
1530 fun from: String do return json["from"].as(String)
1531
1532 # Set from.
1533 fun from=(from: String) do json["from"] = from
1534
1535 # Name after renaming.
1536 fun to: String do return json["to"].as(String)
1537
1538 # Set to.
1539 fun to=(to: String) do json["to"] = to
1540 end
1541
1542 # Contributors list with additions, deletions, and commit counts.
1543 #
1544 # Should be accessed from `Repo::contrib_stats`.
1545 #
1546 # See <https://developer.github.com/v3/repos/statistics/>.
1547 class ContributorStats
1548 super Comparable
1549
1550 redef type OTHER: ContributorStats
1551
1552 # Github API client.
1553 var api: GithubAPI
1554
1555 # Json content.
1556 var json: JsonObject
1557
1558 # Init `self` from a `json` object.
1559 init from_json(api: GithubAPI, json: JsonObject) do
1560 self.api = api
1561 self.json = json
1562 end
1563
1564 # User these statistics are about.
1565 fun author: User do
1566 return new User.from_json(api, json["author"].as(JsonObject))
1567 end
1568
1569 # Set author.
1570 fun author=(author: User) do json["author"] = author.json
1571
1572 # Total number of commit.
1573 fun total: Int do return json["total"].as(Int)
1574
1575 # Set total.
1576 fun total=(total: Int) do json["total"] = total
1577
1578 # Are of weeks of activity with detailed statistics.
1579 fun weeks: JsonArray do return json["weeks"].as(JsonArray)
1580
1581 # Set weeks.
1582 fun weeks=(weeks: JsonArray) do json["weeks"] = weeks
1583
1584 # ContributorStats can be compared on the total amount of commits.
1585 redef fun <(o) do return total < o.total
1586 end
1587
1588 # A Github file representation.
1589 #
1590 # Mostly a wrapper around a json object.
1591 class GithubFile
1592
1593 # Json content.
1594 var json: JsonObject
1595
1596 # File name.
1597 fun filename: String do return json["filename"].as(String)
1598
1599 # Set filename.
1600 fun filename=(filename: String) do json["filename"] = filename
1601 end