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