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