pdk 1.2.1 → 1.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +300 -21
- data/lib/pdk/cli.rb +3 -2
- data/lib/pdk/cli/bundle.rb +0 -2
- data/lib/pdk/cli/convert.rb +25 -0
- data/lib/pdk/cli/exec.rb +4 -34
- data/lib/pdk/cli/exec_group.rb +2 -2
- data/lib/pdk/cli/module.rb +2 -3
- data/lib/pdk/cli/module/generate.rb +9 -4
- data/lib/pdk/cli/new/class.rb +1 -1
- data/lib/pdk/cli/new/module.rb +12 -9
- data/lib/pdk/cli/test/unit.rb +16 -7
- data/lib/pdk/cli/util.rb +47 -4
- data/lib/pdk/generate.rb +4 -4
- data/lib/pdk/{generators → generate}/defined_type.rb +1 -1
- data/lib/pdk/{generators → generate}/module.rb +47 -58
- data/lib/pdk/{generators → generate}/puppet_class.rb +1 -1
- data/lib/pdk/{generators → generate}/puppet_object.rb +1 -1
- data/lib/pdk/{generators → generate}/task.rb +1 -1
- data/lib/pdk/module/convert.rb +163 -0
- data/lib/pdk/module/metadata.rb +11 -3
- data/lib/pdk/module/templatedir.rb +81 -42
- data/lib/pdk/module/update_manager.rb +203 -0
- data/lib/pdk/tests/unit.rb +7 -6
- data/lib/pdk/util.rb +42 -1
- data/lib/pdk/util/bundler.rb +2 -2
- data/lib/pdk/util/git.rb +36 -0
- data/lib/pdk/util/version.rb +2 -1
- data/lib/pdk/validate.rb +3 -3
- data/lib/pdk/{validators → validate}/base_validator.rb +0 -0
- data/lib/pdk/{validators → validate}/metadata/metadata_json_lint.rb +1 -1
- data/lib/pdk/{validators → validate}/metadata/metadata_syntax.rb +2 -2
- data/lib/pdk/{validators → validate}/metadata/task_metadata_lint.rb +3 -3
- data/lib/pdk/{validators → validate}/metadata_validator.rb +4 -4
- data/lib/pdk/{validators → validate}/puppet/puppet_lint.rb +1 -1
- data/lib/pdk/{validators → validate}/puppet/puppet_syntax.rb +1 -1
- data/lib/pdk/{validators → validate}/puppet_validator.rb +3 -3
- data/lib/pdk/{validators → validate}/ruby/rubocop.rb +2 -2
- data/lib/pdk/{validators → validate}/ruby_validator.rb +2 -2
- data/lib/pdk/version.rb +2 -1
- metadata +36 -18
@@ -223,7 +223,7 @@ module PDK
|
|
223
223
|
@templates ||= [
|
224
224
|
{ type: 'CLI', url: @options[:'template-url'], allow_fallback: false },
|
225
225
|
{ type: 'metadata', url: module_metadata.data['template-url'], allow_fallback: true },
|
226
|
-
{ type: 'default', url: PDK::
|
226
|
+
{ type: 'default', url: PDK::Util.default_template_url, allow_fallback: false },
|
227
227
|
]
|
228
228
|
end
|
229
229
|
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'pdk/generate/module'
|
2
|
+
require 'pdk/module/update_manager'
|
3
|
+
require 'pdk/util'
|
4
|
+
require 'pdk/report'
|
5
|
+
|
6
|
+
module PDK
|
7
|
+
module Module
|
8
|
+
class Convert
|
9
|
+
def self.invoke(options)
|
10
|
+
update_manager = PDK::Module::UpdateManager.new
|
11
|
+
template_url = options.fetch(:'template-url', PDK::Util.default_template_url)
|
12
|
+
|
13
|
+
PDK::Module::TemplateDir.new(template_url, nil, false) do |templates|
|
14
|
+
new_metadata = update_metadata('metadata.json', templates.metadata, options)
|
15
|
+
|
16
|
+
if options[:noop] && new_metadata.nil?
|
17
|
+
update_manager.add_file('metadata.json', '')
|
18
|
+
elsif File.file?('metadata.json')
|
19
|
+
update_manager.modify_file('metadata.json', new_metadata)
|
20
|
+
else
|
21
|
+
update_manager.add_file('metadata.json', new_metadata)
|
22
|
+
end
|
23
|
+
|
24
|
+
templates.render do |file_path, file_content|
|
25
|
+
if File.exist? file_path
|
26
|
+
update_manager.modify_file(file_path, file_content)
|
27
|
+
else
|
28
|
+
update_manager.add_file(file_path, file_content)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
unless update_manager.changes?
|
34
|
+
PDK::Report.default_target.puts(_('No changes required.'))
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
# Print the summary to the default target of reports
|
39
|
+
summary = get_summary(update_manager)
|
40
|
+
print_summary(summary)
|
41
|
+
|
42
|
+
# Generates the full convert report
|
43
|
+
full_report(update_manager) unless update_manager.changes[:modified].empty?
|
44
|
+
|
45
|
+
return if options[:noop]
|
46
|
+
|
47
|
+
unless options[:force]
|
48
|
+
PDK.logger.info _(
|
49
|
+
'Module conversion is a potentially destructive action. ' \
|
50
|
+
'Ensure that you have committed your module to a version control ' \
|
51
|
+
'system or have a backup, and review the changes above before continuing.',
|
52
|
+
)
|
53
|
+
continue = PDK::CLI::Util.prompt_for_yes(_('Do you want to continue and make these changes to your module?'))
|
54
|
+
return unless continue
|
55
|
+
end
|
56
|
+
|
57
|
+
# Mark these files for removal after generating the report as these
|
58
|
+
# changes are not something that the user needs to review.
|
59
|
+
if update_manager.changed?('Gemfile')
|
60
|
+
update_manager.remove_file('Gemfile.lock')
|
61
|
+
update_manager.remove_file(File.join('.bundle', 'config'))
|
62
|
+
end
|
63
|
+
|
64
|
+
update_manager.sync_changes!
|
65
|
+
|
66
|
+
PDK::Util::Bundler.ensure_bundle! if update_manager.changed?('Gemfile')
|
67
|
+
|
68
|
+
print_result(summary)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.update_metadata(metadata_path, template_metadata, options = {})
|
72
|
+
if File.file?(metadata_path)
|
73
|
+
if File.readable?(metadata_path)
|
74
|
+
begin
|
75
|
+
metadata = PDK::Module::Metadata.from_file(metadata_path)
|
76
|
+
new_values = PDK::Module::Metadata::DEFAULTS.reject { |key, _| metadata.data.key?(key) }
|
77
|
+
metadata.update!(new_values)
|
78
|
+
rescue ArgumentError
|
79
|
+
metadata = PDK::Generate::Module.prepare_metadata(options) unless options[:noop] # rubocop:disable Metrics/BlockNesting
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise PDK::CLI::ExitWithError, _('Unable to convert module metadata; %{path} exists but it is not readable.') % {
|
83
|
+
path: metadata_path,
|
84
|
+
}
|
85
|
+
end
|
86
|
+
elsif File.exist?(metadata_path)
|
87
|
+
raise PDK::CLI::ExitWithError, _('Unable to convert module metadata; %{path} exists but it is not a file.') % {
|
88
|
+
path: metadata_path,
|
89
|
+
}
|
90
|
+
else
|
91
|
+
return nil if options[:noop]
|
92
|
+
|
93
|
+
project_dir = File.basename(Dir.pwd)
|
94
|
+
options[:module_name] = project_dir.split('-', 2).compact[-1]
|
95
|
+
options[:prompt] = false
|
96
|
+
options[:'skip-interview'] = true if options[:force]
|
97
|
+
|
98
|
+
metadata = PDK::Generate::Module.prepare_metadata(options)
|
99
|
+
end
|
100
|
+
|
101
|
+
metadata.update!(template_metadata)
|
102
|
+
metadata.to_json
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.get_summary(update_manager)
|
106
|
+
summary = {}
|
107
|
+
update_manager.changes.each do |category, update_category|
|
108
|
+
updated_files = if update_category.respond_to?(:keys)
|
109
|
+
update_category.keys
|
110
|
+
else
|
111
|
+
update_category.map { |file| file[:path] }
|
112
|
+
end
|
113
|
+
|
114
|
+
summary[category] = updated_files
|
115
|
+
end
|
116
|
+
|
117
|
+
summary
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.print_summary(summary)
|
121
|
+
footer = false
|
122
|
+
|
123
|
+
summary.keys.each do |category|
|
124
|
+
next if summary[category].empty?
|
125
|
+
|
126
|
+
PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner("Files to be #{category}", 40) })
|
127
|
+
PDK::Report.default_target.puts(summary[category])
|
128
|
+
footer = true
|
129
|
+
end
|
130
|
+
|
131
|
+
PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner('', 40) }) if footer
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.print_result(summary)
|
135
|
+
PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner('Convert completed', 40) })
|
136
|
+
summary_to_print = summary.map { |k, v| "#{v.length} files #{k}" unless v.empty? }.compact
|
137
|
+
PDK::Report.default_target.puts(_("\n%{summary}\n\n") % { summary: "#{summary_to_print.join(', ')}." })
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.full_report(update_manager)
|
141
|
+
File.open('convert_report.txt', 'w') do |f|
|
142
|
+
f.write("/* Convert Report generated by PDK at #{Time.now} */")
|
143
|
+
update_manager.changes[:modified].each do |_, diff|
|
144
|
+
f.write("\n\n\n" + diff)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
PDK::Report.default_target.puts(_("\nYou can find a report of differences in convert_report.txt.\n\n"))
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.generate_banner(text, width = 80)
|
151
|
+
padding = width - text.length
|
152
|
+
banner = ''
|
153
|
+
padding_char = '-'
|
154
|
+
|
155
|
+
(padding / 2.0).ceil.times { banner << padding_char }
|
156
|
+
banner << text
|
157
|
+
(padding / 2.0).floor.times { banner << padding_char }
|
158
|
+
|
159
|
+
banner
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/pdk/module/metadata.rb
CHANGED
@@ -7,14 +7,14 @@ module PDK
|
|
7
7
|
|
8
8
|
DEFAULTS = {
|
9
9
|
'name' => nil,
|
10
|
-
'version' =>
|
10
|
+
'version' => '0.1.0',
|
11
11
|
'author' => nil,
|
12
12
|
'summary' => '',
|
13
13
|
'license' => 'Apache-2.0',
|
14
14
|
'source' => '',
|
15
15
|
'project_page' => nil,
|
16
16
|
'issues_url' => nil,
|
17
|
-
'dependencies' =>
|
17
|
+
'dependencies' => [],
|
18
18
|
'data_provider' => nil,
|
19
19
|
'operatingsystem_support' => [
|
20
20
|
{
|
@@ -34,7 +34,9 @@ module PDK
|
|
34
34
|
'operatingsystemrelease' => ['2012 R2'],
|
35
35
|
},
|
36
36
|
],
|
37
|
-
'requirements' =>
|
37
|
+
'requirements' => [
|
38
|
+
{ 'name' => 'puppet', 'version_requirement' => '>= 4.7.0 < 6.0.0' },
|
39
|
+
],
|
38
40
|
}.freeze
|
39
41
|
|
40
42
|
def initialize(params = {})
|
@@ -71,6 +73,12 @@ module PDK
|
|
71
73
|
JSON.pretty_generate(@data.dup.delete_if { |_key, value| value.nil? })
|
72
74
|
end
|
73
75
|
|
76
|
+
def write!(path)
|
77
|
+
File.open(path, 'w') do |file|
|
78
|
+
file.puts to_json
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
74
82
|
private
|
75
83
|
|
76
84
|
# Do basic validation and parsing of the name parameter.
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'yaml'
|
2
|
+
require 'deep_merge'
|
2
3
|
require 'pdk/util'
|
3
|
-
require 'pdk/
|
4
|
+
require 'pdk/util/git'
|
4
5
|
require 'pdk/cli/errors'
|
5
6
|
require 'pdk/template_file'
|
6
7
|
|
@@ -21,7 +22,7 @@ module PDK
|
|
21
22
|
# the template available on disk.
|
22
23
|
#
|
23
24
|
# @example Using a git repository as a template
|
24
|
-
# PDK::Module::TemplateDir.new('https://github.com/puppetlabs/pdk-
|
25
|
+
# PDK::Module::TemplateDir.new('https://github.com/puppetlabs/pdk-templates') do |t|
|
25
26
|
# t.render do |filename, content|
|
26
27
|
# File.open(filename, 'w') do |file|
|
27
28
|
# file.write(content)
|
@@ -36,7 +37,7 @@ module PDK
|
|
36
37
|
# @raise [ArgumentError] (see #validate_module_template!)
|
37
38
|
#
|
38
39
|
# @api public
|
39
|
-
def initialize(path_or_url, module_metadata = {})
|
40
|
+
def initialize(path_or_url, module_metadata = {}, init = false)
|
40
41
|
if File.directory?(path_or_url)
|
41
42
|
@path = path_or_url
|
42
43
|
else
|
@@ -46,10 +47,19 @@ module PDK
|
|
46
47
|
# @todo When switching this over to using rugged, cache the cloned
|
47
48
|
# template repo in `%AppData%` or `$XDG_CACHE_DIR` and update before
|
48
49
|
# use.
|
49
|
-
temp_dir = PDK::Util.make_tmpdir_name('pdk-
|
50
|
+
temp_dir = PDK::Util.make_tmpdir_name('pdk-templates')
|
51
|
+
git_ref = PDK::Util.default_template_ref
|
50
52
|
|
51
|
-
clone_result = PDK::
|
52
|
-
|
53
|
+
clone_result = PDK::Util::Git.git('clone', path_or_url, temp_dir)
|
54
|
+
|
55
|
+
if clone_result[:exit_code].zero?
|
56
|
+
reset_result = PDK::Util::Git.git('-C', temp_dir, 'reset', '--hard', git_ref)
|
57
|
+
unless reset_result[:exit_code].zero?
|
58
|
+
PDK.logger.error reset_result[:stdout]
|
59
|
+
PDK.logger.error reset_result[:stderr]
|
60
|
+
raise PDK::CLI::FatalError, _("Unable to set git repository '%{repo}' to ref:'%{ref}'.") % { repo: temp_dir, ref: git_ref }
|
61
|
+
end
|
62
|
+
else
|
53
63
|
PDK.logger.error clone_result[:stdout]
|
54
64
|
PDK.logger.error clone_result[:stderr]
|
55
65
|
raise PDK::CLI::FatalError, _("Unable to clone git repository '%{repo}' to '%{dest}'.") % { repo: path_or_url, dest: temp_dir }
|
@@ -59,7 +69,11 @@ module PDK
|
|
59
69
|
@repo = path_or_url
|
60
70
|
end
|
61
71
|
|
72
|
+
@init = init
|
62
73
|
@moduleroot_dir = File.join(@path, 'moduleroot')
|
74
|
+
@moduleroot_init = File.join(@path, 'moduleroot_init')
|
75
|
+
@dirs = [@moduleroot_dir]
|
76
|
+
@dirs << @moduleroot_init if @init
|
63
77
|
@object_dir = File.join(@path, 'object_templates')
|
64
78
|
validate_module_template!
|
65
79
|
|
@@ -85,7 +99,7 @@ module PDK
|
|
85
99
|
def metadata
|
86
100
|
return {} unless @repo
|
87
101
|
|
88
|
-
ref_result = PDK::
|
102
|
+
ref_result = PDK::Util::Git.git('--git-dir', File.join(@path, '.git'), 'describe', '--all', '--long', '--always')
|
89
103
|
if ref_result[:exit_code].zero?
|
90
104
|
{ 'template-url' => @repo, 'template-ref' => ref_result[:stdout].strip }
|
91
105
|
else
|
@@ -107,12 +121,12 @@ module PDK
|
|
107
121
|
#
|
108
122
|
# @api public
|
109
123
|
def render
|
110
|
-
files_in_template.each do |template_file|
|
124
|
+
PDK::Module::TemplateDir.files_in_template(@dirs).each do |template_file, template_loc|
|
125
|
+
template_file = template_file.to_s
|
111
126
|
PDK.logger.debug(_("Rendering '%{template}'...") % { template: template_file })
|
112
127
|
dest_path = template_file.sub(%r{\.erb\Z}, '')
|
113
|
-
|
114
128
|
begin
|
115
|
-
dest_content = PDK::TemplateFile.new(File.join(
|
129
|
+
dest_content = PDK::TemplateFile.new(File.join(template_loc, template_file), configs: config_for(dest_path)).render
|
116
130
|
rescue => e
|
117
131
|
error_msg = _(
|
118
132
|
"Failed to render template '%{template}'\n" \
|
@@ -120,7 +134,6 @@ module PDK
|
|
120
134
|
) % { template: template_file, exception: e.class, message: e.message }
|
121
135
|
raise PDK::CLI::FatalError, error_msg
|
122
136
|
end
|
123
|
-
|
124
137
|
yield dest_path, dest_content
|
125
138
|
end
|
126
139
|
end
|
@@ -165,8 +178,6 @@ module PDK
|
|
165
178
|
config_for(nil)
|
166
179
|
end
|
167
180
|
|
168
|
-
private
|
169
|
-
|
170
181
|
# Validate the content of the template directory.
|
171
182
|
#
|
172
183
|
# @raise [ArgumentError] If the specified path is not a directory.
|
@@ -181,36 +192,46 @@ module PDK
|
|
181
192
|
raise ArgumentError, _("The specified template '%{path}' is not a directory.") % { path: @path }
|
182
193
|
end
|
183
194
|
|
184
|
-
unless File.directory?(@moduleroot_dir)
|
195
|
+
unless File.directory?(@moduleroot_dir)
|
185
196
|
raise ArgumentError, _("The template at '%{path}' does not contain a 'moduleroot/' directory.") % { path: @path }
|
186
197
|
end
|
198
|
+
|
199
|
+
unless File.directory?(@moduleroot_init) # rubocop:disable Style/GuardClause
|
200
|
+
# rubocop:disable Metrics/LineLength
|
201
|
+
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 }
|
202
|
+
# rubocop:enable Metrics/LineLength
|
203
|
+
end
|
187
204
|
end
|
188
205
|
|
189
206
|
# Get a list of template files in the template directory.
|
190
207
|
#
|
191
|
-
# @return [
|
192
|
-
#
|
208
|
+
# @return [Hash{String=>String}] A hash of key file names and
|
209
|
+
# value locations.
|
193
210
|
#
|
194
|
-
# @api
|
195
|
-
def files_in_template
|
196
|
-
|
197
|
-
|
211
|
+
# @api public
|
212
|
+
def self.files_in_template(dirs)
|
213
|
+
temp_paths = []
|
214
|
+
dirlocs = []
|
215
|
+
dirs.each do |dir|
|
216
|
+
raise ArgumentError, _("The directory '%{dir}' doesn't exist") unless Dir.exist?(dir)
|
217
|
+
temp_paths += Dir.glob(File.join(dir, '**', '*'), File::FNM_DOTMATCH).select do |template_path|
|
198
218
|
File.file?(template_path) && !File.symlink?(template_path)
|
219
|
+
dirlocs << dir
|
199
220
|
end
|
200
|
-
|
201
|
-
|
202
|
-
template_path.sub(%r{\A#{Regexp.escape(@moduleroot_dir)}#{Regexp.escape(File::SEPARATOR)}}, '')
|
221
|
+
temp_paths.map do |template_path|
|
222
|
+
template_path.sub!(%r{\A#{Regexp.escape(dir)}#{Regexp.escape(File::SEPARATOR)}}, '')
|
203
223
|
end
|
204
224
|
end
|
225
|
+
template_paths = Hash[temp_paths.zip dirlocs]
|
226
|
+
template_paths.delete('.')
|
227
|
+
template_paths.delete('spec')
|
228
|
+
template_paths.delete('spec/.')
|
229
|
+
template_paths
|
205
230
|
end
|
206
231
|
|
207
232
|
# Generate a hash of data to be used when rendering the specified
|
208
233
|
# template.
|
209
234
|
#
|
210
|
-
# Read `config_defaults.yml` from the root of the template directory (if
|
211
|
-
# it exists) build a hash of values by merging the value of the `:global`
|
212
|
-
# key with the value of the key that matches `dest_path`.
|
213
|
-
#
|
214
235
|
# @param dest_path [String] The destination path of the file that the
|
215
236
|
# data is for, relative to the root of the module.
|
216
237
|
#
|
@@ -218,26 +239,44 @@ module PDK
|
|
218
239
|
# `@configs` instance variable.
|
219
240
|
#
|
220
241
|
# @api private
|
221
|
-
def config_for(dest_path)
|
242
|
+
def config_for(dest_path, sync_config_path = nil)
|
243
|
+
module_root = PDK::Util.module_root
|
244
|
+
sync_config_path ||= File.join(module_root, '.sync.yml') unless module_root.nil?
|
245
|
+
config_path = File.join(@path, 'config_defaults.yml')
|
246
|
+
|
222
247
|
if @config.nil?
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
@config = YAML.safe_load(File.read(config_path), [], [], true)
|
228
|
-
rescue StandardError => e
|
229
|
-
PDK.logger.warn(_("'%{file}' is not a valid YAML file: %{message}") % { file: config_path, message: e.message })
|
230
|
-
@config = {}
|
231
|
-
end
|
232
|
-
else
|
233
|
-
@config = {}
|
234
|
-
end
|
248
|
+
conf_defaults = read_config(config_path)
|
249
|
+
sync_config = read_config(sync_config_path) unless sync_config_path.nil?
|
250
|
+
@config = conf_defaults
|
251
|
+
@config.deep_merge!(sync_config) unless sync_config.nil?
|
235
252
|
end
|
236
|
-
|
237
253
|
file_config = @config.fetch(:global, {})
|
238
254
|
file_config['module_metadata'] = @module_metadata
|
239
255
|
file_config.merge!(@config.fetch(dest_path, {})) unless dest_path.nil?
|
240
|
-
file_config
|
256
|
+
file_config.merge!(@config)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Generates a hash of data from a given yaml file location.
|
260
|
+
#
|
261
|
+
# @param loc [String] The path of the yaml config file.
|
262
|
+
#
|
263
|
+
# @warn If the specified path is not a valid yaml file. Returns an empty Hash
|
264
|
+
# if so.
|
265
|
+
#
|
266
|
+
# @return [Hash] The data that has been read in from the given yaml file.
|
267
|
+
#
|
268
|
+
# @api private
|
269
|
+
def read_config(loc)
|
270
|
+
if File.file?(loc) && File.readable?(loc)
|
271
|
+
begin
|
272
|
+
YAML.safe_load(File.read(loc), [], [], true)
|
273
|
+
rescue StandardError => e
|
274
|
+
PDK.logger.warn(_("'%{file}' is not a valid YAML file: %{message}") % { file: config_path, message: e.message })
|
275
|
+
{}
|
276
|
+
end
|
277
|
+
else
|
278
|
+
{}
|
279
|
+
end
|
241
280
|
end
|
242
281
|
end
|
243
282
|
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'diff/lcs'
|
3
|
+
require 'diff/lcs/hunk'
|
4
|
+
require 'English'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module PDK
|
8
|
+
module Module
|
9
|
+
class UpdateManager
|
10
|
+
# Initialises a blank UpdateManager object, which is used to store and
|
11
|
+
# process file additions/removals/modifications.
|
12
|
+
def initialize
|
13
|
+
@modified_files = Set.new
|
14
|
+
@added_files = Set.new
|
15
|
+
@removed_files = Set.new
|
16
|
+
@diff_cache = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Store a pending modification to an existing file.
|
20
|
+
#
|
21
|
+
# @param path [String] The path to the file to be modified.
|
22
|
+
# @param content [String] The new content of the file.
|
23
|
+
def modify_file(path, content)
|
24
|
+
@modified_files << { path: path, content: content }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Store a pending file addition.
|
28
|
+
#
|
29
|
+
# @param path [String] The path where the file will be created.
|
30
|
+
# @param content [String] The content of the new file.
|
31
|
+
def add_file(path, content)
|
32
|
+
@added_files << { path: path, content: content }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Store a pending file removal.
|
36
|
+
#
|
37
|
+
# @param path [String] The path to the file to be removed.
|
38
|
+
def remove_file(path)
|
39
|
+
@removed_files << path
|
40
|
+
end
|
41
|
+
|
42
|
+
# Generate a summary of the changes that will be applied to the module.
|
43
|
+
#
|
44
|
+
# @raise (see #calculate_diffs)
|
45
|
+
# @return [Hash{Symbol => Set,Hash}] the summary of the pending changes.
|
46
|
+
def changes
|
47
|
+
calculate_diffs
|
48
|
+
|
49
|
+
{
|
50
|
+
added: @added_files,
|
51
|
+
removed: @removed_files,
|
52
|
+
modified: @diff_cache.reject { |_, value| value.nil? },
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check if there are any pending changes to apply to the module.
|
57
|
+
#
|
58
|
+
# @raise (see #changes)
|
59
|
+
# @return [Boolean] true if there are changes to apply to the module.
|
60
|
+
def changes?
|
61
|
+
!changes[:added].empty? ||
|
62
|
+
!changes[:removed].empty? ||
|
63
|
+
changes[:modified].any? { |_, value| !value.nil? }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if the update manager will change the specified file upon sync.
|
67
|
+
#
|
68
|
+
# @param path [String] The path to the file.
|
69
|
+
#
|
70
|
+
# @raise (see #changes)
|
71
|
+
# @return [Boolean] true if the file will be changed.
|
72
|
+
def changed?(path)
|
73
|
+
changes[:added].any? { |add| add[:path] == path } ||
|
74
|
+
changes[:removed].include?(path) ||
|
75
|
+
changes[:modified].keys.include?(path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Apply any pending changes stored in the UpdateManager to the module.
|
79
|
+
#
|
80
|
+
# @raise (see #calculate_diffs)
|
81
|
+
# @raise (see #write_file)
|
82
|
+
# @raise (see #unlink_file)
|
83
|
+
def sync_changes!
|
84
|
+
calculate_diffs
|
85
|
+
|
86
|
+
files_to_write = @added_files
|
87
|
+
files_to_write += @modified_files.reject { |file| @diff_cache[file[:path]].nil? }
|
88
|
+
|
89
|
+
files_to_write.each do |file|
|
90
|
+
write_file(file[:path], file[:content])
|
91
|
+
end
|
92
|
+
|
93
|
+
@removed_files.each do |file|
|
94
|
+
unlink_file(file)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Loop through all the files to be modified and cache of unified diff of
|
101
|
+
# the changes to be made to each file.
|
102
|
+
#
|
103
|
+
# @raise [PDK::CLI::ExitWithError] if a file being modified isn't
|
104
|
+
# readable.
|
105
|
+
def calculate_diffs
|
106
|
+
@modified_files.each do |file|
|
107
|
+
next if @diff_cache.key?(file[:path])
|
108
|
+
|
109
|
+
unless File.readable?(file[:path])
|
110
|
+
raise PDK::CLI::ExitWithError, _("Unable to open '%{path}' for reading") % { path: file[:path] }
|
111
|
+
end
|
112
|
+
|
113
|
+
old_content = File.read(file[:path])
|
114
|
+
file_diff = unified_diff(file[:path], old_content, file[:content])
|
115
|
+
@diff_cache[file[:path]] = file_diff
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Write or overwrite a file with the specified content.
|
120
|
+
#
|
121
|
+
# @param path [String] The path to be written to.
|
122
|
+
# @param content [String] The data to be written to the file.
|
123
|
+
#
|
124
|
+
# @raise [PDK::CLI::ExitWithError] if the file is not writeable.
|
125
|
+
def write_file(path, content)
|
126
|
+
FileUtils.mkdir_p(File.dirname(path))
|
127
|
+
File.open(path, 'w') { |f| f.puts content }
|
128
|
+
rescue Errno::EACCES
|
129
|
+
raise PDK::CLI::ExitWithError, _("You do not have permission to write to '%{path}'") % { path: path }
|
130
|
+
end
|
131
|
+
|
132
|
+
# Remove a file from disk.
|
133
|
+
#
|
134
|
+
# Like FileUtils.rm_f, this method will not fail if the file does not
|
135
|
+
# exist. Unlink FileUtils.rm_f, this method will not blindly swallow all
|
136
|
+
# exceptions.
|
137
|
+
#
|
138
|
+
# @param path [String] The path to the file to be removed.
|
139
|
+
#
|
140
|
+
# @raise [PDK::CLI::ExitWithError] if the file could not be removed.
|
141
|
+
def unlink_file(path)
|
142
|
+
FileUtils.rm(path) if File.file?(path)
|
143
|
+
rescue => e
|
144
|
+
raise PDK::CLI::ExitWithError, _("Unable to remove '%{path}': %{message}") % {
|
145
|
+
path: path,
|
146
|
+
message: e.message,
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
# Generate a unified diff of the changes to be made to a file.
|
151
|
+
#
|
152
|
+
# @param path [String] The path to the file being diffed (only used to
|
153
|
+
# generate the diff header).
|
154
|
+
# @param old_content [String] The current content of the file.
|
155
|
+
# @param new_content [String] The new content of the file if the pending
|
156
|
+
# change is applied.
|
157
|
+
# @param lines_of_context [Integer] The maximum number of lines of
|
158
|
+
# context to include around the changed lines in the diff output
|
159
|
+
# (default: 3).
|
160
|
+
#
|
161
|
+
# @return [String] The unified diff of the pending changes to the file.
|
162
|
+
def unified_diff(path, old_content, new_content, lines_of_context = 3)
|
163
|
+
output = []
|
164
|
+
|
165
|
+
old_lines = old_content.split($INPUT_RECORD_SEPARATOR).map(&:chomp)
|
166
|
+
new_lines = new_content.split($INPUT_RECORD_SEPARATOR).map(&:chomp)
|
167
|
+
|
168
|
+
diffs = Diff::LCS.diff(old_lines, new_lines)
|
169
|
+
|
170
|
+
return nil if diffs.empty?
|
171
|
+
|
172
|
+
file_mtime = File.stat(path).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
|
173
|
+
now = Time.now.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
|
174
|
+
|
175
|
+
output << "--- #{path}\t#{file_mtime}"
|
176
|
+
output << "+++ #{path}.pdknew\t#{now}"
|
177
|
+
|
178
|
+
oldhunk = hunk = nil
|
179
|
+
file_length_difference = 0
|
180
|
+
|
181
|
+
diffs.each do |piece|
|
182
|
+
begin
|
183
|
+
hunk = Diff::LCS::Hunk.new(old_lines, new_lines, piece, lines_of_context, file_length_difference)
|
184
|
+
file_length_difference = hunk.file_length_difference
|
185
|
+
|
186
|
+
next unless oldhunk
|
187
|
+
|
188
|
+
# If the hunk overlaps with the oldhunk, merge them.
|
189
|
+
next if lines_of_context > 0 && hunk.merge(oldhunk)
|
190
|
+
|
191
|
+
output << oldhunk.diff(:unified)
|
192
|
+
ensure
|
193
|
+
oldhunk = hunk
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
output << oldhunk.diff(:unified)
|
198
|
+
|
199
|
+
output.join($INPUT_RECORD_SEPARATOR)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|