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