lib/github: introduce `Issue::is_pull_request` method
[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
913 # Is this issue linked to a pull request?
914 fun is_pull_request: Bool do return json.has_key("pull_request")
915 end
916
917 # A Github pull request.
918 #
919 # Should be accessed from `GithubAPI::load_pull`.
920 #
921 # PullRequest are basically Issues with more data.
922 # See <https://developer.github.com/v3/pulls/>.
923 class PullRequest
924 super Issue
925
926 redef var key is lazy do return "{repo.key}/pulls/{number}"
927
928 # Merge time in ISODate format (if any).
929 fun merged_at: nullable ISODate do
930 var res = json.get_or_null("merged_at")
931 if res isa String then return new ISODate.from_string(res)
932 return null
933 end
934
935 # Set pull request merge time.
936 fun merged_at=(merged_at: nullable ISODate) do
937 if merged_at == null then
938 json["merged_at"] = null
939 else
940 json["merged_at"] = merged_at.to_s
941 end
942 end
943
944 # Merge commit SHA.
945 fun merge_commit_sha: String do return json["merge_commit_sha"].as(String)
946
947 # Set merge_commit_sha
948 fun merge_commit_sha=(sha: String) do json["merge_commit_sha"] = sha
949
950 # Count of comments made on the pull request diff.
951 fun review_comments: Int do return json["review_comments"].as(Int)
952
953 # Set review_comments
954 fun review_comments=(count: Int) do json["review_comments"] = count
955
956 # Pull request head (can be a commit SHA or a branch name).
957 fun head: PullRef do
958 var json = json["head"].as(JsonObject)
959 return new PullRef(api, json)
960 end
961
962 # Set head
963 fun head=(head: PullRef) do json["head"] = head.json
964
965 # Pull request base (can be a commit SHA or a branch name).
966 fun base: PullRef do
967 var json = json["base"].as(JsonObject)
968 return new PullRef(api, json)
969 end
970
971 # Set base
972 fun base=(base: PullRef) do json["base"] = base.json
973
974 # Is this pull request merged?
975 fun merged: Bool do return json["merged"].as(Bool)
976
977 # Set merged
978 fun merged=(merged: Bool) do json["merged"] = merged
979
980 # Is this pull request mergeable?
981 fun mergeable: Bool do return json["mergeable"].as(Bool)
982
983 # Set mergeable
984 fun mergeable=(mergeable: Bool) do json["mergeable"] = mergeable
985
986 # Mergeable state of this pull request.
987 #
988 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
989 fun mergeable_state: Int do return json["mergeable_state"].as(Int)
990
991 # Set mergeable_state
992 fun mergeable_state=(mergeable_state: Int) do json["mergeable_state"] = mergeable_state
993
994 # User that merged this pull request (if any).
995 fun merged_by: nullable User do
996 var merger = json.get_or_null("merged_by")
997 if merger isa JsonObject then return new User.from_json(api, merger)
998 return null
999 end
1000
1001 # Set merged_by.
1002 fun merged_by=(merged_by: nullable User) do
1003 if merged_by == null then
1004 json["merged_by"] = null
1005 else
1006 json["merged_by"] = merged_by.json
1007 end
1008 end
1009
1010 # Count of commits in this pull request.
1011 fun commits: Int do return json["commits"].as(Int)
1012
1013 # Set commits
1014 fun commits=(commits: Int) do json["commits"] = commits
1015
1016 # Added line count.
1017 fun additions: Int do return json["additions"].as(Int)
1018
1019 # Set additions
1020 fun additions=(additions: Int) do json["additions"] = additions
1021
1022 # Deleted line count.
1023 fun deletions: Int do return json["deletions"].as(Int)
1024
1025 # Set deletions
1026 fun deletions=(deletions: Int) do json["deletions"] = deletions
1027
1028 # Changed files count.
1029 fun changed_files: Int do return json["changed_files"].as(Int)
1030
1031 # Set changed_files
1032 fun changed_files=(changed_files: Int) do json["changed_files"] = changed_files
1033 end
1034
1035 # A pull request reference (used for head and base).
1036 class PullRef
1037
1038 # Api instance that maintains self.
1039 var api: GithubAPI
1040
1041 # JSON representation.
1042 var json: JsonObject
1043
1044 # Label pointed by `self`.
1045 fun labl: String do return json["label"].as(String)
1046
1047 # Set labl
1048 fun labl=(labl: String) do json["label"] = labl
1049
1050 # Reference pointed by `self`.
1051 fun ref: String do return json["ref"].as(String)
1052
1053 # Set ref
1054 fun ref=(ref: String) do json["ref"] = ref
1055
1056 # Commit SHA pointed by `self`.
1057 fun sha: String do return json["sha"].as(String)
1058
1059 # Set sha
1060 fun sha=(sha: String) do json["sha"] = sha
1061
1062 # User pointed by `self`.
1063 fun user: User do
1064 return new User.from_json(api, json["user"].as(JsonObject))
1065 end
1066
1067 # Set user
1068 fun user=(user: User) do json["user"] = user.json
1069
1070 # Repo pointed by `self`.
1071 fun repo: Repo do
1072 return new Repo.from_json(api, json["repo"].as(JsonObject))
1073 end
1074
1075 # Set repo
1076 fun repo=(repo: Repo) do json["repo"] = repo.json
1077 end
1078
1079 # A Github label.
1080 #
1081 # Should be accessed from `GithubAPI::load_label`.
1082 #
1083 # See <https://developer.github.com/v3/issues/labels/>.
1084 class Label
1085 super RepoEntity
1086
1087 redef var key is lazy do return "{repo.key}/labels/{name}"
1088
1089 # Label name.
1090 var name: String
1091
1092 redef init from_json(api, repo, json) do
1093 self.name = json["name"].as(String)
1094 super
1095 end
1096
1097 # Label color code.
1098 fun color: String do return json["color"].as(String)
1099
1100 # Set color
1101 fun color=(color: String) do json["color"] = color
1102 end
1103
1104 # A Github milestone.
1105 #
1106 # Should be accessed from `GithubAPI::load_milestone`.
1107 #
1108 # See <https://developer.github.com/v3/issues/milestones/>.
1109 class Milestone
1110 super RepoEntity
1111
1112 redef var key is lazy do return "{repo.key}/milestones/{number}"
1113
1114 # The milestone id on Github.
1115 var number: Int
1116
1117 redef init from_json(api, repo, json) do
1118 super
1119 self.number = json["number"].as(Int)
1120 end
1121
1122 # Milestone title.
1123 fun title: String do return json["title"].as(String)
1124
1125 # Set title
1126 fun title=(title: String) do json["title"] = title
1127
1128 # Milestone long description.
1129 fun description: String do return json["description"].as(String)
1130
1131 # Set description
1132 fun description=(description: String) do json["description"] = description
1133
1134 # Count of opened issues linked to this milestone.
1135 fun open_issues: Int do return json["open_issues"].as(Int)
1136
1137 # Set open_issues
1138 fun open_issues=(open_issues: Int) do json["open_issues"] = open_issues
1139
1140 # Count of closed issues linked to this milestone.
1141 fun closed_issues: Int do return json["closed_issues"].as(Int)
1142
1143 # Set closed_issues
1144 fun closed_issues=(closed_issues: Int) do json["closed_issues"] = closed_issues
1145
1146 # Milestone state.
1147 fun state: String do return json["state"].as(String)
1148
1149 # Set state
1150 fun state=(state: String) do json["state"] = state
1151
1152 # Creation time in ISODate format.
1153 fun created_at: ISODate do
1154 return new ISODate.from_string(json["created_at"].as(String))
1155 end
1156
1157 # Set created_at
1158 fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
1159
1160 # User that created this milestone.
1161 fun creator: User do
1162 return new User.from_json(api, json["creator"].as(JsonObject))
1163 end
1164
1165 # Set creator
1166 fun creator=(creator: User) do json["creator"] = creator.json
1167
1168 # Due time in ISODate format (if any).
1169 fun due_on: nullable ISODate do
1170 var res = json.get_or_null("updated_at")
1171 if res isa String then return new ISODate.from_string(res)
1172 return null
1173 end
1174
1175 # Set due_on.
1176 fun due_on=(due_on: nullable ISODate) do
1177 if due_on == null then
1178 json["due_on"] = null
1179 else
1180 json["due_on"] = due_on.to_s
1181 end
1182 end
1183
1184 # Update time in ISODate format (if any).
1185 fun updated_at: nullable ISODate do
1186 var res = json.get_or_null("updated_at")
1187 if res isa String then return new ISODate.from_string(res)
1188 return null
1189 end
1190
1191 # Set updated_at.
1192 fun updated_at=(updated_at: nullable ISODate) do
1193 if updated_at == null then
1194 json["updated_at"] = null
1195 else
1196 json["updated_at"] = updated_at.to_s
1197 end
1198 end
1199
1200 # Close time in ISODate format (if any).
1201 fun closed_at: nullable ISODate do
1202 var res = json.get_or_null("closed_at")
1203 if res isa String then return new ISODate.from_string(res)
1204 return null
1205 end
1206
1207 # Set closed_at.
1208 fun closed_at=(closed_at: nullable ISODate) do
1209 if closed_at == null then
1210 json["closed_at"] = null
1211 else
1212 json["closed_at"] = closed_at.to_s
1213 end
1214 end
1215 end
1216
1217 # A Github comment
1218 #
1219 # There is two kinds of comments:
1220 #
1221 # * `CommitComment` are made on a commit page.
1222 # * `IssueComment` are made on an issue or pull request page.
1223 # * `ReviewComment` are made on the diff associated to a pull request.
1224 abstract class Comment
1225 super RepoEntity
1226
1227 # Identifier of this comment.
1228 var id: Int
1229
1230 redef init from_json(api, repo, json) do
1231 self.id = json["id"].as(Int)
1232 super
1233 end
1234
1235 # User that made this comment.
1236 fun user: User do
1237 return new User.from_json(api, json["user"].as(JsonObject))
1238 end
1239
1240 # Set user
1241 fun user=(user: User) do json["user"] = user.json
1242
1243 # Creation time in ISODate format.
1244 fun created_at: ISODate do
1245 return new ISODate.from_string(json["created_at"].as(String))
1246 end
1247
1248 # Set created_at
1249 fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
1250
1251 # Last update time in ISODate format (if any).
1252 fun updated_at: nullable ISODate do
1253 var res = json.get_or_null("updated_at")
1254 if res isa String then return new ISODate.from_string(res)
1255 return null
1256 end
1257
1258 # Set updated_at.
1259 fun updated_at=(updated_at: nullable ISODate) do
1260 if updated_at == null then
1261 json["updated_at"] = null
1262 else
1263 json["updated_at"] = updated_at.to_s
1264 end
1265 end
1266
1267 # Comment body text.
1268 fun body: String do return json["body"].as(String)
1269
1270 # Set body
1271 fun body=(body: String) do json["body"] = body
1272
1273 # Does the comment contain an acknowledgement (+1)
1274 fun is_ack: Bool
1275 do
1276 return body.has("\\+1\\b".to_re) or body.has(":+1:") or body.has(":shipit:")
1277 end
1278 end
1279
1280 # A comment made on a commit.
1281 class CommitComment
1282 super Comment
1283
1284 redef var key is lazy do return "{repo.key}/comments/{id}"
1285
1286 # Commented commit.
1287 fun commit: Commit do
1288 return api.load_commit(repo, json["commit_id"].as(String)).as(not null)
1289 end
1290
1291 # Set commit
1292 fun commit=(commit: Commit) do json["commit_id"] = commit.json
1293
1294 # Position of the comment on the line.
1295 fun position: nullable String do
1296 var res = json.get_or_null("position")
1297 if res isa String then return res
1298 return null
1299 end
1300
1301 # Set position.
1302 fun position=(position: nullable String) do json["position"] = position
1303
1304 # Line of the comment.
1305 fun line: nullable String do
1306 var res = json.get_or_null("line")
1307 if res isa String then return res
1308 return null
1309 end
1310
1311 # Set line.
1312 fun line=(line: nullable String) do json["line"] = line
1313
1314 # Path of the commented file.
1315 fun path: String do return json["path"].as(String)
1316
1317 # Set path.
1318 fun path=(path: String) do json["path"] = path
1319 end
1320
1321 # Comments made on Github issue and pull request pages.
1322 #
1323 # Should be accessed from `GithubAPI::load_issue_comment`.
1324 #
1325 # See <https://developer.github.com/v3/issues/comments/>.
1326 class IssueComment
1327 super Comment
1328
1329 redef var key is lazy do return "{repo.key}/issues/comments/{id}"
1330
1331 # Issue that contains `self`.
1332 fun issue: Issue do
1333 var number = issue_url.split("/").last.to_i
1334 return api.load_issue(repo, number).as(not null)
1335 end
1336
1337 # Link to the issue document on API.
1338 fun issue_url: String do return json["issue_url"].as(String)
1339
1340 # Set issue_url.
1341 fun issue_url=(issue_url: String) do json["issue_url"] = issue_url
1342 end
1343
1344 # Comments made on Github pull request diffs.
1345 #
1346 # Should be accessed from `GithubAPI::load_diff_comment`.
1347 #
1348 # See <https://developer.github.com/v3/pulls/comments/>.
1349 class ReviewComment
1350 super Comment
1351
1352 redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
1353
1354 # Pull request that contains `self`.
1355 fun pull: PullRequest do
1356 var number = pull_request_url.split("/").last.to_i
1357 return api.load_pull(repo, number).as(not null)
1358 end
1359
1360 # Link to the pull request on API.
1361 fun pull_request_url: String do return json["pull_request_url"].as(String)
1362
1363 # Set pull_request_url.
1364 fun pull_request_url=(pull_request_url: String) do json["pull_request_url"] = pull_request_url
1365
1366 # Diff hunk.
1367 fun diff_hunk: String do return json["diff_hunk"].as(String)
1368
1369 # Set diff_hunk.
1370 fun diff_hunk=(diff_hunk: String) do json["diff_hunk"] = diff_hunk
1371
1372 # Path of commented file.
1373 fun path: String do return json["path"].as(String)
1374
1375 # Set path.
1376 fun path=(path: String) do json["path"] = path
1377
1378 # Position of the comment on the file.
1379 fun position: Int do return json["position"].as(Int)
1380
1381 # Set position.
1382 fun position=(position: Int) do json["position"] = position
1383
1384 # Original position in the diff.
1385 fun original_position: Int do return json["original_position"].as(Int)
1386
1387 # Set original_position.
1388 fun original_position=(original_position: Int) do json["original_position"] = original_position
1389
1390 # Commit referenced by this comment.
1391 fun commit_id: String do return json["commit_id"].as(String)
1392
1393 # Set commit_id.
1394 fun commit_id=(commit_id: String) do json["commit_id"] = commit_id
1395
1396 # Original commit id.
1397 fun original_commit_id: String do return json["original_commit_id"].as(String)
1398
1399 # Set original_commit_id.
1400 fun original_commit_id=(commit_id: String) do json["original_commit_id"] = commit_id
1401 end
1402
1403 # An event that occurs on a Github `Issue`.
1404 #
1405 # Should be accessed from `GithubAPI::load_issue_event`.
1406 #
1407 # See <https://developer.github.com/v3/issues/events/>.
1408 class IssueEvent
1409 super RepoEntity
1410
1411 redef var key is lazy do return "{repo.key}/issues/events/{id}"
1412
1413 # Event id on Github.
1414 var id: Int
1415
1416 redef init from_json(api, repo, json) do
1417 self.id = json["id"].as(Int)
1418 super
1419 end
1420
1421 # Issue that contains `self`.
1422 fun issue: Issue do
1423 return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
1424 end
1425
1426 # Set issue.
1427 fun issue=(issue: Issue) do json["issue"] = issue.json
1428
1429 # User that initiated the event.
1430 fun actor: User do
1431 return new User.from_json(api, json["actor"].as(JsonObject))
1432 end
1433
1434 # Set actor.
1435 fun actor=(actor: User) do json["actor"] = actor.json
1436
1437 # Creation time in ISODate format.
1438 fun created_at: ISODate do
1439 return new ISODate.from_string(json["created_at"].as(String))
1440 end
1441
1442 # Set created_at.
1443 fun created_at=(created_at: ISODate) do json["created_at"] = created_at.to_s
1444
1445 # Event descriptor.
1446 fun event: String do return json["event"].as(String)
1447
1448 # Set event.
1449 fun event=(event: String) do json["event"] = event
1450
1451 # Commit linked to this event (if any).
1452 fun commit_id: nullable String do
1453 var res = json.get_or_null("commit_id")
1454 if res isa String then return res
1455 return null
1456 end
1457
1458 # Set commit_id.
1459 fun commit_id=(commit_id: nullable String) do json["commit_id"] = commit_id
1460
1461 # Label linked to this event (if any).
1462 fun labl: nullable Label do
1463 var res = json.get_or_null("label")
1464 if res isa JsonObject then return new Label.from_json(api, repo, res)
1465 return null
1466 end
1467
1468 # Set labl.
1469 fun labl=(labl: nullable Label) do
1470 if labl == null then
1471 json["labl"] = null
1472 else
1473 json["labl"] = labl.json
1474 end
1475 end
1476
1477 # User linked to this event (if any).
1478 fun assignee: nullable User do
1479 var res = json.get_or_null("assignee")
1480 if res isa JsonObject then return new User.from_json(api, res)
1481 return null
1482 end
1483
1484 # Set assignee.
1485 fun assignee=(assignee: nullable User) do
1486 if assignee == null then
1487 json["assignee"] = null
1488 else
1489 json["assignee"] = assignee.json
1490 end
1491 end
1492
1493 # Milestone linked to this event (if any).
1494 fun milestone: nullable Milestone do
1495 var res = json.get_or_null("milestone")
1496 if res isa JsonObject then return new Milestone.from_json(api, repo, res)
1497 return null
1498 end
1499
1500 # Set milestone.
1501 fun milestone=(milestone: nullable User) do
1502 if milestone == null then
1503 json["milestone"] = null
1504 else
1505 json["milestone"] = milestone.json
1506 end
1507 end
1508
1509 # Rename linked to this event (if any).
1510 fun rename: nullable RenameAction do
1511 var res = json.get_or_null("rename")
1512 if res isa JsonObject then return new RenameAction(res)
1513 return null
1514 end
1515
1516 # Set rename.
1517 fun rename=(rename: nullable User) do
1518 if rename == null then
1519 json["rename"] = null
1520 else
1521 json["rename"] = rename.json
1522 end
1523 end
1524 end
1525
1526 # A rename action maintains the name before and after a renaming action.
1527 class RenameAction
1528
1529 # JSON content.
1530 var json: JsonObject
1531
1532 # Name before renaming.
1533 fun from: String do return json["from"].as(String)
1534
1535 # Set from.
1536 fun from=(from: String) do json["from"] = from
1537
1538 # Name after renaming.
1539 fun to: String do return json["to"].as(String)
1540
1541 # Set to.
1542 fun to=(to: String) do json["to"] = to
1543 end
1544
1545 # Contributors list with additions, deletions, and commit counts.
1546 #
1547 # Should be accessed from `Repo::contrib_stats`.
1548 #
1549 # See <https://developer.github.com/v3/repos/statistics/>.
1550 class ContributorStats
1551 super Comparable
1552
1553 redef type OTHER: ContributorStats
1554
1555 # Github API client.
1556 var api: GithubAPI
1557
1558 # Json content.
1559 var json: JsonObject
1560
1561 # Init `self` from a `json` object.
1562 init from_json(api: GithubAPI, json: JsonObject) do
1563 self.api = api
1564 self.json = json
1565 end
1566
1567 # User these statistics are about.
1568 fun author: User do
1569 return new User.from_json(api, json["author"].as(JsonObject))
1570 end
1571
1572 # Set author.
1573 fun author=(author: User) do json["author"] = author.json
1574
1575 # Total number of commit.
1576 fun total: Int do return json["total"].as(Int)
1577
1578 # Set total.
1579 fun total=(total: Int) do json["total"] = total
1580
1581 # Are of weeks of activity with detailed statistics.
1582 fun weeks: JsonArray do return json["weeks"].as(JsonArray)
1583
1584 # Set weeks.
1585 fun weeks=(weeks: JsonArray) do json["weeks"] = weeks
1586
1587 # ContributorStats can be compared on the total amount of commits.
1588 redef fun <(o) do return total < o.total
1589 end
1590
1591 # A Github file representation.
1592 #
1593 # Mostly a wrapper around a json object.
1594 class GithubFile
1595
1596 # Json content.
1597 var json: JsonObject
1598
1599 # File name.
1600 fun filename: String do return json["filename"].as(String)
1601
1602 # Set filename.
1603 fun filename=(filename: String) do json["filename"] = filename
1604 end