Merge: fix ci nitunit some
[nit.git] / lib / github / loader.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 module loader
18
19 import config
20 import github::wallet
21 import github::events
22 import popcorn::pop_repos
23 import popcorn::pop_logging
24
25 # Loader configuration file
26 class LoaderConfig
27 super IniConfig
28
29 redef var default_config_file = "loader.ini"
30
31 # Default database host string for MongoDb
32 var default_db_host = "mongodb://mongo:27017/"
33
34 # Default database hostname
35 var default_db_name = "github_loader"
36
37 # MongoDb host name
38 var opt_db_host = new OptionString("MongoDb host", "--db-host")
39
40 # MongoDb database name
41 var opt_db_name = new OptionString("MongoDb database name", "--db-name")
42
43 # --verbose
44 var opt_verbose = new OptionCount("Verbosity level", "-v", "--verbose")
45
46 # --no-colors
47 var opt_no_colors = new OptionBool("Do not use colors in output", "--no-colors")
48
49 # --tokens
50 var opt_tokens = new OptionArray("Token list", "--tokens")
51
52 # --show-wallet
53 var opt_show_wallet = new OptionBool("Show wallet status", "--show-wallet")
54
55 # --show-jobs
56 var opt_show_jobs = new OptionBool("Show jobs status", "--show-jobs")
57
58 # --no-branches
59 var opt_no_branches = new OptionBool("Do not load branches", "--no-branches")
60
61 # --no-commits
62 var opt_no_commits = new OptionBool("Do not load commits from default branch", "--no-commits")
63
64 # --no-issues
65 var opt_no_issues = new OptionBool("Do not load issues", "--no-issues")
66
67 # --no-comments
68 var opt_no_comments = new OptionBool("Do not load issue comments", "--no-comments")
69
70 # --no-events
71 var opt_no_events = new OptionBool("Do not load issues events", "--no-events")
72
73 # --from
74 var opt_start = new OptionInt("Start loading issues from a number", 0, "--from")
75
76 # --clear
77 var opt_clear = new OptionBool("Clear job for given repo name", "--clear")
78
79 init do
80 super
81 tool_description = "Usage: loader <repo_name>\nLoad a GitHub repo into a MongoDb."
82 add_option(opt_db_host, opt_db_name)
83 add_option(opt_tokens, opt_show_wallet)
84 add_option(opt_verbose, opt_no_colors)
85 add_option(opt_show_jobs, opt_no_commits, opt_no_issues, opt_no_comments, opt_no_events)
86 add_option(opt_start, opt_clear)
87 end
88
89 # MongoDB server used for data persistence
90 fun db_host: String do
91 return opt_db_host.value or else ini["db.host"] or else default_db_host
92 end
93
94 # MongoDB DB used for data persistence
95 fun db_name: String do
96 return opt_db_name.value or else ini["db.name"] or else default_db_name
97 end
98
99 # Mongo db client
100 var client = new MongoClient(db_host) is lazy
101
102 # Mongo db instance
103 var db: MongoDb = client.database(db_name) is lazy
104
105 # Github tokens used to access data.
106 var tokens: Array[String] is lazy do
107 var opt_tokens = self.opt_tokens.value
108 if opt_tokens.not_empty then return opt_tokens
109
110 var res = new Array[String]
111 var ini_tokens = ini.section("tokens")
112 if ini_tokens == null then return res
113
114 for token in ini_tokens.values do
115 if token == null then continue
116 res.add token
117 end
118 return res
119 end
120
121 # Github tokens wallet\13
122 var wallet: GithubWallet is lazy do
123 return new GithubWallet(tokens)
124 end
125
126 # Use colors in console display
127 fun no_colors: Bool do
128 if opt_no_colors.value then return true
129 return ini["loader.no_colors"] == "true"
130 end
131
132 # Verbosity level (the higher the more verbose)
133 fun verbose_level: Int do
134 var opt = opt_start.value
135 if opt > 0 then
136 return info_level
137 end
138 var v = ini["loader.verbose"]
139 if v != null and v.to_i > 0 then
140 return info_level
141 end
142 return warn_level
143 end
144
145 # Logger used to print things
146 var logger: PopLogger is lazy do
147 var logger = new PopLogger
148 logger.level = verbose_level
149 return logger
150 end
151
152 # Should we avoid loading branches?
153 fun no_branches: Bool do
154 if opt_no_branches.value then return true
155 return ini["loader.no_branches"] == "true"
156 end
157
158 # Should we avoid loading commits?
159 fun no_commits: Bool do
160 if opt_no_commits.value then return true
161 return ini["loader.no_commits"] == "true"
162 end
163
164 # Should we avoid loading issues?
165 fun no_issues: Bool do
166 if opt_no_issues.value then return true
167 return ini["loader.no_issues"] == "true"
168 end
169
170 # Should we avoid loading issue comments?
171 fun no_comments: Bool do
172 if opt_no_comments.value then return true
173 return ini["loader.no_comments"] == "true"
174 end
175
176 # Should we avoid loading events?
177 fun no_events: Bool do
178 if opt_no_events.value then return true
179 return ini["loader.no_events"] == "true"
180 end
181
182 # At which issue number should we start?
183 fun start_from_issue: Int do
184 var opt = opt_start.value
185 if opt > 0 then return opt
186 var v = ini["loader.start"]
187 if v != null then return v.to_i
188 return 1
189 end
190 end
191
192 redef class GithubWallet
193 redef fun api do
194 var api = super
195 api.enable_cache = true
196 return api
197 end
198 end
199
200 class Loader
201
202 var config = new LoaderConfig
203
204 # Jobs repository
205 var jobs: LoaderJobRepo is lazy do
206 return new LoaderJobRepo(config.db.collection("loader_status"))
207 end
208
209 var repos: RepoRepo is lazy do
210 return new RepoRepo(config.db.collection("repos"))
211 end
212
213 var branches: BranchRepo is lazy do
214 return new BranchRepo(config.db.collection("branches"))
215 end
216
217 var commits: CommitRepo is lazy do
218 return new CommitRepo(config.db.collection("commits"))
219 end
220
221 var issues: IssueRepo is lazy do
222 return new IssueRepo(config.db.collection("issues"))
223 end
224
225 var pulls: PullRequestRepo is lazy do
226 return new PullRequestRepo(config.db.collection("pull_requests"))
227 end
228
229 var issue_comments: IssueCommentRepo is lazy do
230 return new IssueCommentRepo(config.db.collection("issue_comments"))
231 end
232
233 var issue_events: IssueEventRepo is lazy do
234 return new IssueEventRepo(config.db.collection("issue_events"))
235 end
236
237 fun start(repo_slug: String) do
238 var job = jobs.find_by_id(repo_slug)
239 if job == null then
240 log.info "Creating new job for `{repo_slug}`"
241 job = add_job(repo_slug)
242 else
243 log.info "Resuming pending job for `{repo_slug}`"
244 end
245 print "Load history for {job}..."
246 get_branches(job)
247 get_issues(job)
248 finish_job(job)
249 end
250
251 fun remove(repo_slug: String) do
252 var job = jobs.find_by_id(repo_slug)
253 if job == null then
254 log.info "No job found for `{repo_slug}`"
255 else
256 jobs.remove_by_id(repo_slug)
257 log.info "Deleted job for `{repo_slug}`"
258 end
259 end
260
261 # Show wallet status
262 fun show_wallet do config.wallet.show_status(config.no_colors)
263
264 # Show jobs status
265 fun show_jobs do
266 var jobs = jobs.find_all
267 print "{jobs.length} jobs pending..."
268 for job in jobs do
269 print " * {job}"
270 end
271 print "\nUse `loader <job> to start a new or resume a pending one"
272 end
273
274 # Add a new job
275 fun add_job(repo_slug: String): LoaderJob do
276 var repo = config.wallet.api.get_repo(repo_slug)
277 assert repo != null else
278 error "Repository `{repo_slug}` not found"
279 end
280 repos.save repo
281 var job = new LoaderJob(repo, config.start_from_issue)
282 jobs.save job
283 return job
284 end
285
286 # Finish a job
287 fun finish_job(job: LoaderJob) do
288 print "Finished job {job}"
289 jobs.remove_by_id(job.id)
290 end
291
292 fun get_branches(job: LoaderJob) do
293 if config.no_branches then return
294
295 var api = config.wallet.api
296 var repo = job.repo
297 for branch in api.get_repo_branches(repo.full_name) do
298 branch.repo = repo
299 branches.save branch
300 get_commits(job, branch)
301 end
302 end
303
304 fun get_commits(job: LoaderJob, branch: Branch) do
305 if config.no_commits then return
306 get_commit(job, branch.commit.sha)
307 end
308
309 fun get_commit(job: LoaderJob, commit_sha: String) do
310 if commits.find_by_id(commit_sha) != null then return
311 var api = config.wallet.api
312 var commit = api.get_commit(job.repo.full_name, commit_sha)
313 # print commit or else "NULL"
314 if commit == null then return
315 var message = commit.message or else "no message"
316 log.info "Load commit {commit_sha}: {message.split("\n").first}"
317 commit.repo = job.repo
318 commits.save commit
319 var parents = commit.parents
320 if parents == null then return
321 for parent in parents do
322 get_commit(job, parent.sha)
323 end
324 end
325
326 # Load game for `repo_name`.
327 fun get_issues(job: LoaderJob) do
328 if config.no_issues then return
329
330 var api = config.wallet.api
331 var page = 1
332 var issues = api.get_repo_issues(job.repo.full_name, page, 100)
333 while issues.not_empty do
334 for issue in issues do
335 get_issue(job, issue.number)
336 job.last_issue = issue.number
337 jobs.save job
338 end
339 end
340 end
341
342 # Load an issue or abort.
343 private fun get_issue(job: LoaderJob, issue_number: Int) do
344 if issues.find_by_id("{job.repo.mongo_id}/{issue_number}") != null then return
345
346 var api = config.wallet.api
347 var issue = api.get_issue(job.repo.full_name, issue_number)
348 assert issue != null else
349 check_error(api, "Issue #{issue_number} not found")
350 end
351 if issue.is_pull_request then
352 get_pull(job, issue)
353 else
354 log.info "Load issue #{issue.number}: {issue.title.split("\n").first}"
355 issue.repo = job.repo
356 issues.save issue
357 get_issue_events(job, issue)
358 end
359 get_issue_comments(job, issue)
360 end
361
362 # Load issue comments.
363 private fun get_issue_comments(job: LoaderJob, issue: Issue) do
364 if config.no_comments then return
365 var api = config.wallet.api
366 for comment in api.get_issue_comments(job.repo.full_name, issue.number) do
367 comment.repo = job.repo
368 issue_comments.save comment
369 end
370 end
371
372 # Load issue events.
373 private fun get_issue_events(job: LoaderJob, issue: Issue) do
374 if config.no_events then return
375
376 var api = config.wallet.api
377 for event in api.get_issue_events(job.repo.full_name, issue.number) do
378 event.repo = job.repo
379 issue_events.save event
380 end
381 end
382
383 # Load a pull request or abort.
384 private fun get_pull(job: LoaderJob, issue: Issue): PullRequest do
385 var api = config.wallet.api
386 var pr = api.get_pull(job.repo.full_name, issue.number)
387 assert pr != null else
388 check_error(api, "Pull request #{issue.number} not found")
389 end
390 log.info "Load pull request #{issue.number}: {pr.title.split("\n").first}"
391 pr.repo = job.repo
392 pulls.save pr
393 get_pull_events(job, pr)
394 return pr
395 end
396
397 # Load pull events.
398 private fun get_pull_events(job: LoaderJob, pull: PullRequest) do
399 if config.no_events then return
400
401 var api = config.wallet.api
402 for event in api.get_issue_events(job.repo.full_name, pull.number) do
403 event.repo = job.repo
404 issue_events.save event
405 end
406 end
407
408 # Check if the API is in error state then abort
409 fun check_error(api: GithubAPI, message: nullable String) do
410 var err = api.last_error
411 if err != null then
412 error message or else err.message
413 end
414 end
415
416 # Logger shortcut
417 fun log: PopLogger do return config.logger
418
419 # Display a error and exit
420 fun error(msg: String) do
421 log.error "Error: {msg}"
422 exit 1
423 end
424 end
425
426 # Loader status by repo
427 class LoaderJob
428 super RepoObject
429 serialize
430
431 # Repo this status is about
432 var repo: Repo
433
434 # Primary key: the repo id
435 redef var id is lazy, serialize_as("_id") do return repo.full_name
436
437 # Last issue loaded
438 var last_issue: Int
439 end
440
441 # Loader status repository
442 class LoaderJobRepo
443 super MongoRepository[LoaderJob]
444 end
445
446 class RepoEntity
447 serialize
448
449 var repo: nullable Repo = null is writable
450 end
451
452 redef class Repo
453 serialize
454
455 var mongo_id: String is lazy, serialize_as("_id") do return full_name
456 end
457
458 class RepoRepo
459 super MongoRepository[Repo]
460 end
461
462 redef class Branch
463 super RepoEntity
464 serialize
465
466 var mongo_id: String is lazy, serialize_as("_id") do
467 var repo = self.repo
468 if repo == null then return name
469 return "{repo.mongo_id}/{name}"
470 end
471 end
472
473 class BranchRepo
474 super MongoRepository[Branch]
475
476 fun find_by_repo(repo: Repo): Array[Branch] do
477 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
478 end
479 end
480
481 redef class Commit
482 super RepoEntity
483 serialize
484
485 var mongo_id: String is lazy, serialize_as("_id") do return sha
486 end
487
488 class CommitRepo
489 super MongoRepository[Commit]
490
491 fun find_by_repo(repo: Repo): Array[Commit] do
492 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
493 end
494 end
495
496 redef class Issue
497 super RepoEntity
498 serialize
499
500 var mongo_id: String is lazy, serialize_as("_id") do
501 var repo = self.repo
502 if repo == null then return number.to_s
503 return "{repo.mongo_id}/{number}"
504 end
505 end
506
507 class IssueRepo
508 super MongoRepository[Issue]
509
510 fun find_by_repo(repo: Repo): Array[Issue] do
511 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
512 end
513 end
514
515 class PullRequestRepo
516 super MongoRepository[PullRequest]
517
518 fun find_by_repo(repo: Repo): Array[Issue] do
519 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
520 end
521 end
522
523 redef class IssueComment
524 super RepoEntity
525 serialize
526
527 var mongo_id: String is lazy, serialize_as("_id") do return id.to_s
528 end
529
530 class IssueCommentRepo
531 super MongoRepository[IssueComment]
532
533 fun find_by_repo(repo: Repo): Array[IssueComment] do
534 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
535 end
536 end
537
538 redef class IssueEvent
539 super RepoEntity
540 serialize
541
542 var mongo_id: String is lazy, serialize_as("_id") do return id.to_s
543 end
544
545 class IssueEventRepo
546 super MongoRepository[IssueEvent]
547
548 fun find_by_repo(repo: Repo): Array[IssueEvent] do
549 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
550 end
551 end
552
553 # Init options
554 var loader = new Loader
555 loader.config.parse_options(args)
556
557 # TODO TMP
558 loader.jobs.clear
559 loader.repos.clear
560 loader.branches.clear
561 loader.commits.clear
562 loader.issues.clear
563 loader.pulls.clear
564 loader.issue_comments.clear
565 loader.issue_events.clear
566
567 if loader.config.help then
568 loader.config.usage
569 exit 0
570 end
571
572 if loader.config.opt_show_wallet.value then
573 loader.show_wallet
574 end
575
576 var args = loader.config.args
577 if loader.config.opt_show_jobs.value or args.is_empty then
578 loader.show_jobs
579 end
580
581 if args.is_empty then return
582
583 if loader.config.opt_clear.value then
584 loader.remove args.first
585 else
586 loader.start args.first
587
588 var repo = loader.config.wallet.api.get_repo(args.first)
589 if repo == null then return
590 print "Loaded"
591 print "* {if loader.repos.find_by_id(args.first) != null then 1 else 0} repos"
592 print "* {loader.branches.find_by_repo(repo).length} branches"
593 print "* {loader.commits.find_by_repo(repo).length} commits"
594 print "* {loader.issues.find_by_repo(repo).length} issues"
595 print "* {loader.pulls.find_by_repo(repo).length} pulls"
596 print "* {loader.issue_comments.find_by_repo(repo).length} comments"
597 print "* {loader.issue_events.find_by_repo(repo).length} events"
598 end