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