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