1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
22 import popcorn
::pop_repos
23 import popcorn
::pop_logging
25 # Loader configuration file
29 redef var default_config_file
= "loader.ini"
31 # Default database host string for MongoDb
32 var default_db_host
= "mongodb://mongo:27017/"
34 # Default database hostname
35 var default_db_name
= "github_loader"
38 var opt_db_host
= new OptionString("MongoDb host", "--db-host")
40 # MongoDb database name
41 var opt_db_name
= new OptionString("MongoDb database name", "--db-name")
44 var opt_verbose
= new OptionCount("Verbosity level", "-v", "--verbose")
47 var opt_no_colors
= new OptionBool("Do not use colors in output", "--no-colors")
50 var opt_tokens
= new OptionArray("Token list", "--tokens")
53 var opt_show_wallet
= new OptionBool("Show wallet status", "--show-wallet")
56 var opt_show_jobs
= new OptionBool("Show jobs status", "--show-jobs")
59 var opt_no_branches
= new OptionBool("Do not load branches", "--no-branches")
62 var opt_no_commits
= new OptionBool("Do not load commits from default branch", "--no-commits")
65 var opt_no_issues
= new OptionBool("Do not load issues", "--no-issues")
68 var opt_no_comments
= new OptionBool("Do not load issue comments", "--no-comments")
71 var opt_no_events
= new OptionBool("Do not load issues events", "--no-events")
74 var opt_start
= new OptionInt("Start loading issues from a number", 0, "--from")
77 var opt_clear
= new OptionBool("Clear job for given repo name", "--clear")
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
)
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
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
100 var client
= new MongoClient(db_host
) is lazy
103 var db
: MongoDb = client
.database
(db_name
) is lazy
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
110 var res
= new Array[String]
111 var ini_tokens
= ini
.section
("tokens")
112 if ini_tokens
== null then return res
114 for token
in ini_tokens
.values
do
115 if token
== null then continue
121 # Github tokens wallet\13
122 var wallet
: GithubWallet is lazy
do
123 return new GithubWallet(tokens
)
126 # Use colors in console display
127 fun no_colors
: Bool do
128 if opt_no_colors
.value
then return true
129 return ini
["loader.no_colors"] == "true"
132 # Verbosity level (the higher the more verbose)
133 fun verbose_level
: Int do
134 var opt
= opt_start
.value
138 var v
= ini
["loader.verbose"]
139 if v
!= null and v
.to_i
> 0 then
145 # Logger used to print things
146 var logger
: PopLogger is lazy
do
147 var logger
= new PopLogger
148 logger
.level
= verbose_level
152 # Should we avoid loading branches?
153 fun no_branches
: Bool do
154 if opt_no_branches
.value
then return true
155 return ini
["loader.no_branches"] == "true"
158 # Should we avoid loading commits?
159 fun no_commits
: Bool do
160 if opt_no_commits
.value
then return true
161 return ini
["loader.no_commits"] == "true"
164 # Should we avoid loading issues?
165 fun no_issues
: Bool do
166 if opt_no_issues
.value
then return true
167 return ini
["loader.no_issues"] == "true"
170 # Should we avoid loading issue comments?
171 fun no_comments
: Bool do
172 if opt_no_comments
.value
then return true
173 return ini
["loader.no_comments"] == "true"
176 # Should we avoid loading events?
177 fun no_events
: Bool do
178 if opt_no_events
.value
then return true
179 return ini
["loader.no_events"] == "true"
182 # At which issue number should we start?
183 fun start_from_issue
: Int do
184 var opt
= opt_start
.value
185 if opt
> 0 then return opt
186 var v
= ini
["loader.start"]
187 if v
!= null then return v
.to_i
192 redef class GithubWallet
195 api
.enable_cache
= true
202 var config
= new LoaderConfig
205 var jobs
: LoaderJobRepo is lazy
do
206 return new LoaderJobRepo(config
.db
.collection
("loader_status"))
209 var repos
: RepoRepo is lazy
do
210 return new RepoRepo(config
.db
.collection
("repos"))
213 var branches
: BranchRepo is lazy
do
214 return new BranchRepo(config
.db
.collection
("branches"))
217 var commits
: CommitRepo is lazy
do
218 return new CommitRepo(config
.db
.collection
("commits"))
221 var issues
: IssueRepo is lazy
do
222 return new IssueRepo(config
.db
.collection
("issues"))
225 var pulls
: PullRequestRepo is lazy
do
226 return new PullRequestRepo(config
.db
.collection
("pull_requests"))
229 var issue_comments
: IssueCommentRepo is lazy
do
230 return new IssueCommentRepo(config
.db
.collection
("issue_comments"))
233 var issue_events
: IssueEventRepo is lazy
do
234 return new IssueEventRepo(config
.db
.collection
("issue_events"))
237 fun start
(repo_slug
: String) do
238 var job
= jobs
.find_by_id
(repo_slug
)
240 log
.info
"Creating new job for `{repo_slug}`"
241 job
= add_job
(repo_slug
)
243 log
.info
"Resuming pending job for `{repo_slug}`"
245 print
"Load history for {job}..."
251 fun remove
(repo_slug
: String) do
252 var job
= jobs
.find_by_id
(repo_slug
)
254 log
.info
"No job found for `{repo_slug}`"
256 jobs
.remove_by_id
(repo_slug
)
257 log
.info
"Deleted job for `{repo_slug}`"
262 fun show_wallet
do config
.wallet
.show_status
(config
.no_colors
)
266 var jobs
= jobs
.find_all
267 print
"{jobs.length} jobs pending..."
271 print
"\nUse `loader <job> to start a new or resume a pending one"
275 fun add_job
(repo_slug
: String): LoaderJob do
276 var repo
= config
.wallet
.api
.get_repo
(repo_slug
)
277 assert repo
!= null else
278 error
"Repository `{repo_slug}` not found"
281 var job
= new LoaderJob(repo
, config
.start_from_issue
)
287 fun finish_job
(job
: LoaderJob) do
288 print
"Finished job {job}"
289 jobs
.remove_by_id
(job
.id
)
292 fun get_branches
(job
: LoaderJob) do
293 if config
.no_branches
then return
295 var api
= config
.wallet
.api
297 for branch
in api
.get_repo_branches
(repo
.full_name
) do
300 get_commits
(job
, branch
)
304 fun get_commits
(job
: LoaderJob, branch
: Branch) do
305 if config
.no_commits
then return
306 get_commit
(job
, branch
.commit
.sha
)
309 fun get_commit
(job
: LoaderJob, commit_sha
: String) do
310 if commits
.find_by_id
(commit_sha
) != null then return
311 var api
= config
.wallet
.api
312 var commit
= api
.get_commit
(job
.repo
.full_name
, commit_sha
)
313 # print commit or else "NULL"
314 if commit
== null then return
315 var message
= commit
.message
or else "no message"
316 log
.info
"Load commit {commit_sha}: {message.split("\n").first}"
317 commit
.repo
= job
.repo
319 var parents
= commit
.parents
320 if parents
== null then return
321 for parent
in parents
do
322 get_commit
(job
, parent
.sha
)
326 # Load game for `repo_name`.
327 fun get_issues
(job
: LoaderJob) do
328 if config
.no_issues
then return
330 var api
= config
.wallet
.api
332 var issues
= api
.get_repo_issues
(job
.repo
.full_name
, page
, 100)
333 while issues
.not_empty
do
334 for issue
in issues
do
335 get_issue
(job
, issue
.number
)
336 job
.last_issue
= issue
.number
342 # Load an issue or abort.
343 private fun get_issue
(job
: LoaderJob, issue_number
: Int) do
344 if issues
.find_by_id
("{job.repo.mongo_id}/{issue_number}") != null then return
346 var api
= config
.wallet
.api
347 var issue
= api
.get_issue
(job
.repo
.full_name
, issue_number
)
348 assert issue
!= null else
349 check_error
(api
, "Issue #{issue_number} not found")
351 if issue
.is_pull_request
then
354 log
.info
"Load issue #{issue.number}: {issue.title.split("\n").first}"
355 issue
.repo
= job
.repo
357 get_issue_events
(job
, issue
)
359 get_issue_comments
(job
, issue
)
362 # Load issue comments.
363 private fun get_issue_comments
(job
: LoaderJob, issue
: Issue) do
364 if config
.no_comments
then return
365 var api
= config
.wallet
.api
366 for comment
in api
.get_issue_comments
(job
.repo
.full_name
, issue
.number
) do
367 comment
.repo
= job
.repo
368 issue_comments
.save comment
373 private fun get_issue_events
(job
: LoaderJob, issue
: Issue) do
374 if config
.no_events
then return
376 var api
= config
.wallet
.api
377 for event
in api
.get_issue_events
(job
.repo
.full_name
, issue
.number
) do
378 event
.repo
= job
.repo
379 issue_events
.save event
383 # Load a pull request or abort.
384 private fun get_pull
(job
: LoaderJob, issue
: Issue): PullRequest do
385 var api
= config
.wallet
.api
386 var pr
= api
.get_pull
(job
.repo
.full_name
, issue
.number
)
387 assert pr
!= null else
388 check_error
(api
, "Pull request #{issue.number} not found")
390 log
.info
"Load pull request #{issue.number}: {pr.title.split("\n").first}"
393 get_pull_events
(job
, pr
)
398 private fun get_pull_events
(job
: LoaderJob, pull
: PullRequest) do
399 if config
.no_events
then return
401 var api
= config
.wallet
.api
402 for event
in api
.get_issue_events
(job
.repo
.full_name
, pull
.number
) do
403 event
.repo
= job
.repo
404 issue_events
.save event
408 # Check if the API is in error state then abort
409 fun check_error
(api
: GithubAPI, message
: nullable String) do
410 var err
= api
.last_error
412 error message
or else err
.message
417 fun log
: PopLogger do return config
.logger
419 # Display a error and exit
420 fun error
(msg
: String) do
421 log
.error
"Error: {msg}"
426 # Loader status by repo
431 # Repo this status is about
434 # Primary key: the repo id
435 redef var id
is lazy
, serialize_as
("_id") do return repo
.full_name
441 # Loader status repository
443 super MongoRepository[LoaderJob]
449 var repo
: nullable Repo = null is writable
455 var mongo_id
: String is lazy
, serialize_as
("_id") do return full_name
459 super MongoRepository[Repo]
466 var mongo_id
: String is lazy
, serialize_as
("_id") do
468 if repo
== null then return name
469 return "{repo.mongo_id}/{name}"
474 super MongoRepository[Branch]
476 fun find_by_repo
(repo
: Repo): Array[Branch] do
477 return find_all
((new MongoMatch).eq
("repo.full_name", repo
.full_name
))
485 var mongo_id
: String is lazy
, serialize_as
("_id") do return sha
489 super MongoRepository[Commit]
491 fun find_by_repo
(repo
: Repo): Array[Commit] do
492 return find_all
((new MongoMatch).eq
("repo.full_name", repo
.full_name
))
500 var mongo_id
: String is lazy
, serialize_as
("_id") do
502 if repo
== null then return number
.to_s
503 return "{repo.mongo_id}/{number}"
508 super MongoRepository[Issue]
510 fun find_by_repo
(repo
: Repo): Array[Issue] do
511 return find_all
((new MongoMatch).eq
("repo.full_name", repo
.full_name
))
515 class PullRequestRepo
516 super MongoRepository[PullRequest]
518 fun find_by_repo
(repo
: Repo): Array[Issue] do
519 return find_all
((new MongoMatch).eq
("repo.full_name", repo
.full_name
))
523 redef class IssueComment
527 var mongo_id
: String is lazy
, serialize_as
("_id") do return id
.to_s
530 class IssueCommentRepo
531 super MongoRepository[IssueComment]
533 fun find_by_repo
(repo
: Repo): Array[IssueComment] do
534 return find_all
((new MongoMatch).eq
("repo.full_name", repo
.full_name
))
538 redef class IssueEvent
542 var mongo_id
: String is lazy
, serialize_as
("_id") do return id
.to_s
546 super MongoRepository[IssueEvent]
548 fun find_by_repo
(repo
: Repo): Array[IssueEvent] do
549 return find_all
((new MongoMatch).eq
("repo.full_name", repo
.full_name
))
554 var loader
= new Loader
555 loader
.config
.parse_options
(args
)
560 loader
.branches
.clear
564 loader
.issue_comments
.clear
565 loader
.issue_events
.clear
567 if loader
.config
.help
then
572 if loader
.config
.opt_show_wallet
.value
then
576 var args
= loader
.config
.args
577 if loader
.config
.opt_show_jobs
.value
or args
.is_empty
then
581 if args
.is_empty
then return
583 if loader
.config
.opt_clear
.value
then
584 loader
.remove args
.first
586 loader
.start args
.first
588 var repo
= loader
.config
.wallet
.api
.get_repo
(args
.first
)
589 if repo
== null then return
591 print
"* {if loader.repos.find_by_id(args.first) != null then 1 else 0} repos"
592 print
"* {loader.branches.find_by_repo(repo).length} branches"
593 print
"* {loader.commits.find_by_repo(repo).length} commits"
594 print
"* {loader.issues.find_by_repo(repo).length} issues"
595 print
"* {loader.pulls.find_by_repo(repo).length} pulls"
596 print
"* {loader.issue_comments.find_by_repo(repo).length} comments"
597 print
"* {loader.issue_events.find_by_repo(repo).length} events"