pdk-akerl 1.8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +826 -0
  3. data/LICENSE +201 -0
  4. data/README.md +133 -0
  5. data/exe/pdk +10 -0
  6. data/lib/pdk.rb +10 -0
  7. data/lib/pdk/answer_file.rb +121 -0
  8. data/lib/pdk/cli.rb +113 -0
  9. data/lib/pdk/cli/build.rb +76 -0
  10. data/lib/pdk/cli/bundle.rb +42 -0
  11. data/lib/pdk/cli/convert.rb +41 -0
  12. data/lib/pdk/cli/errors.rb +23 -0
  13. data/lib/pdk/cli/exec.rb +246 -0
  14. data/lib/pdk/cli/exec_group.rb +67 -0
  15. data/lib/pdk/cli/module.rb +14 -0
  16. data/lib/pdk/cli/module/build.rb +14 -0
  17. data/lib/pdk/cli/module/generate.rb +45 -0
  18. data/lib/pdk/cli/new.rb +17 -0
  19. data/lib/pdk/cli/new/class.rb +32 -0
  20. data/lib/pdk/cli/new/defined_type.rb +30 -0
  21. data/lib/pdk/cli/new/module.rb +41 -0
  22. data/lib/pdk/cli/new/provider.rb +27 -0
  23. data/lib/pdk/cli/new/task.rb +31 -0
  24. data/lib/pdk/cli/test.rb +12 -0
  25. data/lib/pdk/cli/test/unit.rb +88 -0
  26. data/lib/pdk/cli/update.rb +32 -0
  27. data/lib/pdk/cli/util.rb +193 -0
  28. data/lib/pdk/cli/util/command_redirector.rb +26 -0
  29. data/lib/pdk/cli/util/interview.rb +63 -0
  30. data/lib/pdk/cli/util/option_normalizer.rb +53 -0
  31. data/lib/pdk/cli/util/option_validator.rb +56 -0
  32. data/lib/pdk/cli/validate.rb +124 -0
  33. data/lib/pdk/generate.rb +11 -0
  34. data/lib/pdk/generate/defined_type.rb +49 -0
  35. data/lib/pdk/generate/module.rb +318 -0
  36. data/lib/pdk/generate/provider.rb +82 -0
  37. data/lib/pdk/generate/puppet_class.rb +48 -0
  38. data/lib/pdk/generate/puppet_object.rb +288 -0
  39. data/lib/pdk/generate/task.rb +86 -0
  40. data/lib/pdk/i18n.rb +4 -0
  41. data/lib/pdk/logger.rb +28 -0
  42. data/lib/pdk/module.rb +21 -0
  43. data/lib/pdk/module/build.rb +214 -0
  44. data/lib/pdk/module/convert.rb +209 -0
  45. data/lib/pdk/module/metadata.rb +193 -0
  46. data/lib/pdk/module/templatedir.rb +313 -0
  47. data/lib/pdk/module/update.rb +111 -0
  48. data/lib/pdk/module/update_manager.rb +210 -0
  49. data/lib/pdk/report.rb +112 -0
  50. data/lib/pdk/report/event.rb +357 -0
  51. data/lib/pdk/template_file.rb +89 -0
  52. data/lib/pdk/tests/unit.rb +213 -0
  53. data/lib/pdk/util.rb +271 -0
  54. data/lib/pdk/util/bundler.rb +253 -0
  55. data/lib/pdk/util/filesystem.rb +12 -0
  56. data/lib/pdk/util/git.rb +74 -0
  57. data/lib/pdk/util/puppet_version.rb +242 -0
  58. data/lib/pdk/util/ruby_version.rb +147 -0
  59. data/lib/pdk/util/vendored_file.rb +88 -0
  60. data/lib/pdk/util/version.rb +42 -0
  61. data/lib/pdk/util/windows.rb +13 -0
  62. data/lib/pdk/util/windows/api_types.rb +57 -0
  63. data/lib/pdk/util/windows/file.rb +36 -0
  64. data/lib/pdk/util/windows/string.rb +16 -0
  65. data/lib/pdk/validate.rb +14 -0
  66. data/lib/pdk/validate/base_validator.rb +209 -0
  67. data/lib/pdk/validate/metadata/metadata_json_lint.rb +86 -0
  68. data/lib/pdk/validate/metadata/metadata_syntax.rb +109 -0
  69. data/lib/pdk/validate/metadata_validator.rb +30 -0
  70. data/lib/pdk/validate/puppet/puppet_lint.rb +67 -0
  71. data/lib/pdk/validate/puppet/puppet_syntax.rb +112 -0
  72. data/lib/pdk/validate/puppet_validator.rb +30 -0
  73. data/lib/pdk/validate/ruby/rubocop.rb +77 -0
  74. data/lib/pdk/validate/ruby_validator.rb +29 -0
  75. data/lib/pdk/validate/tasks/metadata_lint.rb +126 -0
  76. data/lib/pdk/validate/tasks/name.rb +88 -0
  77. data/lib/pdk/validate/tasks_validator.rb +33 -0
  78. data/lib/pdk/version.rb +4 -0
  79. data/locales/config.yaml +21 -0
  80. data/locales/pdk.pot +1283 -0
  81. metadata +304 -0
@@ -0,0 +1,313 @@
1
+ require 'yaml'
2
+ require 'deep_merge'
3
+ require 'pdk/util'
4
+ require 'pdk/util/git'
5
+ require 'pdk/cli/errors'
6
+ require 'pdk/template_file'
7
+
8
+ module PDK
9
+ module Module
10
+ class TemplateDir
11
+ attr_accessor :module_metadata
12
+
13
+ # Initialises the TemplateDir object with the path or URL to the template
14
+ # and the block of code to run to be run while the template is available.
15
+ #
16
+ # The template directory is only guaranteed to be available on disk
17
+ # within the scope of the block passed to this method.
18
+ #
19
+ # @param path_or_url [String] The path to a directory to use as the
20
+ # template or a URL to a git repository.
21
+ # @param module_metadata [Hash] A Hash containing the module metadata.
22
+ # Defaults to an empty Hash.
23
+ # @yieldparam self [PDK::Module::TemplateDir] The initialised object with
24
+ # the template available on disk.
25
+ #
26
+ # @example Using a git repository as a template
27
+ # PDK::Module::TemplateDir.new('https://github.com/puppetlabs/pdk-templates') do |t|
28
+ # t.render do |filename, content|
29
+ # File.open(filename, 'w') do |file|
30
+ # file.write(content)
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ # @raise [ArgumentError] If no block is given to this method.
36
+ # @raise [PDK::CLI::FatalError] (see #clone_repo)
37
+ # @raise [ArgumentError] (see #validate_module_template!)
38
+ #
39
+ # @api public
40
+ def initialize(path_or_url, module_metadata = {}, init = false)
41
+ unless block_given?
42
+ raise ArgumentError, _('%{class_name} must be initialized with a block.') % { class_name: self.class.name }
43
+ end
44
+
45
+ if PDK::Util::Git.repo?(path_or_url)
46
+ @path = self.class.clone_template_repo(path_or_url)
47
+ @repo = path_or_url
48
+ else
49
+ @path = path_or_url
50
+ end
51
+
52
+ @init = init
53
+ @moduleroot_dir = File.join(@path, 'moduleroot')
54
+ @moduleroot_init = File.join(@path, 'moduleroot_init')
55
+ @dirs = [@moduleroot_dir]
56
+ @dirs << @moduleroot_init if @init
57
+ @object_dir = File.join(@path, 'object_templates')
58
+
59
+ validate_module_template!
60
+
61
+ @module_metadata = module_metadata
62
+
63
+ yield self
64
+ ensure
65
+ # If we cloned a git repo to get the template, remove the clone once
66
+ # we're done with it.
67
+ if @repo
68
+ FileUtils.remove_dir(@path)
69
+ end
70
+ end
71
+
72
+ # Retrieve identifying metadata for the template.
73
+ #
74
+ # For git repositories, this will return the URL to the repository and
75
+ # a reference to the HEAD.
76
+ #
77
+ # @return [Hash{String => String}] A hash of identifying metadata.
78
+ #
79
+ # @api public
80
+ def metadata
81
+ result = {
82
+ 'pdk-version' => PDK::Util::Version.version_string,
83
+ }
84
+
85
+ result['template-url'] = @repo ? @repo : @path
86
+
87
+ ref_result = PDK::Util::Git.git('--git-dir', File.join(@path, '.git'), 'describe', '--all', '--long', '--always')
88
+ result['template-ref'] = ref_result[:stdout].strip if ref_result[:exit_code].zero?
89
+
90
+ result
91
+ end
92
+
93
+ # Loop through the files in the template, yielding each rendered file to
94
+ # the supplied block.
95
+ #
96
+ # @yieldparam dest_path [String] The path of the destination file,
97
+ # relative to the root of the module.
98
+ # @yieldparam dest_content [String] The rendered content of the
99
+ # destination file.
100
+ #
101
+ # @raise [PDK::CLI::FatalError] If the template fails to render.
102
+ #
103
+ # @return [void]
104
+ #
105
+ # @api public
106
+ def render
107
+ PDK::Module::TemplateDir.files_in_template(@dirs).each do |template_file, template_loc|
108
+ template_file = template_file.to_s
109
+ PDK.logger.debug(_("Rendering '%{template}'...") % { template: template_file })
110
+ dest_path = template_file.sub(%r{\.erb\Z}, '')
111
+ config = config_for(dest_path)
112
+ dest_status = :manage
113
+
114
+ if config['unmanaged']
115
+ dest_status = :unmanage
116
+ elsif config['delete']
117
+ dest_status = :delete
118
+ else
119
+ begin
120
+ dest_content = PDK::TemplateFile.new(File.join(template_loc, template_file), configs: config).render
121
+ rescue => e
122
+ error_msg = _(
123
+ "Failed to render template '%{template}'\n" \
124
+ '%{exception}: %{message}',
125
+ ) % { template: template_file, exception: e.class, message: e.message }
126
+ raise PDK::CLI::FatalError, error_msg
127
+ end
128
+ end
129
+
130
+ yield dest_path, dest_content, dest_status
131
+ end
132
+ end
133
+
134
+ # Searches the template directory for template files that can be used to
135
+ # render files for the specified object type.
136
+ #
137
+ # @param object_type [Symbol] The object type, e.g. (`:class`,
138
+ # `:defined_type`, `:fact`, etc).
139
+ #
140
+ # @return [Hash{Symbol => String}] if the templates are available in the
141
+ # template dir, otherwise `nil`. The returned hash can contain two keys,
142
+ # :object contains the path on disk to the template for the object, :spec
143
+ # contains the path on disk to the template for the object's spec file
144
+ # (if available).
145
+ #
146
+ # @api public
147
+ def object_template_for(object_type)
148
+ object_path = File.join(@object_dir, "#{object_type}.erb")
149
+ type_path = File.join(@object_dir, "#{object_type}_type.erb")
150
+ spec_path = File.join(@object_dir, "#{object_type}_spec.erb")
151
+ type_spec_path = File.join(@object_dir, "#{object_type}_type_spec.erb")
152
+
153
+ if File.file?(object_path) && File.readable?(object_path)
154
+ result = { object: object_path }
155
+ result[:type] = type_path if File.file?(type_path) && File.readable?(type_path)
156
+ result[:spec] = spec_path if File.file?(spec_path) && File.readable?(spec_path)
157
+ result[:type_spec] = type_spec_path if File.file?(type_spec_path) && File.readable?(type_spec_path)
158
+ result
159
+ else
160
+ nil
161
+ end
162
+ end
163
+
164
+ # Generate a hash of data to be used when rendering object templates.
165
+ #
166
+ # Read `config_defaults.yml` from the root of the template directory (if
167
+ # it exists) build a hash of values from the value of the `:global`
168
+ # key.
169
+ #
170
+ # @return [Hash] The data that will be available to the template via the
171
+ # `@configs` instance variable.
172
+ #
173
+ # @api private
174
+ def object_config
175
+ config_for(nil)
176
+ end
177
+
178
+ # Validate the content of the template directory.
179
+ #
180
+ # @raise [ArgumentError] If the specified path is not a directory.
181
+ # @raise [ArgumentError] If the template directory does not contain
182
+ # a directory called 'moduleroot'.
183
+ #
184
+ # @return [void]
185
+ #
186
+ # @api private
187
+ def validate_module_template!
188
+ # rubocop:disable Style/GuardClause
189
+ unless File.directory?(@path)
190
+ if PDK::Util.package_install? && File.fnmatch?(File.join(PDK::Util.package_cachedir, '*'), @path)
191
+ raise ArgumentError, _('The built-in template has substantially changed. Please run "pdk convert" on your module to continue.')
192
+ else
193
+ raise ArgumentError, _("The specified template '%{path}' is not a directory.") % { path: @path }
194
+ end
195
+ end
196
+
197
+ unless File.directory?(@moduleroot_dir)
198
+ raise ArgumentError, _("The template at '%{path}' does not contain a 'moduleroot/' directory.") % { path: @path }
199
+ end
200
+
201
+ unless File.directory?(@moduleroot_init)
202
+ # rubocop:disable Metrics/LineLength
203
+ raise ArgumentError, _("The template at '%{path}' does not contain a 'moduleroot_init/' directory, which indicates you are using an older style of template. Before continuing please use the --template-url flag when running the pdk new commands to pass a new style template.") % { path: @path }
204
+ # rubocop:enable Metrics/LineLength Style/GuardClause
205
+ end
206
+ end
207
+
208
+ # Get a list of template files in the template directory.
209
+ #
210
+ # @return [Hash{String=>String}] A hash of key file names and
211
+ # value locations.
212
+ #
213
+ # @api public
214
+ def self.files_in_template(dirs)
215
+ temp_paths = []
216
+ dirlocs = []
217
+ dirs.each do |dir|
218
+ raise ArgumentError, _("The directory '%{dir}' doesn't exist") % { dir: dir } unless Dir.exist?(dir)
219
+ temp_paths += Dir.glob(File.join(dir, '**', '*'), File::FNM_DOTMATCH).select do |template_path|
220
+ if File.file?(template_path) && !File.symlink?(template_path)
221
+ dirlocs << dir
222
+ end
223
+ end
224
+ temp_paths.map do |template_path|
225
+ template_path.sub!(%r{\A#{Regexp.escape(dir)}#{Regexp.escape(File::SEPARATOR)}}, '')
226
+ end
227
+ end
228
+ Hash[temp_paths.zip dirlocs]
229
+ end
230
+
231
+ # Generate a hash of data to be used when rendering the specified
232
+ # template.
233
+ #
234
+ # @param dest_path [String] The destination path of the file that the
235
+ # data is for, relative to the root of the module.
236
+ #
237
+ # @return [Hash] The data that will be available to the template via the
238
+ # `@configs` instance variable.
239
+ #
240
+ # @api private
241
+ def config_for(dest_path, sync_config_path = nil)
242
+ module_root = PDK::Util.module_root
243
+ sync_config_path ||= File.join(module_root, '.sync.yml') unless module_root.nil?
244
+ config_path = File.join(@path, 'config_defaults.yml')
245
+
246
+ if @config.nil?
247
+ conf_defaults = read_config(config_path)
248
+ sync_config = read_config(sync_config_path) unless sync_config_path.nil?
249
+ @config = conf_defaults
250
+ @config.deep_merge!(sync_config, knockout_prefix: '---') unless sync_config.nil?
251
+ end
252
+ file_config = @config.fetch(:global, {})
253
+ file_config['module_metadata'] = @module_metadata
254
+ file_config.merge!(@config.fetch(dest_path, {})) unless dest_path.nil?
255
+ file_config.merge!(@config)
256
+ end
257
+
258
+ # Generates a hash of data from a given yaml file location.
259
+ #
260
+ # @param loc [String] The path of the yaml config file.
261
+ #
262
+ # @warn If the specified path is not a valid yaml file. Returns an empty Hash
263
+ # if so.
264
+ #
265
+ # @return [Hash] The data that has been read in from the given yaml file.
266
+ #
267
+ # @api private
268
+ def read_config(loc)
269
+ if File.file?(loc) && File.readable?(loc)
270
+ begin
271
+ YAML.safe_load(File.read(loc), [], [], true)
272
+ rescue StandardError => e
273
+ PDK.logger.warn(_("'%{file}' is not a valid YAML file: %{message}") % { file: loc, message: e.message })
274
+ {}
275
+ end
276
+ else
277
+ {}
278
+ end
279
+ end
280
+
281
+ # @return [String] Path to working directory into which template repo has been cloned and reset
282
+ #
283
+ # @raise [PDK::CLI::FatalError] If unable to clone the given origin_repo into a tempdir.
284
+ # @raise [PDK::CLI::FatalError] If reset HEAD of the cloned repo to desired ref.
285
+ #
286
+ # @api private
287
+ def self.clone_template_repo(origin_repo)
288
+ # @todo When switching this over to using rugged, cache the cloned
289
+ # template repo in `%AppData%` or `$XDG_CACHE_DIR` and update before
290
+ # use.
291
+ temp_dir = PDK::Util.make_tmpdir_name('pdk-templates')
292
+ git_ref = (origin_repo == PDK::Util.default_template_url) ? PDK::Util.default_template_ref : 'origin/master'
293
+
294
+ clone_result = PDK::Util::Git.git('clone', origin_repo, temp_dir)
295
+
296
+ if clone_result[:exit_code].zero?
297
+ reset_result = PDK::Util::Git.git('-C', temp_dir, 'reset', '--hard', git_ref)
298
+ unless reset_result[:exit_code].zero?
299
+ PDK.logger.error reset_result[:stdout]
300
+ PDK.logger.error reset_result[:stderr]
301
+ raise PDK::CLI::FatalError, _("Unable to set HEAD of git repository at '%{repo}' to ref:'%{ref}'.") % { repo: temp_dir, ref: git_ref }
302
+ end
303
+ else
304
+ PDK.logger.error clone_result[:stdout]
305
+ PDK.logger.error clone_result[:stderr]
306
+ raise PDK::CLI::FatalError, _("Unable to clone git repository at '%{repo}' into '%{dest}'.") % { repo: origin_repo, dest: temp_dir }
307
+ end
308
+
309
+ PDK::Util.canonical_path(temp_dir)
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,111 @@
1
+ require 'pdk/module/convert'
2
+
3
+ module PDK
4
+ module Module
5
+ class Update < Convert
6
+ GIT_DESCRIBE_PATTERN = %r{\A(?<base>.+?)-(?<additional_commits>\d+)-g(?<sha>.+)\Z}
7
+
8
+ def run
9
+ stage_changes!
10
+
11
+ if current_version == new_version
12
+ PDK.logger.debug _('This module is already up to date with version %{version} of the template.') % {
13
+ version: new_version,
14
+ }
15
+ end
16
+
17
+ unless update_manager.changes?
18
+ PDK::Report.default_target.puts(_('No changes required.'))
19
+ return
20
+ end
21
+
22
+ PDK.logger.info(update_message)
23
+
24
+ print_summary
25
+ full_report('update_report.txt') unless update_manager.changes[:modified].empty?
26
+
27
+ return if noop?
28
+
29
+ unless force?
30
+ message = _('Do you want to continue and make these changes to your module?')
31
+ return unless PDK::CLI::Util.prompt_for_yes(message)
32
+ end
33
+
34
+ # Remove these files straight away as these changes are not something that the user needs to review.
35
+ if needs_bundle_update?
36
+ update_manager.unlink_file('Gemfile.lock')
37
+ update_manager.unlink_file(File.join('.bundle', 'config'))
38
+ end
39
+
40
+ update_manager.sync_changes!
41
+
42
+ PDK::Util::Bundler.ensure_bundle! if needs_bundle_update?
43
+
44
+ print_result 'Update completed'
45
+ end
46
+
47
+ def module_metadata
48
+ @module_metadata ||= PDK::Module::Metadata.from_file('metadata.json')
49
+ rescue ArgumentError => e
50
+ raise PDK::CLI::ExitWithError, e.message
51
+ end
52
+
53
+ def template_url
54
+ @template_url ||= module_metadata.data['template-url']
55
+ end
56
+
57
+ def current_version
58
+ @current_version ||= describe_ref_to_s(current_template_version)
59
+ end
60
+
61
+ def new_version
62
+ @new_version ||= fetch_remote_version(new_template_version)
63
+ end
64
+
65
+ private
66
+
67
+ def current_template_version
68
+ @current_template_version ||= module_metadata.data['template-ref']
69
+ end
70
+
71
+ def describe_ref_to_s(describe_ref)
72
+ data = GIT_DESCRIBE_PATTERN.match(describe_ref)
73
+
74
+ return data if data.nil?
75
+
76
+ if data[:base].start_with?('heads/')
77
+ "#{data[:base].gsub(%r{^heads/}, '')}@#{data[:sha]}"
78
+ else
79
+ data[:base]
80
+ end
81
+ end
82
+
83
+ def new_template_version
84
+ PDK::Util.default_template_ref
85
+ end
86
+
87
+ def fetch_remote_version(version)
88
+ return version unless version.include?('/')
89
+
90
+ branch = version.partition('/').last
91
+ sha_length = GIT_DESCRIBE_PATTERN.match(current_template_version)[:sha].length - 1
92
+ "#{branch}@#{PDK::Util::Git.ls_remote(template_url, "refs/heads/#{branch}")[0..sha_length]}"
93
+ end
94
+
95
+ def update_message
96
+ format_string = if template_url == PDK::Util.puppetlabs_template_url
97
+ _('Updating %{module_name} using the default template, from %{current_version} to %{new_version}')
98
+ else
99
+ _('Updating %{module_name} using the template at %{template_url}, from %{current_version} to %{new_version}')
100
+ end
101
+
102
+ format_string % {
103
+ module_name: module_metadata.data['name'],
104
+ template_url: template_url,
105
+ current_version: current_version,
106
+ new_version: new_version,
107
+ }
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,210 @@
1
+ require 'diff/lcs'
2
+ require 'diff/lcs/hunk'
3
+ require 'English'
4
+ require 'fileutils'
5
+ require 'set'
6
+ require 'pdk/util/filesystem'
7
+
8
+ module PDK
9
+ module Module
10
+ class UpdateManager
11
+ # Initialises a blank UpdateManager object, which is used to store and
12
+ # process file additions/removals/modifications.
13
+ def initialize
14
+ @modified_files = Set.new
15
+ @added_files = Set.new
16
+ @removed_files = Set.new
17
+ @diff_cache = {}
18
+ end
19
+
20
+ # Store a pending modification to an existing file.
21
+ #
22
+ # @param path [String] The path to the file to be modified.
23
+ # @param content [String] The new content of the file.
24
+ def modify_file(path, content)
25
+ @modified_files << { path: path, content: content }
26
+ end
27
+
28
+ # Store a pending file addition.
29
+ #
30
+ # @param path [String] The path where the file will be created.
31
+ # @param content [String] The content of the new file.
32
+ def add_file(path, content)
33
+ @added_files << { path: path, content: content }
34
+ end
35
+
36
+ # Store a pending file removal.
37
+ #
38
+ # @param path [String] The path to the file to be removed.
39
+ def remove_file(path)
40
+ @removed_files << path
41
+ end
42
+
43
+ # Generate a summary of the changes that will be applied to the module.
44
+ #
45
+ # @raise (see #calculate_diffs)
46
+ # @return [Hash{Symbol => Set,Hash}] the summary of the pending changes.
47
+ def changes
48
+ calculate_diffs
49
+
50
+ {
51
+ added: @added_files,
52
+ removed: @removed_files.select { |f| File.exist?(f) },
53
+ modified: @diff_cache.reject { |_, value| value.nil? },
54
+ }
55
+ end
56
+
57
+ # Check if there are any pending changes to apply to the module.
58
+ #
59
+ # @raise (see #changes)
60
+ # @return [Boolean] true if there are changes to apply to the module.
61
+ def changes?
62
+ !changes[:added].empty? ||
63
+ !changes[:removed].empty? ||
64
+ changes[:modified].any? { |_, value| !value.nil? }
65
+ end
66
+
67
+ # Check if the update manager will change the specified file upon sync.
68
+ #
69
+ # @param path [String] The path to the file.
70
+ #
71
+ # @raise (see #changes)
72
+ # @return [Boolean] true if the file will be changed.
73
+ def changed?(path)
74
+ changes[:added].any? { |add| add[:path] == path } ||
75
+ changes[:removed].include?(path) ||
76
+ changes[:modified].keys.include?(path)
77
+ end
78
+
79
+ # Apply any pending changes stored in the UpdateManager to the module.
80
+ #
81
+ # @raise (see #calculate_diffs)
82
+ # @raise (see #write_file)
83
+ # @raise (see #unlink_file)
84
+ def sync_changes!
85
+ calculate_diffs
86
+
87
+ files_to_write = @added_files
88
+ files_to_write += @modified_files.reject { |file| @diff_cache[file[:path]].nil? }
89
+
90
+ @removed_files.each do |file|
91
+ unlink_file(file)
92
+ end
93
+
94
+ files_to_write.each do |file|
95
+ write_file(file[:path], file[:content])
96
+ end
97
+ end
98
+
99
+ # Remove a file from disk.
100
+ #
101
+ # Like FileUtils.rm_f, this method will not fail if the file does not
102
+ # exist. Unlike FileUtils.rm_f, this method will not blindly swallow all
103
+ # exceptions.
104
+ #
105
+ # @param path [String] The path to the file to be removed.
106
+ #
107
+ # @raise [PDK::CLI::ExitWithError] if the file could not be removed.
108
+ def unlink_file(path)
109
+ if File.file?(path)
110
+ PDK.logger.debug(_("unlinking '%{path}'") % { path: path })
111
+ FileUtils.rm(path)
112
+ else
113
+ PDK.logger.debug(_("'%{path}': already gone") % { path: path })
114
+ end
115
+ rescue => e
116
+ raise PDK::CLI::ExitWithError, _("Unable to remove '%{path}': %{message}") % {
117
+ path: path,
118
+ message: e.message,
119
+ }
120
+ end
121
+
122
+ private
123
+
124
+ # Loop through all the files to be modified and cache of unified diff of
125
+ # the changes to be made to each file.
126
+ #
127
+ # @raise [PDK::CLI::ExitWithError] if a file being modified isn't
128
+ # readable.
129
+ def calculate_diffs
130
+ @modified_files.each do |file|
131
+ next if @diff_cache.key?(file[:path])
132
+
133
+ unless File.readable?(file[:path])
134
+ raise PDK::CLI::ExitWithError, _("Unable to open '%{path}' for reading") % { path: file[:path] }
135
+ end
136
+
137
+ old_content = File.read(file[:path])
138
+ file_diff = unified_diff(file[:path], old_content, file[:content])
139
+ @diff_cache[file[:path]] = file_diff
140
+ end
141
+ end
142
+
143
+ # Write or overwrite a file with the specified content.
144
+ #
145
+ # @param path [String] The path to be written to.
146
+ # @param content [String] The data to be written to the file.
147
+ #
148
+ # @raise [PDK::CLI::ExitWithError] if the file is not writeable.
149
+ def write_file(path, content)
150
+ FileUtils.mkdir_p(File.dirname(path))
151
+ PDK.logger.debug(_("writing '%{path}'") % { path: path })
152
+ PDK::Util::Filesystem.write_file(path, content)
153
+ rescue Errno::EACCES
154
+ raise PDK::CLI::ExitWithError, _("You do not have permission to write to '%{path}'") % { path: path }
155
+ end
156
+
157
+ # Generate a unified diff of the changes to be made to a file.
158
+ #
159
+ # @param path [String] The path to the file being diffed (only used to
160
+ # generate the diff header).
161
+ # @param old_content [String] The current content of the file.
162
+ # @param new_content [String] The new content of the file if the pending
163
+ # change is applied.
164
+ # @param lines_of_context [Integer] The maximum number of lines of
165
+ # context to include around the changed lines in the diff output
166
+ # (default: 3).
167
+ #
168
+ # @return [String] The unified diff of the pending changes to the file.
169
+ def unified_diff(path, old_content, new_content, lines_of_context = 3)
170
+ output = []
171
+
172
+ old_lines = old_content.split($INPUT_RECORD_SEPARATOR).map(&:chomp)
173
+ new_lines = new_content.split($INPUT_RECORD_SEPARATOR).map(&:chomp)
174
+
175
+ diffs = Diff::LCS.diff(old_lines, new_lines)
176
+
177
+ return nil if diffs.empty?
178
+
179
+ file_mtime = File.stat(path).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
180
+ now = Time.now.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
181
+
182
+ output << "--- #{path}\t#{file_mtime}"
183
+ output << "+++ #{path}.pdknew\t#{now}"
184
+
185
+ oldhunk = hunk = nil
186
+ file_length_difference = 0
187
+
188
+ diffs.each do |piece|
189
+ begin
190
+ hunk = Diff::LCS::Hunk.new(old_lines, new_lines, piece, lines_of_context, file_length_difference)
191
+ file_length_difference = hunk.file_length_difference
192
+
193
+ next unless oldhunk
194
+
195
+ # If the hunk overlaps with the oldhunk, merge them.
196
+ next if lines_of_context > 0 && hunk.merge(oldhunk)
197
+
198
+ output << oldhunk.diff(:unified)
199
+ ensure
200
+ oldhunk = hunk
201
+ end
202
+ end
203
+
204
+ output << oldhunk.diff(:unified)
205
+
206
+ output.join($INPUT_RECORD_SEPARATOR)
207
+ end
208
+ end
209
+ end
210
+ end