github: Remove `get_repo_last_issue`
[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 get_branches(job)
249 get_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.get_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 get_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.get_repo_branches(repo) do
300 branch.repo = repo
301 branches.save branch
302 get_commits(job, branch)
303 end
304 end
305
306 fun get_commits(job: LoaderJob, branch: Branch) do
307 if config.no_commits then return
308 get_commit(job, branch.commit.sha)
309 end
310
311 fun get_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.get_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 get_commit(job, parent.sha)
325 end
326 end
327
328 # Load game for `repo_name`.
329 fun get_issues(job: LoaderJob) do
330 if config.no_issues then return
331
332 var api = config.wallet.api
333 var page = 1
334 var issues = api.get_repo_issues(job.repo.full_name, page, 100)
335 while issues.not_empty do
336 for issue in issues do
337 get_issue(job, issue.number)
338 job.last_issue = issue.number
339 jobs.save job
340 end
341 end
342 end
343
344 # Load an issue or abort.
345 private fun get_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.get_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 get_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 get_issue_events(job, issue)
360 end
361 get_issue_comments(job, issue)
362 end
363
364 # Load issue comments.
365 private fun get_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.get_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 get_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.get_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 get_pull(job: LoaderJob, issue: Issue): PullRequest do
387 var api = config.wallet.api
388 var pr = api.get_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 get_pull_events(job, pr)
396 return pr
397 end
398
399 # Load pull events.
400 private fun get_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.get_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.get_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