pdk-akerl 1.8.0.1

Sign up to get free protection for your applications and to get access to all the features.
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