lib/github: handles commits
[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.
16 #
17 # This modules reifies Github API elements as Nit classes.
18 #
19 # For most use-cases you need to use the `GithubAPI` client.
20 module api
21
22 import github_curl
23
24 # Interface to Github REST API.
25 #
26 # Used by all `GithubEntity` to perform requests.
27 #
28 # Usage:
29 #
30 # ~~~
31 # # Get Github authentification token.
32 # var token = get_github_oauth
33 # assert not token.is_empty
34 #
35 # # Init the client.
36 # var api = new GithubAPI(token)
37 # ~~~
38 #
39 # The API client allows to get Github API entities:
40 #
41 # ~~~
42 # var repo = api.load_repo("privat/nit")
43 # assert repo isa Repo
44 # assert repo.name == "nit"
45 #
46 # var user = api.load_user("Morriar")
47 # assert user isa User
48 # assert user.login == "Morriar"
49 # ~~~
50 class GithubAPI
51
52 # Github API OAuth token.
53 #
54 # This token is used to authenticate the application on Github API.
55 # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
56 # associated to the key.
57 var auth: String
58
59 # Github API base url.
60 #
61 # Default is `https://api.github.com` and should not be changed.
62 var api_url = "https://api.github.com"
63
64 # User agent used for HTTP requests.
65 #
66 # Default is `nit_github_api`.
67 #
68 # See <https://developer.github.com/v3/#user-agent-required>
69 var user_agent = "nit_github_api"
70
71 # Curl instance.
72 #
73 # Internal Curl instance used to perform API calls.
74 private var ghcurl: GithubCurl is noinit
75
76 # Verbosity level.
77 #
78 # * `0`: only errors (default)
79 # * `1`: verbose
80 var verbose_lvl = 0 is public writable
81
82 init do
83 ghcurl = new GithubCurl(auth, user_agent)
84 end
85
86 # Execute a GET request on Github API.
87 #
88 # This method returns raw json data.
89 # See other `load_*` methods to use more expressive types.
90 #
91 # var api = new GithubAPI(get_github_oauth)
92 # var obj = api.get("repos/privat/nit")
93 # assert obj isa JsonObject
94 # assert obj["name"] == "nit"
95 #
96 # Returns `null` in case of `error`.
97 #
98 # obj = api.get("foo/bar/baz")
99 # assert obj == null
100 # assert api.was_error
101 # var err = api.last_error
102 # assert err isa GithubError
103 # assert err.name == "GithubAPIError"
104 # assert err.message == "Not Found"
105 fun get(path: String): nullable Jsonable do
106 path = sanitize_uri(path)
107 var res = ghcurl.get_and_parse("{api_url}/{path}")
108 if res isa Error then
109 last_error = res
110 was_error = true
111 return null
112 end
113 was_error = false
114 return res
115 end
116
117 # Display a message depending on `verbose_lvl`.
118 fun message(lvl: Int, message: String) do
119 if lvl <= verbose_lvl then print message
120 end
121
122 # Escape `uri` in an acceptable format for Github.
123 private fun sanitize_uri(uri: String): String do
124 # TODO better URI escape.
125 return uri.replace(" ", "%20")
126 end
127
128 # Last error occured during Github API communications.
129 var last_error: nullable Error = null is public writable
130
131 # Does the last request provoqued an error?
132 var was_error = false is protected writable
133
134 # Load the json object from Github.
135 # See `GithubEntity::load_from_github`.
136 private fun load_from_github(key: String): JsonObject do
137 message(1, "Get {key} (github)")
138 var res = get(key)
139 if was_error then return new JsonObject
140 return res.as(JsonObject)
141 end
142
143 # Get the Github user with `login`.
144 #
145 # Returns `null` if the user cannot be found.
146 #
147 # var api = new GithubAPI(get_github_oauth)
148 # var user = api.load_user("Morriar")
149 # assert user.login == "Morriar"
150 fun load_user(login: String): nullable User do
151 var user = new User(self, login)
152 user.load_from_github
153 if was_error then return null
154 return user
155 end
156
157 # Get the Github repo with `full_name`.
158 #
159 # Returns `null` if the repo cannot be found.
160 #
161 # var api = new GithubAPI(get_github_oauth)
162 # var repo = api.load_repo("privat/nit")
163 # assert repo.name == "nit"
164 # assert repo.owner.login == "privat"
165 # assert repo.default_branch.name == "master"
166 fun load_repo(full_name: String): nullable Repo do
167 var repo = new Repo(self, full_name)
168 repo.load_from_github
169 if was_error then return null
170 return repo
171 end
172
173 # Get the Github branch with `name`.
174 #
175 # Returns `null` if the branch cannot be found.
176 #
177 # var api = new GithubAPI(get_github_oauth)
178 # var repo = api.load_repo("privat/nit")
179 # assert repo isa Repo
180 # var branch = api.load_branch(repo, "master")
181 # assert branch.name == "master"
182 # assert branch.commit isa Commit
183 fun load_branch(repo: Repo, name: String): nullable Branch do
184 var branch = new Branch(self, repo, name)
185 branch.load_from_github
186 if was_error then return null
187 return branch
188 end
189
190 # Get the Github commit with `sha`.
191 #
192 # Returns `null` if the commit cannot be found.
193 #
194 # var api = new GithubAPI(get_github_oauth)
195 # var repo = api.load_repo("privat/nit")
196 # assert repo isa Repo
197 # var commit = api.load_commit(repo, "64ce1f")
198 # assert commit isa Commit
199 fun load_commit(repo: Repo, sha: String): nullable Commit do
200 var commit = new Commit(self, repo, sha)
201 commit.load_from_github
202 if was_error then return null
203 return commit
204 end
205 end
206
207 # Something returned by the Github API.
208 #
209 # Mainly a Nit wrapper around a JSON objet.
210 abstract class GithubEntity
211
212 # Github API instance.
213 var api: GithubAPI
214
215 # FIXME constructor should be private
216
217 # Key used to access this entity from Github api base.
218 fun key: String is abstract
219
220 # JSON representation of `self`.
221 #
222 # This is the same json structure than used by Github API.
223 var json: JsonObject is noinit, protected writable
224
225 # Load `json` from Github API.
226 private fun load_from_github do
227 json = api.load_from_github(key)
228 end
229
230 redef fun to_s do return json.to_json
231 end
232
233 # A Github user.
234 #
235 # Should be accessed from `GithubAPI::load_user`.
236 #
237 # See <https://developer.github.com/v3/users/>.
238 class User
239 super GithubEntity
240
241 redef var key is lazy do return "users/{login}"
242
243 # Github login.
244 var login: String
245
246 # Init `self` from a `json` object.
247 init from_json(api: GithubAPI, json: JsonObject) do
248 init(api, json["login"].to_s)
249 self.json = json
250 end
251
252 # Github User page url.
253 fun html_url: String do return json["html_url"].to_s
254
255 # Avatar image url for this user.
256 fun avatar_url: String do return json["avatar_url"].to_s
257 end
258
259 # A Github repository.
260 #
261 # Should be accessed from `GithubAPI::load_repo`.
262 #
263 # See <https://developer.github.com/v3/repos/>.
264 class Repo
265 super GithubEntity
266
267 redef var key is lazy do return "repos/{full_name}"
268
269 # Repo full name on Github.
270 var full_name: String
271
272 # Init `self` from a `json` object.
273 init from_json(api: GithubAPI, json: JsonObject) do
274 init(api, json["full_name"].to_s)
275 self.json = json
276 end
277
278 # Repo short name on Github.
279 fun name: String do return json["name"].to_s
280
281 # Github User page url.
282 fun html_url: String do return json["html_url"].to_s
283
284 # Get the repo owner.
285 fun owner: User do
286 return new User.from_json(api, json["owner"].as(JsonObject))
287 end
288
289 # List of branches associated with their names.
290 fun branches: Map[String, Branch] do
291 api.message(1, "Get branches for {full_name}")
292 var array = api.get("repos/{full_name}/branches")
293 var res = new HashMap[String, Branch]
294 if not array isa JsonArray then return res
295 for obj in array do
296 if not obj isa JsonObject then continue
297 var name = obj["name"].to_s
298 res[name] = new Branch.from_json(api, self, obj)
299 end
300 return res
301 end
302
303 # Repo default branch.
304 fun default_branch: Branch do
305 var name = json["default_branch"].to_s
306 var branch = api.load_branch(self, name)
307 assert branch isa Branch
308 return branch
309 end
310 end
311
312 # A `RepoEntity` is something contained in a `Repo`.
313 abstract class RepoEntity
314 super GithubEntity
315
316 # Repo that contains `self`.
317 var repo: Repo
318
319 # Init `self` from a `json` object.
320 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
321 self.api = api
322 self.repo = repo
323 self.json = json
324 end
325 end
326
327 # A Github branch.
328 #
329 # Should be accessed from `GithubAPI::load_branch`.
330 #
331 # See <https://developer.github.com/v3/repos/#list-branches>.
332 class Branch
333 super RepoEntity
334
335 redef var key is lazy do return "{repo.key}/branches/{name}"
336
337 # Branch name.
338 var name: String
339
340 redef init from_json(api, repo, json) do
341 self.name = json["name"].to_s
342 super
343 end
344
345 # Get the last commit of `self`.
346 fun commit: Commit do
347 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
348 end
349
350 # List all commits in `self`.
351 #
352 # This can be long depending on the branch size.
353 # Commit are returned in an unspecified order.
354 fun commits: Array[Commit] do
355 var res = new Array[Commit]
356 var done = new HashSet[String]
357 var todos = new Array[Commit]
358 todos.add commit
359 while not todos.is_empty do
360 var commit = todos.pop
361 if done.has(commit.sha) then continue
362 done.add commit.sha
363 res.add commit
364 for parent in commit.parents do
365 todos.add parent
366 end
367 end
368 return res
369 end
370 end
371
372 # A Github commit.
373 #
374 # Should be accessed from `GithubAPI::load_commit`.
375 #
376 # See <https://developer.github.com/v3/commits/>.
377 class Commit
378 super RepoEntity
379
380 redef var key is lazy do return "{repo.key}/commits/{sha}"
381
382 # Commit SHA.
383 var sha: String
384
385 redef init from_json(api, repo, json) do
386 self.sha = json["sha"].to_s
387 super
388 end
389
390 # Parent commits of `self`.
391 fun parents: Array[Commit] do
392 var res = new Array[Commit]
393 var parents = json["parents"]
394 if not parents isa JsonArray then return res
395 for obj in parents do
396 if not obj isa JsonObject then continue
397 res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
398 end
399 return res
400 end
401
402 # Author of the commit.
403 fun author: nullable User do
404 if not json.has_key("author") then return null
405 var user = json["author"]
406 if not user isa JsonObject then return null
407 return new User.from_json(api, user)
408 end
409
410 # Committer of the commit.
411 fun committer: nullable User do
412 if not json.has_key("committer") then return null
413 var user = json["author"]
414 if not user isa JsonObject then return null
415 return new User.from_json(api, user)
416 end
417
418 # Authoring date as ISODate.
419 fun author_date: ISODate do
420 var commit = json["commit"].as(JsonObject)
421 var author = commit["author"].as(JsonObject)
422 return new ISODate.from_string(author["date"].to_s)
423 end
424
425 # Commit date as ISODate.
426 fun commit_date: ISODate do
427 var commit = json["commit"].as(JsonObject)
428 var author = commit["committer"].as(JsonObject)
429 return new ISODate.from_string(author["date"].to_s)
430 end
431
432 # Commit message.
433 fun message: String do return json["commit"].as(JsonObject)["message"].to_s
434 end