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