ee62031b8da58801041d91bdd5b4e6343b5d2cc5
[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 intrude import json::serialization_read
23
24 import base64
25 import curl
26 import json
27
28 # Client to Github API
29 #
30 # To access the API you need an instance of a `GithubAPI` client.
31 #
32 # ~~~nitish
33 # # Get Github authentification token.
34 # var token = get_github_oauth
35 # assert not token.is_empty
36 #
37 # # Init the client.
38 # var api = new GithubAPI(token)
39 # ~~~
40 #
41 # The API client allows you to get Github API entities.
42 #
43 # ~~~nitish
44 # var repo = api.get_repo("nitlang/nit")
45 # assert repo != null
46 # assert repo.name == "nit"
47 #
48 # var user = api.get_user("Morriar")
49 # assert user != null
50 # assert user.login == "Morriar"
51 # ~~~
52 class GithubAPI
53
54 # Github API OAuth token
55 #
56 # To access your private ressources, you must
57 # [authenticate](https://developer.github.com/guides/basics-of-authentication/).
58 #
59 # For client applications, Github recommands to use the
60 # [OAuth tokens](https://developer.github.com/v3/oauth/) authentification method.
61 #
62 #
63 #
64 # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
65 # associated to the key.
66 var auth: nullable String = null is optional
67
68 # User agent used for HTTP requests.
69 #
70 # Default is `nit_github_api`.
71 #
72 # See <https://developer.github.com/v3/#user-agent-required>
73 var user_agent: String = "nit_github_api" is optional
74
75 # Headers to use on all requests
76 fun new_headers: HeaderMap do
77 var map = new HeaderMap
78 var auth = self.auth
79 if auth != null then
80 map["Authorization"] = "token {auth}"
81 end
82 map["User-Agent"] = user_agent
83 # FIXME remove when projects and team are no more in beta
84 map["Accept"] = "application/vnd.github.inertia-preview+json"
85 map["Accept"] = "application/vnd.github.hellcat-preview+json"
86 return map
87 end
88
89 # Github API base url.
90 #
91 # Default is `https://api.github.com` and should not be changed.
92 var api_url = "https://api.github.com"
93
94 # Verbosity level.
95 #
96 # * `0`: only errors (default)
97 # * `1`: verbose
98 var verbose_lvl = 0 is public writable
99
100 # Send a HTTPRequest to the Github API
101 fun send(method, path: String, headers: nullable HeaderMap, body: nullable String): nullable String do
102 last_error = null
103 path = sanitize_uri(path)
104 var uri = "{api_url}{path}"
105 var request = new CurlHTTPRequest(uri)
106 request.method = method
107 request.user_agent = user_agent
108 request.headers = headers or else self.new_headers
109 request.body = body
110 return check_response(uri, request.execute)
111 end
112
113 private fun check_response(uri: String, response: CurlResponse): nullable String do
114 if response isa CurlResponseSuccess then
115 was_error = false
116 return response.body_str
117 else if response isa CurlResponseFailed then
118 last_error = new GithubAPIError(
119 response.error_msg,
120 response.error_code,
121 uri
122 )
123 was_error = true
124 return null
125 else abort
126 end
127
128 # Deserialize an object
129 fun deserialize(string: nullable Serializable): nullable Object do
130 if string == null then return null
131 var deserializer = new GithubDeserializer(string.to_s)
132 var res = deserializer.deserialize
133 if deserializer.errors.not_empty then
134 was_error = true
135 last_error = new GithubDeserializerErrors("Deserialization failed", deserializer.errors)
136 return null
137 else if res isa GithubError then
138 was_error = true
139 last_error = res
140 return null
141 end
142 was_error = false
143 return res
144 end
145
146 # Display a message depending on `verbose_lvl`.
147 fun message(lvl: Int, message: String) do
148 if lvl <= verbose_lvl then print message
149 end
150
151 # Escape `uri` in an acceptable format for Github.
152 private fun sanitize_uri(uri: String): String do
153 # TODO better URI escape.
154 return uri.replace(" ", "%20")
155 end
156
157 # Last error occured during Github API communications.
158 var last_error: nullable Error = null is public writable
159
160 # Does the last request provoqued an error?
161 var was_error = false is protected writable
162
163 # Execute a GET request on Github API.
164 #
165 # This method returns a deserialized result.
166 #
167 # For raw data see `send`.
168 #
169 # ~~~nitish
170 # var api = new GithubAPI(get_github_oauth)
171 # var obj = api.get("/repos/nitlang/nit")
172 # assert obj isa Repo
173 # assert obj.name == "nit"
174 # ~~~
175 #
176 # Returns `null` in case of `error`.
177 #
178 # ~~~nitish
179 # obj = api.get("/foo/bar/baz")
180 # assert obj == null
181 # assert api.was_error
182 # assert api.last_error isa GithubError
183 # ~~~
184 fun get(path: String, headers: nullable HeaderMap, data: nullable String): nullable Object do
185 return deserialize(send("GET", path, headers, data))
186 end
187
188 # Get the Github logged user from `auth` token.
189 #
190 # Loads the `User` from the API or returns `null` if the user cannot be found.
191 #
192 # ~~~nitish
193 # var api = new GithubAPI(get_github_oauth)
194 # var user = api.get_auth_user
195 # assert user.login == "Morriar"
196 # ~~~
197 fun get_auth_user: nullable User do
198 return get("/user").as(nullable User)
199 end
200
201 # Get the Github user with `login`
202 #
203 # Loads the `User` from the API or returns `null` if the user cannot be found.
204 #
205 # ~~~nitish
206 # var api = new GithubAPI(get_github_oauth)
207 # var user = api.get_user("Morriar")
208 # print user or else "null"
209 # assert user.login == "Morriar"
210 # ~~~
211 fun get_user(login: String): nullable User do
212 return get("/users/{login}").as(nullable User)
213 end
214
215 # Get the Github repo with `full_name`.
216 #
217 # Loads the `Repo` from the API or returns `null` if the repo cannot be found.
218 #
219 # ~~~nitish
220 # var api = new GithubAPI(get_github_oauth)
221 # var repo = api.get_repo("nitlang/nit")
222 # assert repo.name == "nit"
223 # assert repo.owner.login == "nitlang"
224 # assert repo.default_branch == "master"
225 # ~~~
226 fun get_repo(repo_slug: String): nullable Repo do
227 return get("/repos/{repo_slug}").as(nullable Repo)
228 end
229
230 # List of repo branches.
231 #
232 # Pagination:
233 # * `page`: page to fetch (default: 1)
234 # * `per_page`: number of branches by page (default: 30)
235 fun get_repo_branches(repo_slug: String, page, per_page: nullable Int): Array[Branch] do
236 return new GithubArray[Branch].from(get(
237 "/repos/{repo_slug}/branches?{pagination(page, per_page)}"))
238 end
239
240 # List of issues associated with their ids.
241 fun get_repo_issues(repo_slug: String, page, per_page: nullable Int): Array[Issue] do
242 return new GithubArray[Issue].from(get(
243 "/repos/{repo_slug}/issues?{pagination(page, per_page)}"))
244 end
245
246 # Search issues in this repo form an advanced query.
247 #
248 # Example:
249 #
250 # ~~~nitish
251 # var issues = repo.search_issues("is:open label:need_review")
252 # ~~~
253 #
254 # See <https://developer.github.com/v3/search/#search-issues>.
255 fun search_repo_issues(repo_slug: String, query: String, page, per_page: nullable Int): nullable SearchResults do
256 return get("/search/issues?q={query} repo:{repo_slug}&{pagination(page, per_page)}").as(nullable SearchResults)
257 end
258
259 # List of labels associated with their names.
260 fun get_repo_labels(repo_slug: String, page, per_page: nullable Int): Array[Label] do
261 return new GithubArray[Label].from(get(
262 "/repos/{repo_slug}/labels?{pagination(page, per_page)}"))
263 end
264
265 # List of milestones associated with their ids.
266 fun get_repo_milestones(repo_slug: String, page, per_page: nullable Int): Array[Milestone] do
267 return new GithubArray[Milestone].from(get(
268 "/repos/{repo_slug}/milestones?{pagination(page, per_page)}"))
269 end
270
271 # List of pull-requests associated with their ids.
272 #
273 # Implementation notes: because PR numbers are not consecutive,
274 # PR are loaded from pages.
275 # See: https://developer.github.com/v3/pulls/#list-pull-requests
276 fun get_repo_pulls(repo_slug: String, page, per_page: nullable Int): Array[PullRequest] do
277 return new GithubArray[PullRequest].from(get(
278 "/repos/{repo_slug}/pulls?{pagination(page, per_page)}"))
279 end
280
281 # List of contributor related statistics.
282 fun get_repo_contrib_stats(repo_slug: String): Array[ContributorStats] do
283 return new GithubArray[ContributorStats].from(get("/repos/{repo_slug}/stats/contributors"))
284 end
285
286 # Get the Github branch with `name`.
287 #
288 # Returns `null` if the branch cannot be found.
289 #
290 # ~~~nitish
291 # var api = new GithubAPI(get_github_oauth)
292 # var repo = api.get_repo("nitlang/nit")
293 # assert repo != null
294 # var branch = api.get_branch(repo, "master")
295 # assert branch.name == "master"
296 # assert branch.commit isa Commit
297 # ~~~
298 fun get_branch(repo_slug: String, name: String): nullable Branch do
299 return get("/repos/{repo_slug}/branches/{name}").as(nullable Branch)
300 end
301
302 # Get the Github commit with `sha`.
303 #
304 # Returns `null` if the commit cannot be found.
305 #
306 # ~~~nitish
307 # var api = new GithubAPI(get_github_oauth)
308 # var repo = api.get_repo("nitlang/nit")
309 # assert repo != null
310 # var commit = api.get_commit(repo, "64ce1f")
311 # assert commit isa Commit
312 # ~~~
313 fun get_commit(repo_slug: String, sha: String): nullable Commit do
314 return get("/repos/{repo_slug}/commits/{sha}").as(nullable Commit)
315 end
316
317 # Get the status of a commit
318 #
319 # The status holds the result of each check ran on a commit like CI, reviews etc.
320 fun get_commit_status(repo_slug: String, sha: String): nullable CommitStatus do
321 return get("/repos/{repo_slug}/commits/{sha}/status").as(nullable CommitStatus)
322 end
323
324 # Get the Github issue #`number`.
325 #
326 # Returns `null` if the issue cannot be found.
327 #
328 # ~~~nitish
329 # var api = new GithubAPI(get_github_oauth)
330 # var repo = api.get_repo("nitlang/nit")
331 # assert repo != null
332 # var issue = api.get_issue(repo, 1)
333 # assert issue.title == "Doc"
334 # ~~~
335 fun get_issue(repo_slug: String, number: Int): nullable Issue do
336 return get("/repos/{repo_slug}/issues/{number}").as(nullable Issue)
337 end
338
339 # List of event on this issue.
340 fun get_issue_comments(repo_slug: String, issue_number: Int, page, per_page: nullable Int): Array[IssueComment] do
341 return new GithubArray[IssueComment].from(get(
342 "/repos/{repo_slug}/issues/{issue_number}/comments?{pagination(page, per_page)}"))
343 end
344
345 # List of events on this issue.
346 fun get_issue_events(repo_slug: String, issue_number: Int, page, per_page: nullable Int): Array[IssueEvent] do
347 return new GithubArray[IssueEvent].from(get(
348 "/repos/{repo_slug}/issues/{issue_number}/events?{pagination(page, per_page)}"))
349 end
350
351 # Get the Github pull request #`number`.
352 #
353 # Returns `null` if the pull request cannot be found.
354 #
355 # ~~~nitish
356 # var api = new GithubAPI(get_github_oauth)
357 # var repo = api.get_repo("nitlang/nit")
358 # assert repo != null
359 # var pull = api.get_pull(repo, 1)
360 # assert pull.title == "Doc"
361 # assert pull.user.login == "Morriar"
362 # ~~~
363 fun get_pull(repo_slug: String, number: Int): nullable PullRequest do
364 return get("/repos/{repo_slug}/pulls/{number}").as(nullable PullRequest)
365 end
366
367 # List of comments on a pull request
368 fun get_pull_comments(repo_slug: String, pull_number: Int, page, per_page: nullable Int): Array[PullComment] do
369 return new GithubArray[PullComment].from(get(
370 "/repos/{repo_slug}/pulls/{pull_number}/comments?{pagination(page, per_page)}"))
371 end
372
373 # Get a specific pull request comment
374 fun get_pull_comment(repo_slug: String, id: Int): nullable PullComment do
375 return get("/repos/{repo_slug}/pulls/comments/{id}").as(nullable PullComment)
376 end
377
378 # Get the Github label with `name`.
379 #
380 # Returns `null` if the label cannot be found.
381 #
382 # ~~~nitish
383 # var api = new GithubAPI(get_github_oauth)
384 # var repo = api.get_repo("nitlang/nit")
385 # assert repo != null
386 # var labl = api.get_label(repo, "ok_will_merge")
387 # assert labl != null
388 # ~~~
389 fun get_label(repo_slug: String, name: String): nullable Label do
390 return get("/repos/{repo_slug}/labels/{name}").as(nullable Label)
391 end
392
393 # Get the Github milestone with `id`.
394 #
395 # Returns `null` if the milestone cannot be found.
396 #
397 # ~~~nitish
398 # var api = new GithubAPI(get_github_oauth)
399 # var repo = api.get_repo("nitlang/nit")
400 # assert repo != null
401 # var stone = api.get_milestone(repo, 4)
402 # assert stone.title == "v1.0prealpha"
403 # ~~~
404 fun get_milestone(repo_slug: String, id: Int): nullable Milestone do
405 return get("/repos/{repo_slug}/milestones/{id}").as(nullable Milestone)
406 end
407
408 # Get the Github issue event with `id`.
409 #
410 # Returns `null` if the event cannot be found.
411 #
412 # ~~~nitish
413 # var api = new GithubAPI(get_github_oauth)
414 # var repo = api.get_repo("nitlang/nit")
415 # assert repo isa Repo
416 # var event = api.get_issue_event(repo, 199674194)
417 # assert event isa IssueEvent
418 # assert event.actor.login == "privat"
419 # assert event.event == "labeled"
420 # assert event.labl isa Label
421 # assert event.labl.name == "need_review"
422 # ~~~
423 fun get_issue_event(repo_slug: String, id: Int): nullable IssueEvent do
424 return get("/repos/{repo_slug}/issues/events/{id}").as(nullable IssueEvent)
425 end
426
427 # Get the Github commit comment with `id`.
428 #
429 # Returns `null` if the comment cannot be found.
430 #
431 # ~~~nitish
432 # var api = new GithubAPI(get_github_oauth)
433 # var repo = api.get_repo("nitlang/nit")
434 # assert repo != null
435 # var comment = api.get_commit_comment(repo, 8982707)
436 # assert comment.user.login == "Morriar"
437 # assert comment.body == "For testing purposes...\n"
438 # assert comment.commit_id == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
439 # ~~~
440 fun get_commit_comment(repo_slug: String, id: Int): nullable CommitComment do
441 return get("/repos/{repo_slug}/comments/{id}").as(nullable CommitComment)
442 end
443
444 # Get the Github issue comment with `id`.
445 #
446 # Returns `null` if the comment cannot be found.
447 #
448 # ~~~nitish
449 # var api = new GithubAPI(get_github_oauth)
450 # var repo = api.get_repo("nitlang/nit")
451 # assert repo != null
452 # var comment = api.get_issue_comment(repo, 6020149)
453 # assert comment.user.login == "privat"
454 # assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
455 # assert comment.issue_number == 10
456 # ~~~
457 fun get_issue_comment(repo_slug: String, id: Int): nullable IssueComment do
458 return get("/repos/{repo_slug}/issues/comments/{id}").as(nullable IssueComment)
459 end
460
461 private fun pagination(page, per_page: nullable Int): String do
462 return "page={page or else 1}&per_page={per_page or else 30}"
463 end
464 end
465
466 # Return deserialization as an array of E
467 #
468 # Non-subtypes will be ignored.
469 private class GithubArray[E]
470 super Array[E]
471
472 # Create `self` from an Array of objects
473 #
474 # Objects non-subtyping E will be ignored.
475 init from(res: nullable Object) do
476 if not res isa Array[Object] then return
477 for obj in res do
478 if obj isa E then add obj
479 end
480 end
481 end
482
483 # An Error returned by GithubAPI
484 class GithubError
485 super Error
486 end
487
488 # An Error returned by https://api.github.com
489 #
490 # Anything that can occurs when sending request to the API:
491 # * Can't connect to API
492 # * Ressource not found
493 # * Validation error
494 # * ...
495 class GithubAPIError
496 super GithubError
497
498 # Status code obtained
499 var status_code: Int
500
501 # URI that returned the error
502 var requested_uri: String
503 end
504
505 # An Error returned while deserializing objects from the API
506 class GithubDeserializerErrors
507 super GithubError
508
509 # Errors returned by the deserizalization process
510 var deserizalization_errors: Array[Error]
511 end
512
513 # A Github user
514 #
515 # Provides access to [Github user data](https://developer.github.com/v3/users/).
516 # Should be accessed from `GithubAPI::get_user`.
517 class User
518 super GitUser
519 serialize
520
521 # Github login.
522 var login: String is writable
523
524 # Avatar image url for this user.
525 var avatar_url: nullable String is writable
526
527 # User public name if any.
528 var name: nullable String is writable
529
530 # User public email if any.
531 var email: nullable String is writable
532
533 # User public blog if any.
534 var blog: nullable String is writable
535 end
536
537 # A Github repository.
538 #
539 # Provides access to [Github repo data](https://developer.github.com/v3/repos/).
540 # Should be accessed from `GithubAPI::get_repo`.
541 class Repo
542 serialize
543
544 # Repo full name on Github.
545 var full_name: String is writable
546
547 # Repo short name on Github.
548 var name: String is writable
549
550 # Get the repo owner.
551 var owner: User is writable
552
553 # Repo default branch name.
554 var default_branch: nullable String = null is optional, writable
555 end
556
557 # A Github branch.
558 #
559 # Should be accessed from `GithubAPI::get_branch`.
560 #
561 # See <https://developer.github.com/v3/repos/#list-branches>.
562 class Branch
563 serialize
564
565 # Branch name.
566 var name: String is writable
567
568 # Get the last commit of `self`.
569 var commit: Commit is writable
570 end
571
572 # A Github commit.
573 #
574 # Should be accessed from `GithubAPI::get_commit`.
575 #
576 # See <https://developer.github.com/v3/repos/commits/>.
577 class Commit
578 serialize
579
580 # Commit SHA.
581 var sha: String is writable
582
583 # Parent commits of `self`.
584 var parents: nullable Array[Commit] = null is writable
585
586 # Author of the commit.
587 var author: nullable GitUser is writable
588
589 # Committer of the commit.
590 var committer: nullable GitUser is writable
591
592 # Authoring date as String.
593 var author_date: nullable String is writable
594
595 # Commit date as String.
596 var commit_date: nullable String is writable
597
598 # List files staged in this commit.
599 var files: nullable Array[GithubFile] = null is optional, writable
600
601 # Commit message.
602 var message: nullable String is writable
603
604 # Git commit representation linked to this commit.
605 var commit: nullable GitCommit
606 end
607
608 # A Git Commit representation
609 class GitCommit
610 serialize
611
612 # Commit SHA.
613 var sha: nullable String is writable
614
615 # Parent commits of `self`.
616 var parents: nullable Array[GitCommit] = null is writable
617
618 # Author of the commit.
619 var author: nullable GitUser is writable
620
621 # Committer of the commit.
622 var committer: nullable GitUser is writable
623
624 # Commit message.
625 var message: nullable String is writable
626 end
627
628 # Git user authoring data
629 class GitUser
630 serialize
631
632 # Authoring date.
633 var date: nullable String = null is writable
634 end
635
636 # A Github issue.
637 #
638 # Should be accessed from `GithubAPI::get_issue`.
639 #
640 # See <https://developer.github.com/v3/issues/>.
641 class Issue
642 serialize
643
644 # Issue Github ID.
645 var number: Int is writable
646
647 # Issue id.
648 var id: nullable Int is writable
649
650 # Issue title.
651 var title: String is writable
652
653 # User that created this issue.
654 var user: nullable User is writable
655
656 # List of labels on this issue associated to their names.
657 var labels: nullable Array[Label] is writable
658
659 # State of the issue on Github.
660 var state: String is writable
661
662 # Is the issue locked?
663 var locked: nullable Bool is writable
664
665 # Assigned `User` (if any).
666 var assignee: nullable User is writable
667
668 # `Milestone` (if any).
669 var milestone: nullable Milestone is writable
670
671 # Number of comments on this issue.
672 var comments: nullable Int is writable
673
674 # Creation time as String.
675 var created_at: String is writable
676
677 # Last update time as String (if any).
678 var updated_at: nullable String is writable
679
680 # Close time as String (if any).
681 var closed_at: nullable String is writable
682
683 # Full description of the issue.
684 var body: nullable String is writable
685
686 # User that closed this issue (if any).
687 var closed_by: nullable User is writable
688
689 # Is this issue linked to a pull request?
690 var is_pull_request: Bool = false is writable
691 end
692
693 # A Github pull request.
694 #
695 # Should be accessed from `GithubAPI::get_pull`.
696 #
697 # PullRequest are basically Issues with more data.
698 # See <https://developer.github.com/v3/pulls/>.
699 class PullRequest
700 super Issue
701 serialize
702
703 # Merge time as String (if any).
704 var merged_at: nullable String is writable
705
706 # Merge commit SHA.
707 var merge_commit_sha: nullable String is writable
708
709 # Count of comments made on the pull request diff.
710 var review_comments: nullable Int is writable
711
712 # Pull request head (can be a commit SHA or a branch name).
713 var head: PullRef is writable
714
715 # Pull request base (can be a commit SHA or a branch name).
716 var base: PullRef is writable
717
718 # Is this pull request merged?
719 var merged: nullable Bool is writable
720
721 # Is this pull request mergeable?
722 var mergeable: nullable Bool is writable
723
724 # Mergeable state of this pull request.
725 #
726 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
727 var mergeable_state: nullable String is writable
728
729 # User that merged this pull request (if any).
730 var merged_by: nullable User is writable
731
732 # Count of commits in this pull request.
733 var commits: nullable Int is writable
734
735 # Added line count.
736 var additions: nullable Int is writable
737
738 # Deleted line count.
739 var deletions: nullable Int is writable
740
741 # Changed files count.
742 var changed_files: nullable Int is writable
743
744 # URL to patch file
745 var patch_url: nullable String is writable
746 end
747
748 # A pull request reference (used for head and base).
749 class PullRef
750 serialize
751
752 # Label pointed by `self`.
753 var labl: String is writable, serialize_as("label")
754
755 # Reference pointed by `self`.
756 var ref: String is writable
757
758 # Commit SHA pointed by `self`.
759 var sha: String is writable
760
761 # User pointed by `self`.
762 var user: User is writable
763
764 # Repo pointed by `self` (if any).
765 #
766 # A `null` value means the `repo` was deleted.
767 var repo: nullable Repo is writable
768 end
769
770 # A Github label.
771 #
772 # Should be accessed from `GithubAPI::get_label`.
773 #
774 # See <https://developer.github.com/v3/issues/labels/>.
775 class Label
776 serialize
777
778 # Label name.
779 var name: String is writable
780
781 # Label color code.
782 var color: String is writable
783 end
784
785 # A Github milestone.
786 #
787 # Should be accessed from `GithubAPI::get_milestone`.
788 #
789 # See <https://developer.github.com/v3/issues/milestones/>.
790 class Milestone
791 serialize
792
793 # The milestone id on Github.
794 var number: nullable Int = null is writable
795
796 # Milestone title.
797 var title: String is writable
798
799 # Milestone long description.
800 var description: nullable String is writable
801
802 # Count of opened issues linked to this milestone.
803 var open_issues: nullable Int = null is writable
804
805 # Count of closed issues linked to this milestone.
806 var closed_issues: nullable Int = null is writable
807
808 # Milestone state.
809 var state: nullable String is writable
810
811 # Creation time as String.
812 var created_at: nullable String is writable
813
814 # User that created this milestone.
815 var creator: nullable User is writable
816
817 # Due time as String (if any).
818 var due_on: nullable String is writable
819
820 # Last update time as String (if any).
821 var updated_at: nullable String is writable
822
823 # Close time as String (if any).
824 var closed_at: nullable String is writable
825 end
826
827 # A Github comment
828 #
829 # There is two kinds of comments:
830 #
831 # * `CommitComment` are made on a commit page.
832 # * `IssueComment` are made on an issue or pull request page.
833 # * `PullComment` are made on the diff associated to a pull request.
834 abstract class Comment
835 serialize
836
837 # Identifier of this comment.
838 var id: Int is writable
839
840 # User that made this comment.
841 var user: User is writable
842
843 # Creation time as String.
844 var created_at: String is writable
845
846 # Last update time as String (if any).
847 var updated_at: nullable String is writable
848
849 # Comment body text.
850 var body: String is writable
851
852 # Does the comment contain an acknowledgement (+1)
853 fun is_ack: Bool do
854 return body.has("\\+1\\b".to_re) or body.has(":+1:") or body.has(":shipit:")
855 end
856 end
857
858 # A comment made on a commit.
859 class CommitComment
860 super Comment
861 serialize
862
863 # Commented commit.
864 var commit_id: String is writable
865
866 # Position of the comment on the line.
867 var position: nullable Int is writable
868
869 # Line of the comment.
870 var line: nullable Int is writable
871
872 # Path of the commented file.
873 var path: nullable String is writable
874 end
875
876 # Status of a commit
877 #
878 # Can contain sub-status for reviews, CI etc.
879 class CommitStatus
880 serialize
881
882 # Global state of this commit
883 var state: nullable String = null is optional, writable
884
885 # Sha of the commit this status is for
886 var sha: nullable String = null is optional, writable
887
888 # Repository the commit belongs to
889 var repository: nullable Repo = null is optional, writable
890
891 # All sub statuses (one for each check)
892 var statuses = new Array[RepoStatus] is optional, writable
893
894 # Total count of sub statuses
895 var total_count: nullable Int = null is optional, writable
896 end
897
898 # Sub status of a CommitStatus
899 #
900 # Represents a check applied to a commit (reviews, CI, ...).
901 class RepoStatus
902 serialize
903
904 # State of this check
905 var state: nullable String = null is optional, writable
906
907 # Description of this check
908 var description: nullable String = null is optional, writable
909
910 # External URL
911 var target_url: nullable String = null is optional, writable
912
913 # Context this status is related to
914 #
915 # Used to hold the name of the check applied.
916 var context: nullable String = null is optional, writable
917
918 # Date when this status was created
919 var created_at: nullable String = null is optional, writable
920
921 # Last date this status was updated
922 var updated_at: nullable String = null is optional, writable
923 end
924
925 # Comments made on Github issue and pull request pages.
926 #
927 # Should be accessed from `GithubAPI::get_issue_comment`.
928 #
929 # See <https://developer.github.com/v3/issues/comments/>.
930 class IssueComment
931 super Comment
932 serialize
933
934 # Issue that contains `self`.
935 fun issue_number: Int do return issue_url.split("/").last.to_i
936
937 # Link to the issue document on API.
938 var issue_url: String is writable
939 end
940
941 # Comments made on Github pull request diffs.
942 #
943 # Should be accessed from `GithubAPI::get_diff_comment`.
944 #
945 # See <https://developer.github.com/v3/pulls/comments/>.
946 class PullComment
947 super Comment
948 serialize
949
950 # Pull request that contains `self`.
951 fun pull_number: Int do return pull_request_url.split("/").last.to_i
952
953 # Link to the pull request on API.
954 var pull_request_url: String is writable
955
956 # Diff hunk.
957 var diff_hunk: String is writable
958
959 # Path of commented file.
960 var path: String is writable
961
962 # Position of the comment on the file.
963 var position: nullable Int is writable
964
965 # Original position in the diff.
966 var original_position: Int is writable
967
968 # Commit referenced by this comment.
969 var commit_id: String is writable
970
971 # Original commit id.
972 var original_commit_id: String is writable
973 end
974
975 # An event that occurs on a Github `Issue`.
976 #
977 # Should be accessed from `GithubAPI::get_issue_event`.
978 #
979 # See <https://developer.github.com/v3/issues/events/>.
980 class IssueEvent
981 serialize
982
983 # Event id on Github.
984 var id: Int is writable
985
986 # User that initiated the event.
987 var actor: User is writable
988
989 # Creation time as String.
990 var created_at: String is writable
991
992 # Event descriptor.
993 var event: String is writable
994
995 # Commit linked to this event (if any).
996 var commit_id: nullable String is writable
997
998 # Label linked to this event (if any).
999 var labl: nullable Label is writable, serialize_as("label")
1000
1001 # User linked to this event (if any).
1002 var assignee: nullable User is writable
1003
1004 # Milestone linked to this event (if any).
1005 var milestone: nullable Milestone is writable
1006
1007 # Rename linked to this event (if any).
1008 var rename: nullable RenameAction is writable
1009 end
1010
1011 # A rename action maintains the name before and after a renaming action.
1012 class RenameAction
1013 serialize
1014
1015 # Name before renaming.
1016 var from: String is writable
1017
1018 # Name after renaming.
1019 var to: String is writable
1020 end
1021
1022 #
1023 # Should be accessed from `Repo::contrib_stats`.
1024 #
1025 # See <https://developer.github.com/v3/repos/statistics/>.
1026 class ContributorStats
1027 super Comparable
1028 serialize
1029
1030 redef type OTHER: ContributorStats
1031
1032 # User these statistics are about.
1033 var author: User is writable
1034
1035 # Total number of commit.
1036 var total: Int is writable
1037
1038 # Array of weeks of activity with detailed statistics.
1039 var weeks: Array[ContributorWeek] is writable
1040
1041 # ContributorStats can be compared on the total amount of commits.
1042 redef fun <(o) do return total < o.total
1043 end
1044
1045 # Contributor stats weekly hash
1046 class ContributorWeek
1047 serialize
1048
1049 # Start of week given a Unix timestamp
1050 var w: Int
1051
1052 # Number of additions
1053 var a: Int
1054
1055 # Number of deletions
1056 var d: Int
1057
1058 # Number of commits
1059 var c: Int
1060 end
1061
1062 # A Github file representation.
1063 #
1064 # Mostly a wrapper around a json object.
1065 class GithubFile
1066 serialize
1067
1068 # File name.
1069 var filename: String is writable
1070 end
1071
1072 # A list of results returned buy `/search`
1073 class SearchResults
1074 serialize
1075
1076 # Total count with other pages
1077 var total_count: Int
1078
1079 # Does this page contain all the results?
1080 var incomplete_results: Bool
1081
1082 # Results in this page
1083 var items: Array[Object]
1084 end
1085
1086 # JsonDeserializer specific for Github objects.
1087 class GithubDeserializer
1088 super JsonDeserializer
1089
1090 private var pattern_base = "https://api.github.com"
1091
1092 # Url patterns to class names
1093 var url_patterns: Map[Regex, String] is lazy do
1094 var map = new HashMap[Regex, String]
1095 map["{pattern_base}/users/[^/]*$".to_re] = "User"
1096 map["{pattern_base}/repos/[^/]*/[^/]*$".to_re] = "Repo"
1097 map["{pattern_base}/repos/[^/]*/[^/]*/labels/[^/]+$".to_re] = "Label"
1098 map["{pattern_base}/repos/[^/]*/[^/]*/milestones/[0-9]+$".to_re] = "Milestone"
1099 map["{pattern_base}/repos/[^/]*/[^/]*/issues/[0-9]+$".to_re] = "Issue"
1100 map["{pattern_base}/repos/[^/]*/[^/]*/issues/comments/[0-9]+$".to_re] = "IssueComment"
1101 map["{pattern_base}/repos/[^/]*/[^/]*/issues/events/[0-9]+$".to_re] = "IssueEvent"
1102 map["{pattern_base}/repos/[^/]*/[^/]*/pulls/[0-9]+$".to_re] = "PullRequest"
1103 map["{pattern_base}/repos/[^/]*/[^/]*/pulls/comments/[0-9]+$".to_re] = "PullComment"
1104 map["{pattern_base}/repos/[^/]*/[^/]*/comments/[0-9]+$".to_re] = "CommitComment"
1105 map["{pattern_base}/repos/[^/]*/[^/]*/commits/[a-f0-9]+$".to_re] = "Commit"
1106 map["{pattern_base}/repos/[^/]*/[^/]*/commits/[a-f0-9]+/status$".to_re] = "CommitStatus"
1107 map["{pattern_base}/repos/[^/]*/[^/]*/statuses/[a-f0-9]+$".to_re] = "RepoStatus"
1108 return map
1109 end
1110
1111 # Match `url` property in object to a class name
1112 fun url_heuristic(raw: Map[String, nullable Object]): nullable String do
1113 if not raw.has_key("url") then return null
1114
1115 var url = raw["url"].as(String)
1116 for re, class_name in url_patterns do
1117 if url.has(re) then return class_name
1118 end
1119 return null
1120 end
1121
1122 redef fun class_name_heuristic(raw) do
1123 # Try with url
1124 var class_name = url_heuristic(raw)
1125 if class_name != null then return class_name
1126
1127 # print raw.serialize_to_json(true, true) # debug
1128
1129 # Use properties heuristics
1130 if raw.has_key("name") and raw.has_key("commit") then
1131 return "Branch"
1132 else if raw.has_key("total_count") and raw.has_key("items") then
1133 return "SearchResults"
1134 else if raw.has_key("total") and raw.has_key("weeks") then
1135 return "ContributorStats"
1136 else if raw.has_key("a") and raw.has_key("d") and raw.has_key("c") then
1137 return "ContributorWeek"
1138 end
1139 return null
1140 end
1141
1142 redef fun deserialize_class(name) do
1143 if name == "Issue" then
1144 var issue = super.as(Issue)
1145 if path.last.has_key("pull_request") then
1146 issue.is_pull_request = true
1147 end
1148 return issue
1149 else if name == "Commit" then
1150 var commit = super.as(Commit)
1151 var git_commit = commit.commit
1152 if git_commit != null then commit.message = git_commit.message
1153 return commit
1154 end
1155 return super
1156 end
1157 end
1158
1159 # Gets the Github token from `git` configuration
1160 #
1161 # Return the value of `git config --get github.oauthtoken`
1162 # or `""` if no key exists.
1163 fun get_github_oauth: String
1164 do
1165 var p = new ProcessReader("git", "config", "--get", "github.oauthtoken")
1166 var token = p.read_line
1167 p.wait
1168 p.close
1169 return token.trim
1170 end