Merge: Introduce `Logger`, a simple yet powerful logging system
[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 arr = opt_tokens.value
108 if arr.is_empty then
109 var iarr = ini.at("tokens")
110 if iarr != null then arr = iarr.values.to_a
111 end
112 return arr or else new Array[String]
113 end
114
115 # Github tokens wallet\13
116 var wallet: GithubWallet is lazy do
117 var wallet = new GithubWallet.from_tokens(tokens)
118 wallet.no_colors = no_colors
119 return wallet
120 end
121
122 # Use colors in console display
123 fun no_colors: Bool do
124 if opt_no_colors.value then return true
125 return ini["loader.no_colors"] == "true"
126 end
127
128 # Verbosity level (the higher the more verbose)
129 fun verbose_level: Int do
130 var opt = opt_start.value
131 if opt > 0 then
132 return info_level
133 end
134 var v = ini["loader.verbose"]
135 if v != null and v.to_i > 0 then
136 return info_level
137 end
138 return warn_level
139 end
140
141 # Logger used to print things
142 var logger: PopLogger is lazy do
143 var logger = new PopLogger
144 logger.level = verbose_level
145 return logger
146 end
147
148 # Should we avoid loading branches?
149 fun no_branches: Bool do
150 if opt_no_branches.value then return true
151 return ini["loader.no_branches"] == "true"
152 end
153
154 # Should we avoid loading commits?
155 fun no_commits: Bool do
156 if opt_no_commits.value then return true
157 return ini["loader.no_commits"] == "true"
158 end
159
160 # Should we avoid loading issues?
161 fun no_issues: Bool do
162 if opt_no_issues.value then return true
163 return ini["loader.no_issues"] == "true"
164 end
165
166 # Should we avoid loading issue comments?
167 fun no_comments: Bool do
168 if opt_no_comments.value then return true
169 return ini["loader.no_comments"] == "true"
170 end
171
172 # Should we avoid loading events?
173 fun no_events: Bool do
174 if opt_no_events.value then return true
175 return ini["loader.no_events"] == "true"
176 end
177
178 # At which issue number should we start?
179 fun start_from_issue: Int do
180 var opt = opt_start.value
181 if opt > 0 then return opt
182 var v = ini["loader.start"]
183 if v != null then return v.to_i
184 return 1
185 end
186 end
187
188 redef class GithubWallet
189 redef fun api do
190 var api = super
191 api.enable_cache = true
192 return api
193 end
194 end
195
196 class Loader
197
198 var config = new LoaderConfig
199
200 # Jobs repository
201 var jobs: LoaderJobRepo is lazy do
202 return new LoaderJobRepo(config.db.collection("loader_status"))
203 end
204
205 var repos: RepoRepo is lazy do
206 return new RepoRepo(config.db.collection("repos"))
207 end
208
209 var branches: BranchRepo is lazy do
210 return new BranchRepo(config.db.collection("branches"))
211 end
212
213 var commits: CommitRepo is lazy do
214 return new CommitRepo(config.db.collection("commits"))
215 end
216
217 var issues: IssueRepo is lazy do
218 return new IssueRepo(config.db.collection("issues"))
219 end
220
221 var pulls: PullRequestRepo is lazy do
222 return new PullRequestRepo(config.db.collection("pull_requests"))
223 end
224
225 var issue_comments: IssueCommentRepo is lazy do
226 return new IssueCommentRepo(config.db.collection("issue_comments"))
227 end
228
229 var issue_events: IssueEventRepo is lazy do
230 return new IssueEventRepo(config.db.collection("issue_events"))
231 end
232
233 fun start(repo_full_name: String) do
234 var job = jobs.find_by_id(repo_full_name)
235 if job == null then
236 log.info "Creating new job for `{repo_full_name}`"
237 job = add_job(repo_full_name)
238 else
239 log.info "Resuming pending job for `{repo_full_name}`"
240 end
241 print "Load history for {job}..."
242 load_branches(job)
243 load_issues(job)
244 finish_job(job)
245 end
246
247 fun remove(repo_full_name: String) do
248 var job = jobs.find_by_id(repo_full_name)
249 if job == null then
250 log.info "No job found for `{repo_full_name}`"
251 else
252 jobs.remove_by_id(repo_full_name)
253 log.info "Deleted job for `{repo_full_name}`"
254 end
255 end
256
257 # Show wallet status
258 fun show_wallet do config.wallet.show_status
259
260 # Show jobs status
261 fun show_jobs do
262 var jobs = jobs.find_all
263 print "{jobs.length} jobs pending..."
264 for job in jobs do
265 print " * {job}"
266 end
267 print "\nUse `loader <job> to start a new or resume a pending one"
268 end
269
270 # Add a new job
271 fun add_job(repo_full_name: String): LoaderJob do
272 var repo = config.wallet.api.load_repo(repo_full_name)
273 assert repo != null else
274 error "Repository `{repo_full_name}` not found"
275 end
276 repos.save repo
277 var job = new LoaderJob(repo, config.start_from_issue)
278 jobs.save job
279 return job
280 end
281
282 # Finish a job
283 fun finish_job(job: LoaderJob) do
284 print "Finished job {job}"
285 jobs.remove_by_id(job.id)
286 end
287
288 fun load_branches(job: LoaderJob) do
289 if config.no_branches then return
290
291 var api = config.wallet.api
292 var repo = job.repo
293 for branch in api.load_repo_branches(repo) do
294 branch.repo = repo
295 branches.save branch
296 load_commits(job, branch)
297 end
298 end
299
300 fun load_commits(job: LoaderJob, branch: Branch) do
301 if config.no_commits then return
302 load_commit(job, branch.commit.sha)
303 end
304
305 fun load_commit(job: LoaderJob, commit_sha: String) do
306 if commits.find_by_id(commit_sha) != null then return
307 var api = config.wallet.api
308 var commit = api.load_commit(job.repo, commit_sha)
309 # print commit or else "NULL"
310 if commit == null then return
311 var message = commit.message or else "no message"
312 log.info "Load commit {commit_sha}: {message.split("\n").first}"
313 commit.repo = job.repo
314 commits.save commit
315 var parents = commit.parents
316 if parents == null then return
317 for parent in parents do
318 load_commit(job, parent.sha)
319 end
320 end
321
322 # Load game for `repo_name`.
323 fun load_issues(job: LoaderJob) do
324 if config.no_issues then return
325
326 var i = job.last_issue
327 var last_issue = load_last_issue(job)
328 if last_issue != null then
329 while i <= last_issue.number do
330 load_issue(job, i)
331 job.last_issue = i
332 jobs.save job
333 i += 1
334 end
335 end
336 end
337
338 # Load the `repo` last issue or abort.
339 private fun load_last_issue(job: LoaderJob): nullable Issue do
340 var api = config.wallet.api
341 return api.load_repo_last_issue(job.repo)
342 end
343
344 # Load an issue or abort.
345 private fun load_issue(job: LoaderJob, issue_number: Int) do
346 if issues.find_by_id("{job.repo.mongo_id}/{issue_number}") != null then return
347
348 var api = config.wallet.api
349 var issue = api.load_issue(job.repo, issue_number)
350 assert issue != null else
351 check_error(api, "Issue #{issue_number} not found")
352 end
353 if issue.is_pull_request then
354 load_pull(job, issue)
355 else
356 log.info "Load issue #{issue.number}: {issue.title.split("\n").first}"
357 issue.repo = job.repo
358 issues.save issue
359 load_issue_events(job, issue)
360 end
361 load_issue_comments(job, issue)
362 end
363
364 # Load issue comments.
365 private fun load_issue_comments(job: LoaderJob, issue: Issue) do
366 if config.no_comments then return
367 var api = config.wallet.api
368 for comment in api.load_issue_comments(job.repo, issue) do
369 comment.repo = job.repo
370 issue_comments.save comment
371 end
372 end
373
374 # Load issue events.
375 private fun load_issue_events(job: LoaderJob, issue: Issue) do
376 if config.no_events then return
377
378 var api = config.wallet.api
379 for event in api.load_issue_events(job.repo, issue) do
380 event.repo = job.repo
381 issue_events.save event
382 end
383 end
384
385 # Load a pull request or abort.
386 private fun load_pull(job: LoaderJob, issue: Issue): PullRequest do
387 var api = config.wallet.api
388 var pr = api.load_pull(job.repo, issue.number)
389 assert pr != null else
390 check_error(api, "Pull request #{issue.number} not found")
391 end
392 log.info "Load pull request #{issue.number}: {pr.title.split("\n").first}"
393 pr.repo = job.repo
394 pulls.save pr
395 load_pull_events(job, pr)
396 return pr
397 end
398
399 # Load pull events.
400 private fun load_pull_events(job: LoaderJob, pull: PullRequest) do
401 if config.no_events then return
402
403 var api = config.wallet.api
404 for event in api.load_issue_events(job.repo, pull) do
405 event.repo = job.repo
406 issue_events.save event
407 end
408 end
409
410 # Check if the API is in error state then abort
411 fun check_error(api: GithubAPI, message: nullable String) do
412 var err = api.last_error
413 if err != null then
414 error message or else err.message
415 end
416 end
417
418 # Logger shortcut
419 fun log: PopLogger do return config.logger
420
421 # Display a error and exit
422 fun error(msg: String) do
423 log.error "Error: {msg}"
424 exit 1
425 end
426 end
427
428 # Loader status by repo
429 class LoaderJob
430 super RepoObject
431 serialize
432
433 # Repo this status is about
434 var repo: Repo
435
436 # Primary key: the repo id
437 redef var id is lazy, serialize_as("_id") do return repo.full_name
438
439 # Last issue loaded
440 var last_issue: Int
441 end
442
443 # Loader status repository
444 class LoaderJobRepo
445 super MongoRepository[LoaderJob]
446 end
447
448 class RepoEntity
449 serialize
450
451 var repo: nullable Repo = null is writable
452 end
453
454 redef class Repo
455 serialize
456
457 var mongo_id: String is lazy, serialize_as("_id") do return full_name
458 end
459
460 class RepoRepo
461 super MongoRepository[Repo]
462 end
463
464 redef class Branch
465 super RepoEntity
466 serialize
467
468 var mongo_id: String is lazy, serialize_as("_id") do
469 var repo = self.repo
470 if repo == null then return name
471 return "{repo.mongo_id}/{name}"
472 end
473 end
474
475 class BranchRepo
476 super MongoRepository[Branch]
477
478 fun find_by_repo(repo: Repo): Array[Branch] do
479 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
480 end
481 end
482
483 redef class Commit
484 super RepoEntity
485 serialize
486
487 var mongo_id: String is lazy, serialize_as("_id") do return sha
488 end
489
490 class CommitRepo
491 super MongoRepository[Commit]
492
493 fun find_by_repo(repo: Repo): Array[Commit] do
494 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
495 end
496 end
497
498 redef class Issue
499 super RepoEntity
500 serialize
501
502 var mongo_id: String is lazy, serialize_as("_id") do
503 var repo = self.repo
504 if repo == null then return number.to_s
505 return "{repo.mongo_id}/{number}"
506 end
507 end
508
509 class IssueRepo
510 super MongoRepository[Issue]
511
512 fun find_by_repo(repo: Repo): Array[Issue] do
513 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
514 end
515 end
516
517 class PullRequestRepo
518 super MongoRepository[PullRequest]
519
520 fun find_by_repo(repo: Repo): Array[Issue] do
521 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
522 end
523 end
524
525 redef class IssueComment
526 super RepoEntity
527 serialize
528
529 var mongo_id: String is lazy, serialize_as("_id") do return id.to_s
530 end
531
532 class IssueCommentRepo
533 super MongoRepository[IssueComment]
534
535 fun find_by_repo(repo: Repo): Array[IssueComment] do
536 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
537 end
538 end
539
540 redef class IssueEvent
541 super RepoEntity
542 serialize
543
544 var mongo_id: String is lazy, serialize_as("_id") do return id.to_s
545 end
546
547 class IssueEventRepo
548 super MongoRepository[IssueEvent]
549
550 fun find_by_repo(repo: Repo): Array[IssueEvent] do
551 return find_all((new MongoMatch).eq("repo.full_name", repo.full_name))
552 end
553 end
554
555 # Init options
556 var loader = new Loader
557 loader.config.parse_options(args)
558
559 # TODO TMP
560 loader.jobs.clear
561 loader.repos.clear
562 loader.branches.clear
563 loader.commits.clear
564 loader.issues.clear
565 loader.pulls.clear
566 loader.issue_comments.clear
567 loader.issue_events.clear
568
569 if loader.config.help then
570 loader.config.usage
571 exit 0
572 end
573
574 if loader.config.opt_show_wallet.value then
575 loader.show_wallet
576 end
577
578 var args = loader.config.args
579 if loader.config.opt_show_jobs.value or args.is_empty then
580 loader.show_jobs
581 end
582
583 if args.is_empty then return
584
585 if loader.config.opt_clear.value then
586 loader.remove args.first
587 else
588 loader.start args.first
589
590 var repo = loader.config.wallet.api.load_repo(args.first)
591 if repo == null then return
592 print "Loaded"
593 print "* {if loader.repos.find_by_id(args.first) != null then 1 else 0} repos"
594 print "* {loader.branches.find_by_repo(repo).length} branches"
595 print "* {loader.commits.find_by_repo(repo).length} commits"
596 print "* {loader.issues.find_by_repo(repo).length} issues"
597 print "* {loader.pulls.find_by_repo(repo).length} pulls"
598 print "* {loader.issue_comments.find_by_repo(repo).length} comments"
599 print "* {loader.issue_events.find_by_repo(repo).length} events"
600 end