bookbindery 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bookbinder.rb +0 -4
  3. data/lib/bookbinder/archive.rb +12 -5
  4. data/lib/bookbinder/archive_menu_configuration.rb +4 -4
  5. data/lib/bookbinder/commands/bind.rb +29 -97
  6. data/lib/bookbinder/commands/bind/bind_options.rb +50 -0
  7. data/lib/bookbinder/commands/bind/directory_preparer.rb +13 -12
  8. data/lib/bookbinder/commands/build_and_push_tarball.rb +1 -8
  9. data/lib/bookbinder/commands/help.rb +1 -1
  10. data/lib/bookbinder/commands/run_publish_ci.rb +1 -3
  11. data/lib/bookbinder/commands/tag.rb +12 -9
  12. data/lib/bookbinder/commands/update_local_doc_repos.rb +23 -7
  13. data/lib/bookbinder/config/bind_config_factory.rb +5 -7
  14. data/lib/bookbinder/config/remote_bind_configuration.rb +23 -31
  15. data/lib/bookbinder/config/section_config.rb +56 -0
  16. data/lib/bookbinder/configuration.rb +58 -18
  17. data/lib/bookbinder/configuration_fetcher.rb +3 -5
  18. data/lib/bookbinder/configuration_validator.rb +1 -8
  19. data/lib/bookbinder/distributor.rb +1 -1
  20. data/lib/bookbinder/dita_command_creator.rb +60 -16
  21. data/lib/bookbinder/dita_html_to_middleman_formatter.rb +3 -2
  22. data/lib/bookbinder/errors/programmer_mistake.rb +5 -0
  23. data/lib/bookbinder/git_accessor.rb +36 -2
  24. data/lib/bookbinder/ingest/cloner_factory.rb +3 -3
  25. data/lib/bookbinder/ingest/destination_directory.rb +7 -1
  26. data/lib/bookbinder/ingest/{git_hub_repository_cloner.rb → git_cloner.rb} +3 -2
  27. data/lib/bookbinder/ingest/repo_identifier.rb +45 -0
  28. data/lib/bookbinder/local_file_system_accessor.rb +4 -9
  29. data/lib/bookbinder/middleman_runner.rb +5 -6
  30. data/lib/bookbinder/preprocessing/copy_to_site_gen_dir.rb +27 -0
  31. data/lib/bookbinder/preprocessing/dita_preprocessor.rb +103 -0
  32. data/lib/bookbinder/preprocessing/preprocessor.rb +26 -0
  33. data/lib/bookbinder/repositories/command_repository.rb +17 -21
  34. data/lib/bookbinder/repositories/section_repository.rb +24 -16
  35. data/lib/bookbinder/repositories/section_repository_factory.rb +19 -0
  36. data/lib/bookbinder/streams/{switchable_stdout_and_red_stderr.rb → colorized_stream.rb} +0 -17
  37. data/lib/bookbinder/time_fetcher.rb +7 -0
  38. data/lib/bookbinder/validation_checkers/dita_section_checker.rb +2 -2
  39. data/lib/bookbinder/values/output_locations.rb +13 -12
  40. data/lib/bookbinder/values/section.rb +22 -5
  41. data/master_middleman/bookbinder_helpers.rb +4 -11
  42. metadata +59 -75
  43. data/lib/bookbinder/book.rb +0 -59
  44. data/lib/bookbinder/config/local_bind_configuration.rb +0 -23
  45. data/lib/bookbinder/dita_preprocessor.rb +0 -68
  46. data/lib/bookbinder/dita_section_gatherer_factory.rb +0 -23
  47. data/lib/bookbinder/git_client.rb +0 -66
  48. data/lib/bookbinder/git_hub_repository.rb +0 -101
  49. data/lib/bookbinder/local_dita_section_gatherer.rb +0 -32
  50. data/lib/bookbinder/remote_dita_section_gatherer.rb +0 -35
  51. data/lib/bookbinder/validation_checkers/config_version_checker.rb +0 -91
  52. data/lib/bookbinder/values/code_example.rb +0 -7
  53. data/lib/bookbinder/values/dita_section.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 61ee95fb7c8fa4d8f94f82fb98582fd65cb95b00
4
- data.tar.gz: 402dbbe4d95e314482d09803c85d2560f424bde5
3
+ metadata.gz: 76b73998f089e3dd934d8eb163cddfd19f66be5e
4
+ data.tar.gz: 98e8dc00c79fad5bd569f4532adde577588afe7a
5
5
  SHA512:
6
- metadata.gz: 657c1ab7338a891a60f59b3f3a01d683f65cac80a528907ee9918d55ef45202615684a57069c72525a65bbf6a310bb4183fecaabde6c2bfa73743ef9daeaa2b1
7
- data.tar.gz: 725747cd9531772d3df4fe41af349bc165bc13fbc8db64c886debfc7f0866d720c730a72ee977ff3759d0f046e3f20dd18267ebe19fe5d47aed302b0f84532ef
6
+ metadata.gz: 932df3bf015a4f130ecbfde27454d173fbdfb58ea187527f60871de14d7066ff797e0c91c01573cf4da8473149a466618021c2cda517a18e3f9275a80d8faae1
7
+ data.tar.gz: 8e481e936fbf7f12f16d4f735d58f6b28f4db037fe42a9393bfa2315f9725f3eaaaa3a965ecaada1e1023c61c84d1ba8820710ef7460f557f9a662df6d25f072
@@ -1,7 +1,6 @@
1
1
  require 'fog/aws'
2
2
  require 'tmpdir'
3
3
  require 'ansi'
4
- require 'octokit'
5
4
  require 'middleman-syntax'
6
5
  require 'middleman-core/cli'
7
6
  require 'middleman-core/profiling'
@@ -11,10 +10,7 @@ require 'vienna'
11
10
  require 'puma'
12
11
 
13
12
  require_relative 'bookbinder/deprecated_logger'
14
- require_relative 'bookbinder/git_client'
15
13
  require_relative 'bookbinder/values/section'
16
- require_relative 'bookbinder/book'
17
- require_relative 'bookbinder/values/code_example'
18
14
  require_relative 'bookbinder/remote_yaml_credential_provider'
19
15
  require_relative 'bookbinder/configuration'
20
16
  require_relative 'bookbinder/configuration_fetcher'
@@ -2,20 +2,22 @@ require 'fog/aws'
2
2
  require 'tmpdir'
3
3
  require_relative 'artifact_namer'
4
4
  require_relative 'deprecated_logger'
5
+ require_relative 'time_fetcher'
5
6
 
6
7
  module Bookbinder
7
8
  class Archive
8
9
  class FileDoesNotExist < StandardError; end
9
10
  class NoNamespaceGiven < StandardError; end
10
11
 
11
- def initialize(logger: nil, key: '', secret: '')
12
+ def initialize(logger: nil, time_fetcher: TimeFetcher.new, key: '', secret: '')
12
13
  @logger = logger
14
+ @time_fetcher = time_fetcher
13
15
  @aws_key = key
14
16
  @aws_secret_key = secret
15
17
  end
16
18
 
17
19
  def create_and_upload_tarball(build_number: nil, app_dir: 'final_app', bucket: '', namespace: nil)
18
- raise 'You must provide a build_number to push an identifiable build.' unless build_number
20
+ build_number ||= time_fetcher.current_time
19
21
  raise 'You must provide a namespace to push an identifiable build.' unless namespace
20
22
 
21
23
  tarball_filename, tarball_path = create_tarball(app_dir, build_number, namespace)
@@ -48,8 +50,14 @@ module Bookbinder
48
50
  @logger.log "Green build ##{build_number.to_s.green} has been downloaded from S3 and untarred into #{download_dir.to_s.cyan}"
49
51
  end
50
52
 
53
+ def tarball_name_regex(namespace)
54
+ /^#{namespace}-(\d+_?\d*)\.tgz/
55
+ end
56
+
51
57
  private
52
58
 
59
+ attr_reader :time_fetcher
60
+
53
61
  def find_or_create_directory(name)
54
62
  connection.directories.create(key: name)
55
63
  rescue Excon::Errors::Conflict
@@ -69,15 +77,14 @@ module Bookbinder
69
77
 
70
78
  all_files_with_namespace = all_files.map do |file|
71
79
  filename = file.key
72
- matches = /^#{namespace}-([\d]+)\.tgz/.match(filename)
73
- file if matches
80
+ file if filename[tarball_name_regex(namespace)]
74
81
  end.compact
75
82
 
76
83
  return nil if all_files_with_namespace.empty?
77
84
  most_recent_file = all_files_with_namespace.sort_by { |file| file.last_modified }.last
78
85
 
79
86
  most_recent_filename = most_recent_file.key
80
- most_recent_build_number = /^#{namespace}-([\d]+)\.tgz/.match(most_recent_filename)[1]
87
+ most_recent_filename[tarball_name_regex(namespace), 1]
81
88
  end
82
89
 
83
90
  def connection
@@ -7,8 +7,8 @@ module Bookbinder
7
7
 
8
8
  def generate(base_config, sections)
9
9
  base_config.merge(
10
- archive_menu: root_config(base_config).merge(section_config(sections))
11
- )
10
+ Configuration.parse(
11
+ 'archive_menu' => root_config(base_config).merge(section_config(sections))))
12
12
  end
13
13
 
14
14
  private
@@ -16,7 +16,7 @@ module Bookbinder
16
16
  attr_reader :loader, :config_filename
17
17
 
18
18
  def root_config(base_config)
19
- { '.' => base_config[:archive_menu] }
19
+ { '.' => base_config.archive_menu }
20
20
  end
21
21
 
22
22
  def section_config(sections)
@@ -24,7 +24,7 @@ module Bookbinder
24
24
  config_path = section.path_to_repository.join(config_filename)
25
25
  archive_config = loader.load_key(config_path, 'archive_menu')
26
26
  if archive_config
27
- config.merge(section.directory_name => archive_config)
27
+ config.merge(section.desired_directory_name => archive_config)
28
28
  else
29
29
  config
30
30
  end
@@ -1,13 +1,10 @@
1
1
  require 'middleman-syntax'
2
2
 
3
3
  require_relative '../archive_menu_configuration'
4
- require_relative '../book'
5
- require_relative '../dita_section_gatherer_factory'
6
4
  require_relative '../errors/cli_error'
7
- require_relative '../streams/switchable_stdout_and_red_stderr'
8
- require_relative '../values/dita_section'
9
5
  require_relative '../values/output_locations'
10
6
  require_relative '../values/section'
7
+ require_relative 'bind/bind_options'
11
8
  require_relative 'naming'
12
9
 
13
10
  module Bookbinder
@@ -15,10 +12,7 @@ module Bookbinder
15
12
  class Bind
16
13
  include Commands::Naming
17
14
 
18
- DitaToHtmlLibraryFailure = Class.new(RuntimeError)
19
-
20
15
  def initialize(logger,
21
- config_fetcher,
22
16
  config_factory,
23
17
  archive_menu_config,
24
18
  version_control_system,
@@ -27,15 +21,11 @@ module Bookbinder
27
21
  sitemap_writer,
28
22
  final_app_directory,
29
23
  context_dir,
30
- dita_preprocessor,
24
+ preprocessor,
31
25
  cloner_factory,
32
- dita_section_gatherer_factory,
33
- section_repository,
34
- command_creator,
35
- sheller,
26
+ section_repository_factory,
36
27
  directory_preparer)
37
28
  @logger = logger
38
- @config_fetcher = config_fetcher
39
29
  @config_factory = config_factory
40
30
  @archive_menu_config = archive_menu_config
41
31
  @version_control_system = version_control_system
@@ -44,18 +34,15 @@ module Bookbinder
44
34
  @sitemap_writer = sitemap_writer
45
35
  @final_app_directory = final_app_directory
46
36
  @context_dir = context_dir
47
- @dita_preprocessor = dita_preprocessor
37
+ @preprocessor = preprocessor
48
38
  @cloner_factory = cloner_factory
49
- @dita_section_gatherer_factory = dita_section_gatherer_factory
50
- @section_repository = section_repository
51
- @command_creator = command_creator
52
- @sheller = sheller
39
+ @section_repository_factory = section_repository_factory
53
40
  @directory_preparer = directory_preparer
54
41
  end
55
42
 
56
43
  def usage
57
- ["bind <local|github> [--verbose]",
58
- "Bind the sections specified in config.yml from <local> or <github> into the final_app directory"]
44
+ ["bind <local|remote> [--verbose] [--dita-flags='<dita-option>=<value>']",
45
+ "Bind the sections specified in config.yml from <local> or <remote> into the final_app directory"]
59
46
  end
60
47
 
61
48
  def command_for?(test_command_name)
@@ -67,59 +54,40 @@ module Bookbinder
67
54
  end
68
55
 
69
56
  def run(cli_arguments)
70
- bind_source, *options = cli_arguments
71
- validate(bind_source, options)
57
+ bind_options = BindComponents::BindOptions.new(cli_arguments)
58
+ bind_options.validate!
72
59
 
60
+ bind_source, *options = cli_arguments
73
61
  bind_config = config_factory.produce(bind_source)
74
- output_streams = Streams::SwitchableStdoutAndRedStderr.new(options)
75
62
 
76
63
  output_locations = OutputLocations.new(
77
64
  context_dir: context_dir,
78
65
  final_app_dir: final_app_directory,
79
- layout_repo_dir: layout_repo_path(generate_local_repo_dir(context_dir, bind_source)),
66
+ layout_repo_dir: layout_repo_path(bind_config, generate_local_repo_dir(context_dir, bind_source)),
80
67
  local_repo_dir: generate_local_repo_dir(context_dir, bind_source)
81
68
  )
69
+ cloner = cloner_factory.produce(output_locations.local_repo_dir)
70
+ section_repository = section_repository_factory.produce(cloner)
82
71
 
83
72
  directory_preparer.prepare_directories(
73
+ bind_config,
84
74
  File.expand_path('../../../../', __FILE__),
85
- bind_config.fetch(:versions, []),
86
- output_locations,
87
- bind_config[:book_repo]
75
+ output_locations
88
76
  )
89
77
 
90
- dita_gatherer = dita_section_gatherer_factory.produce(bind_source, output_locations)
91
- gathered_dita_sections = dita_gatherer.gather(config.dita_sections)
92
-
93
- dita_preprocessor.preprocess(gathered_dita_sections,
94
- output_locations.subnavs_for_layout_dir,
95
- output_locations.dita_subnav_template_path) do |dita_section|
96
- command = command_creator.convert_to_html_command(
97
- dita_section,
98
- write_to: dita_section.html_from_dita_section_dir
99
- )
100
- status = sheller.run_command(command, output_streams.to_h)
101
- unless status.success?
102
- raise DitaToHtmlLibraryFailure.new 'The DITA-to-HTML conversion failed. ' +
103
- 'Please check that you have specified the path to your DITA-OT library in the ENV, ' +
104
- 'that your DITA-specific keys/values in config.yml are set, ' +
105
- 'and that your DITA toolkit is correctly configured.'
106
- end
107
- end
108
-
109
- cloner = cloner_factory.produce(
110
- bind_source,
111
- output_locations.local_repo_dir
112
- )
113
- sections = gather_sections(
114
- output_locations.source_for_site_generator,
115
- cloner,
116
- ('master' if options.include?('--ignore-section-refs'))
78
+ sections = section_repository.fetch(
79
+ configured_sections: bind_config.sections,
80
+ destination_dir: output_locations.cloned_preprocessing_dir,
81
+ ref_override: bind_options.ref_override
117
82
  )
118
83
 
119
- subnavs = (sections + gathered_dita_sections).map(&:subnav).reduce(&:merge)
84
+ preprocessor.preprocess(sections,
85
+ output_locations,
86
+ options: options,
87
+ output_streams: bind_options.streams)
120
88
 
121
89
  success = publish(
122
- subnavs,
90
+ sections.map(&:subnav).reduce(&:merge),
123
91
  {verbose: options.include?('--verbose')},
124
92
  output_locations,
125
93
  archive_menu_config.generate(bind_config, sections),
@@ -132,7 +100,6 @@ module Bookbinder
132
100
  private
133
101
 
134
102
  attr_reader :version_control_system,
135
- :config_fetcher,
136
103
  :config_factory,
137
104
  :archive_menu_config,
138
105
  :logger,
@@ -141,18 +108,15 @@ module Bookbinder
141
108
  :final_app_directory,
142
109
  :sitemap_writer,
143
110
  :context_dir,
144
- :dita_preprocessor,
111
+ :preprocessor,
145
112
  :cloner_factory,
146
- :dita_section_gatherer_factory,
147
- :section_repository,
148
- :command_creator,
149
- :sheller,
113
+ :section_repository_factory,
150
114
  :directory_preparer
151
115
 
152
116
  def publish(subnavs, cli_options, output_locations, publish_config, cloner)
153
117
  FileUtils.cp 'redirects.rb', output_locations.final_app_dir if File.exists?('redirects.rb')
154
118
 
155
- host_for_sitemap = publish_config.fetch(:host_for_sitemap)
119
+ host_for_sitemap = publish_config.public_host
156
120
 
157
121
  static_site_generator.run(output_locations,
158
122
  publish_config,
@@ -178,34 +142,11 @@ module Bookbinder
178
142
  File.expand_path('..', context_dir) if bind_source == 'local'
179
143
  end
180
144
 
181
- def gather_sections(workspace, cloner, ref_override)
182
- config.sections.map do |section_config|
183
- target_ref = ref_override ||
184
- section_config.fetch('repository', {})['ref'] ||
185
- 'master'
186
- repo_name = section_config.fetch('repository').fetch('name')
187
- directory = section_config['directory']
188
- working_copy = cloner.call(source_repo_name: repo_name,
189
- source_ref: target_ref,
190
- destination_parent_dir: workspace,
191
- destination_dir_name: directory)
192
- @section_repository.get_instance(
193
- section_config,
194
- working_copy: working_copy,
195
- destination_dir: workspace
196
- ) { |*args| Section.new(*args) }
197
- end
198
- end
199
-
200
- def config
201
- config_fetcher.fetch_config
202
- end
203
-
204
- def layout_repo_path(local_repo_dir)
145
+ def layout_repo_path(config, local_repo_dir)
205
146
  if local_repo_dir && config.has_option?('layout_repo')
206
147
  File.join(local_repo_dir, config.layout_repo.split('/').last)
207
148
  elsif config.has_option?('layout_repo')
208
- cloner = cloner_factory.produce('github', nil)
149
+ cloner = cloner_factory.produce(nil)
209
150
  working_copy = cloner.call(source_repo_name: config.layout_repo,
210
151
  destination_parent_dir: Dir.mktmpdir)
211
152
  working_copy.path
@@ -213,15 +154,6 @@ module Bookbinder
213
154
  File.absolute_path('master_middleman')
214
155
  end
215
156
  end
216
-
217
- def validate(bind_source, options)
218
- raise CliError::InvalidArguments unless arguments_are_valid?(bind_source, options)
219
- end
220
-
221
- def arguments_are_valid?(bind_source, options)
222
- valid_options = %w(--verbose --ignore-section-refs).to_set
223
- %w(local github).include?(bind_source) && options.to_set.subset?(valid_options)
224
- end
225
157
  end
226
158
  end
227
159
  end
@@ -0,0 +1,50 @@
1
+ require_relative '../../sheller'
2
+ require_relative '../../streams/colorized_stream'
3
+
4
+ module Bookbinder
5
+ module Commands
6
+ module BindComponents
7
+ class BindOptions
8
+ def initialize(opts)
9
+ @opts = opts
10
+ end
11
+
12
+ def validate!
13
+ raise CliError::InvalidArguments unless arguments_are_valid?
14
+ end
15
+
16
+ def ref_override
17
+ 'master' if opts.include?('--ignore-section-refs')
18
+ end
19
+
20
+ def streams
21
+ {
22
+ out: opts.include?('--verbose') ? $stdout : Sheller::DevNull.new,
23
+ err: Streams::ColorizedStream.new(Colorizer::Colors.red, $stderr)
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :opts
30
+
31
+ def arguments_are_valid?
32
+ valid_options = %w(--verbose --ignore-section-refs --dita-flags).to_set
33
+ %w(local remote github).include?(bind_source) && flag_names.to_set.subset?(valid_options)
34
+ end
35
+
36
+ def flag_names
37
+ options.map {|o| o.split('=').first}
38
+ end
39
+
40
+ def bind_source
41
+ opts.first
42
+ end
43
+
44
+ def options
45
+ opts[1..-1]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative '../../directory_helpers'
2
+ require_relative '../../ingest/destination_directory'
2
3
 
3
4
  module Bookbinder
4
5
  module Commands
@@ -12,17 +13,17 @@ module Bookbinder
12
13
  @version_control_system = version_control_system
13
14
  end
14
15
 
15
- def prepare_directories(gem_root, versions, locations, book_repo)
16
+ def prepare_directories(config, gem_root, locations)
16
17
  forget_sections(locations.output_dir)
17
18
  file_system_accessor.remove_directory(File.join(locations.final_app_dir, '.'))
18
- file_system_accessor.remove_directory(locations.dita_home_dir)
19
+ file_system_accessor.remove_directory(locations.preprocessing_home_dir)
19
20
 
20
21
  copy_directory_from_gem(gem_root, 'template_app', locations.final_app_dir)
21
22
  copy_directory_from_gem(gem_root, 'master_middleman', locations.site_generator_home)
22
23
  file_system_accessor.copy(File.join(locations.layout_repo_dir, '.'), locations.site_generator_home)
23
24
 
24
- versions.each do |version|
25
- copy_index_file_from_version_to_master_middleman(version, locations.source_for_site_generator, book_repo)
25
+ config.versions.each do |version|
26
+ copy_index_file_from_version_to_master_middleman(version, locations.source_for_site_generator, config.book_repo_url)
26
27
  end
27
28
  end
28
29
 
@@ -30,18 +31,18 @@ module Bookbinder
30
31
 
31
32
  attr_reader :logger, :file_system_accessor, :version_control_system
32
33
 
33
- def copy_index_file_from_version_to_master_middleman(version, dest_dir, book_repo)
34
+ def copy_index_file_from_version_to_master_middleman(version, dest_dir, url)
35
+ clone_dir_name = Ingest::DestinationDirectory.new(url)
34
36
  Dir.mktmpdir(version) do |tmpdir|
35
- book = Book.from_remote(logger: logger,
36
- full_name: book_repo,
37
- destination_dir: tmpdir,
38
- ref: version,
39
- git_accessor: version_control_system)
40
- index_source_dir = File.join(tmpdir, book.directory, 'master_middleman', source_dir_name)
37
+ version_control_system.clone(url,
38
+ clone_dir_name,
39
+ path: tmpdir,
40
+ checkout: version)
41
+ index_source_dir = Pathname(tmpdir).join(clone_dir_name, 'master_middleman', source_dir_name)
41
42
  index_dest_dir = File.join(dest_dir, version)
42
43
  file_system_accessor.make_directory(index_dest_dir)
43
44
 
44
- Dir.glob(File.join(index_source_dir, 'index.*')) do |f|
45
+ Dir.glob(index_source_dir.join('index.*')) do |f|
45
46
  file_system_accessor.copy(File.expand_path(f), index_dest_dir)
46
47
  end
47
48
  end
@@ -8,24 +8,17 @@ module Bookbinder
8
8
  class BuildAndPushTarball < BookbinderCommand
9
9
  include Commands::Naming
10
10
 
11
- class MissingBuildNumber < StandardError
12
- def initialize
13
- super 'You must set $BUILD_NUMBER to push an identifiable build.'
14
- end
15
- end
16
-
17
11
  def usage
18
12
  [command_name,
19
13
  "Create a tarball from the final_app directory and push to the S3 bucket specified in your credentials.yml"]
20
14
  end
21
15
 
22
16
  def run(_)
23
- raise MissingBuildNumber unless ENV['BUILD_NUMBER']
24
17
  config = configuration_fetcher.fetch_config
25
18
  aws_credentials = configuration_fetcher.fetch_credentials[:aws]
26
19
  archive = Archive.new(logger: @logger, key: aws_credentials.access_key, secret: aws_credentials.secret_key)
27
20
  archive.create_and_upload_tarball(build_number: ENV['BUILD_NUMBER'], bucket: aws_credentials.green_builds_bucket,
28
- namespace: Ingest::DestinationDirectory.new(config.book_repo, nil))
21
+ namespace: Ingest::DestinationDirectory.new(config.book_repo))
29
22
  0
30
23
  end
31
24
  end