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