chef-dk 0.7.0 → 0.8.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/chef-dk/builtin_commands.rb +10 -0
  4. data/lib/chef-dk/command/base.rb +2 -2
  5. data/lib/chef-dk/command/clean_policy_cookbooks.rb +116 -0
  6. data/lib/chef-dk/command/clean_policy_revisions.rb +113 -0
  7. data/lib/chef-dk/command/delete_policy.rb +122 -0
  8. data/lib/chef-dk/command/delete_policy_group.rb +122 -0
  9. data/lib/chef-dk/command/export.rb +3 -3
  10. data/lib/chef-dk/command/generate.rb +8 -0
  11. data/lib/chef-dk/command/generator_commands/app.rb +1 -1
  12. data/lib/chef-dk/command/generator_commands/cookbook.rb +1 -1
  13. data/lib/chef-dk/command/generator_commands/policyfile.rb +1 -1
  14. data/lib/chef-dk/command/generator_commands/repo.rb +1 -1
  15. data/lib/chef-dk/command/install.rb +22 -5
  16. data/lib/chef-dk/command/provision.rb +0 -4
  17. data/lib/chef-dk/command/push.rb +1 -2
  18. data/lib/chef-dk/command/shell_init.rb +65 -6
  19. data/lib/chef-dk/command/show_policy.rb +1 -2
  20. data/lib/chef-dk/command/undelete.rb +155 -0
  21. data/lib/chef-dk/command/update.rb +5 -5
  22. data/lib/chef-dk/command/verify.rb +61 -17
  23. data/lib/chef-dk/completions/bash.sh.erb +5 -0
  24. data/lib/chef-dk/completions/chef.fish.erb +10 -0
  25. data/lib/chef-dk/completions/zsh.zsh.erb +21 -0
  26. data/lib/chef-dk/exceptions.rb +12 -0
  27. data/lib/chef-dk/helpers.rb +17 -0
  28. data/lib/chef-dk/policyfile/community_cookbook_source.rb +0 -3
  29. data/lib/chef-dk/policyfile/lister.rb +3 -1
  30. data/lib/chef-dk/policyfile/undo_record.rb +142 -0
  31. data/lib/chef-dk/policyfile/undo_stack.rb +130 -0
  32. data/lib/chef-dk/policyfile_lock.rb +30 -0
  33. data/lib/chef-dk/policyfile_services/clean_policies.rb +5 -4
  34. data/lib/chef-dk/policyfile_services/clean_policy_cookbooks.rb +125 -0
  35. data/lib/chef-dk/policyfile_services/rm_policy.rb +142 -0
  36. data/lib/chef-dk/policyfile_services/rm_policy_group.rb +86 -0
  37. data/lib/chef-dk/policyfile_services/show_policy.rb +1 -1
  38. data/lib/chef-dk/policyfile_services/undelete.rb +108 -0
  39. data/lib/chef-dk/service_exceptions.rb +11 -0
  40. data/lib/chef-dk/skeletons/code_generator/files/default/chefignore +6 -2
  41. data/lib/chef-dk/skeletons/code_generator/files/default/repo/README.md +1 -1
  42. data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/example/attributes/default.rb +1 -1
  43. data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/example/recipes/default.rb +1 -1
  44. data/lib/chef-dk/version.rb +1 -1
  45. data/lib/kitchen/provisioner/policyfile_zero.rb +4 -1
  46. data/spec/unit/command/base_spec.rb +26 -1
  47. data/spec/unit/command/clean_policy_cookbooks_spec.rb +181 -0
  48. data/spec/unit/command/clean_policy_revisions_spec.rb +181 -0
  49. data/spec/unit/command/delete_policy_group_spec.rb +207 -0
  50. data/spec/unit/command/delete_policy_spec.rb +207 -0
  51. data/spec/unit/command/generate_spec.rb +41 -1
  52. data/spec/unit/command/generator_commands/cookbook_spec.rb +1 -1
  53. data/spec/unit/command/generator_commands/policyfile_spec.rb +1 -1
  54. data/spec/unit/command/install_spec.rb +24 -0
  55. data/spec/unit/command/shell_init_spec.rb +176 -5
  56. data/spec/unit/command/undelete_spec.rb +246 -0
  57. data/spec/unit/helpers_spec.rb +24 -0
  58. data/spec/unit/policyfile/lister_spec.rb +16 -0
  59. data/spec/unit/policyfile/undo_record_spec.rb +260 -0
  60. data/spec/unit/policyfile/undo_stack_spec.rb +266 -0
  61. data/spec/unit/policyfile_lock_serialization_spec.rb +41 -0
  62. data/spec/unit/policyfile_services/clean_policy_cookbooks_spec.rb +275 -0
  63. data/spec/unit/policyfile_services/rm_policy_group_spec.rb +241 -0
  64. data/spec/unit/policyfile_services/rm_policy_spec.rb +266 -0
  65. data/spec/unit/policyfile_services/show_policy_spec.rb +52 -2
  66. data/spec/unit/policyfile_services/undelete_spec.rb +304 -0
  67. metadata +43 -91
@@ -37,8 +37,7 @@ NOTE: `chef update` does not yet support granular updates (e.g., just updating
37
37
  the `run_list` or a specific cookbook version). Support will be added in a
38
38
  future version.
39
39
 
40
- The Policyfile feature is incomplete and beta quality. See our detailed README
41
- for more information.
40
+ See our detailed README for more information:
42
41
 
43
42
  https://github.com/opscode/chef-dk/blob/master/POLICYFILE_README.md
44
43
 
@@ -74,7 +73,7 @@ BANNER
74
73
  end
75
74
 
76
75
  def run(params = [])
77
- apply_params!(params)
76
+ return 1 unless apply_params!(params)
78
77
  if update_attributes?
79
78
  attributes_updater.run
80
79
  else
@@ -116,10 +115,11 @@ BANNER
116
115
  def apply_params!(params)
117
116
  remaining_args = parse_options(params)
118
117
  if remaining_args.size > 1
119
- ui.err(banner)
120
- return 1
118
+ ui.err(opt_parser)
119
+ false
121
120
  else
122
121
  @policyfile_relative_path = remaining_args.first
122
+ true
123
123
  end
124
124
  end
125
125
 
@@ -127,11 +127,17 @@ module ChefDK
127
127
 
128
128
  c.smoke_test do
129
129
  # ------------
130
- # we want to avoid hard-coding driver names, but calling Gem::Specification produces a warning; this
131
- # seems to be the best way to silence it.
130
+ # we want to avoid hard-coding driver names, but calling Gem::Specification produces a warning;
131
+ # changing $VERBOSE seems to be the best way to silence it.
132
132
  verbose = $VERBOSE
133
133
  $VERBOSE = nil
134
134
 
135
+ # construct a hash of { driver_name => [version1, version2, ...]}
136
+ driver_versions = {}
137
+ Gem::Specification.all.map { |gs| [gs.name, gs.version] }.
138
+ select { |n| n[0] =~ /^chef-provisioning-/ }.
139
+ each { |gem, version| (driver_versions[gem] ||= []) << version }
140
+
135
141
  drivers = Gem::Specification.all.map { |gs| gs.name }.
136
142
  select { |n| n =~ /^chef-provisioning-/ }.
137
143
  uniq
@@ -141,6 +147,48 @@ module ChefDK
141
147
  # ------------
142
148
  failures = []
143
149
 
150
+ # ------------
151
+ # fail the verify if we have more than one version of chef-provisioning or any of its drivers.
152
+ def format_gem_failure(name, versions)
153
+ <<-EOS
154
+ #{name} has multiple versions installed:
155
+ #{versions.sort.map { |gv| " #{gv.to_s}" }.join("\n")}
156
+ EOS
157
+ end
158
+
159
+ failures << format_gem_failure("chef-provisioning", versions) if versions.size > 1
160
+
161
+ driver_versions.keys.sort.each do |driver_name|
162
+ v = driver_versions[driver_name]
163
+ failures << format_gem_failure(driver_name, v) if v.size > 1
164
+ end
165
+
166
+ if failures.size > 0
167
+ failures << <<-EOS
168
+
169
+ Some applications may need or prefer different versions of the chef-provisioning gem or its drivers, so
170
+ this multiple-version check can fail if a user has installed new versions of those libraries.
171
+ EOS
172
+ end
173
+
174
+ # ------------
175
+ # load the core gem and all of the drivers (ignoring versions).
176
+ require "chef/provisioning"
177
+ drivers.map { |d| "#{d.gsub('-', '/')}_driver" }.each do |driver_gem|
178
+ begin
179
+ begin
180
+ require driver_gem
181
+ rescue LoadError
182
+ # anomalously, chef-provisioning-fog does not have a fog_driver.rb. (9/2015)
183
+ require "#{driver_gem}/driver.rb"
184
+ end
185
+ rescue LoadError => ex
186
+ puts ex
187
+ end
188
+ end
189
+
190
+ # ------------
191
+ # look for version dependency conflicts.
144
192
  tmpdir do |cwd|
145
193
  versions.each do |provisioning_version|
146
194
  gemfile = "chef-provisioning-#{provisioning_version}-chefdk-test.gemfile"
@@ -154,15 +202,11 @@ module ChefDK
154
202
  result = sh("bundle install --local --quiet", cwd: cwd, env: {"BUNDLE_GEMFILE" => gemfile })
155
203
 
156
204
  if result.exitstatus != 0
157
- failures << result
205
+ failures << result.stdout
158
206
  end
159
-
160
207
  end # end provisioning versions.
161
208
 
162
- if failures.size > 0
163
- failures.each { |fail| puts fail.stdout }
164
- puts "\nDriver list (no version restrictions):\n #{drivers.join("\n ")}"
165
- end
209
+ failures.each { |fail| puts fail }
166
210
 
167
211
  # dubious on Windows.
168
212
  # this is weird, but we seem to require a Mixlib::ShellOut as the return value. suggestions
@@ -238,13 +282,13 @@ end
238
282
 
239
283
  c.smoke_test do
240
284
 
241
- if File.directory?("/usr/bin")
242
- sh!("/usr/bin/berks -v")
285
+ if File.directory?(usr_bin_prefix)
286
+ sh!("#{usr_bin_path("berks")} -v")
243
287
 
244
- sh!("/usr/bin/chef -v")
288
+ sh!("#{usr_bin_path("chef")} -v")
245
289
 
246
- sh!("/usr/bin/chef-client -v")
247
- sh!("/usr/bin/chef-solo -v")
290
+ sh!("#{usr_bin_path("chef-client")} -v")
291
+ sh!("#{usr_bin_path("chef-solo")} -v")
248
292
 
249
293
  # In `knife`, `knife -v` follows a different code path that skips
250
294
  # command/plugin loading; `knife -h` loads commands and plugins, but
@@ -253,17 +297,17 @@ end
253
297
  # exits 0, which runs most of the code.
254
298
  #
255
299
  # See also: https://github.com/opscode/chef-dk/issues/227
256
- sh!("/usr/bin/knife exec -E true")
300
+ sh!("#{usr_bin_path("knife")} exec -E true")
257
301
 
258
302
  tmpdir do |dir|
259
303
  # Kitchen tries to create a .kitchen dir even when just running
260
304
  # `kitchen -v`:
261
- sh!("/usr/bin/kitchen -v", cwd: dir)
305
+ sh!("#{usr_bin_path("kitchen")} -v", cwd: dir)
262
306
  end
263
307
 
264
- sh!("/usr/bin/ohai -v")
308
+ sh!("#{usr_bin_path("ohai")} -v")
265
309
 
266
- sh!("/usr/bin/foodcritic -V")
310
+ sh!("#{usr_bin_path("foodcritic")} -V")
267
311
  end
268
312
 
269
313
  # Test blocks are expected to return a Mixlib::ShellOut compatible
@@ -0,0 +1,5 @@
1
+ _chef_comp() {
2
+ local COMMANDS="<%= commands.keys.join(' ')-%>"
3
+ COMPREPLY=($(compgen -W "$COMMANDS" -- ${COMP_WORDS[COMP_CWORD]} ))
4
+ }
5
+ complete -F _chef_comp chef
@@ -0,0 +1,10 @@
1
+ # Fish Shell command-line completions for ChefDK
2
+
3
+ function __fish_chef_no_command --description 'Test if chef has yet to be given the main command'
4
+ set -l cmd (commandline -opc)
5
+ test (count $cmd) -eq 1
6
+ end
7
+
8
+ <% commands.each do |command, desc| -%>
9
+ complete -c chef -f -n '__fish_chef_no_command' -a <%= command %> -d "<%= desc %>"
10
+ <% end -%>
@@ -0,0 +1,21 @@
1
+ function _chef() {
2
+
3
+ local -a _1st_arguments
4
+ _1st_arguments=(
5
+ <% commands.each do |command, desc| -%>
6
+ '<%=command%>:<%=desc%>'
7
+ <% end -%>
8
+ )
9
+
10
+ _arguments \
11
+ '(-v --version)'{-v,--version}'[version information]' \
12
+ '*:: :->subcmds' && return 0
13
+
14
+ if (( CURRENT == 1 )); then
15
+ _describe -t commands "chef subcommand" _1st_arguments
16
+ return
17
+ fi
18
+ }
19
+
20
+ compdef _chef chef
21
+
@@ -68,6 +68,18 @@ module ChefDK
68
68
  class InvalidPolicyfileFilename < StandardError
69
69
  end
70
70
 
71
+ class InvalidUndoRecord < StandardError
72
+ end
73
+
74
+ class CantUndo < StandardError
75
+ end
76
+
77
+ class UndoRecordNotFound < StandardError
78
+ end
79
+
80
+ class MultipleErrors < StandardError
81
+ end
82
+
71
83
  class BUG < RuntimeError
72
84
  end
73
85
 
@@ -86,6 +86,18 @@ module ChefDK
86
86
  end
87
87
  end
88
88
 
89
+ # Returns the directory that contains our main symlinks.
90
+ # On Mac we place all of our symlinks under /usr/local/bin on other
91
+ # platforms they are under /usr/bin
92
+ def usr_bin_prefix
93
+ @usr_bin_prefix ||= os_x? ? "/usr/local/bin" : "/usr/bin"
94
+ end
95
+
96
+ # Returns the full path to the given command under usr_bin_prefix
97
+ def usr_bin_path(command)
98
+ File.join(usr_bin_prefix, command)
99
+ end
100
+
89
101
  private
90
102
 
91
103
  def omnibus_expand_path(*paths)
@@ -138,5 +150,10 @@ module ChefDK
138
150
  self.instance_variable_set(ivar, nil)
139
151
  end
140
152
  end
153
+
154
+ # Returns true if we are on Mac OS X. Otherwise false
155
+ def os_x?
156
+ !!(RUBY_PLATFORM =~ /darwin/)
157
+ end
141
158
  end
142
159
  end
@@ -20,9 +20,6 @@ require 'chef-dk/cookbook_omnifetch'
20
20
  require 'chef-dk/exceptions'
21
21
  require 'chef/http/simple'
22
22
 
23
- # TODO: fix hardcoding
24
- Chef::Config.ssl_verify_mode = :verify_peer
25
-
26
23
  module ChefDK
27
24
  module Policyfile
28
25
 
@@ -67,6 +67,8 @@ module ChefDK
67
67
 
68
68
  class PolicyGroupRevIDMap
69
69
 
70
+ include Enumerable
71
+
70
72
  attr_reader :policy_name
71
73
  attr_reader :revision_ids_by_group
72
74
 
@@ -215,7 +217,7 @@ module ChefDK
215
217
 
216
218
  def set_policies_by_group_from_api(policy_group_data)
217
219
  @policies_by_group = policy_group_data.inject({}) do |map, (policy_group, policy_info)|
218
- map[policy_group] = policy_info["policies"].inject({}) do |rev_map, (policy_name, rev_info)|
220
+ map[policy_group] = (policy_info["policies"] || []).inject({}) do |rev_map, (policy_name, rev_info)|
219
221
  rev_map[policy_name] = rev_info["revision_id"]; rev_map
220
222
  end
221
223
 
@@ -0,0 +1,142 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2015 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'chef-dk/exceptions'
19
+
20
+ module ChefDK
21
+ module Policyfile
22
+
23
+ class UndoRecord
24
+
25
+ PolicyGroupRestoreData = Struct.new(:policy_name, :policy_group, :data) do
26
+
27
+ def load(data)
28
+ self.policy_name = data["policy_name"]
29
+ self.policy_group = data["policy_group"]
30
+ self.data = data["data"]
31
+ self
32
+ end
33
+
34
+ def for_serialization
35
+ {
36
+ "policy_name" => policy_name,
37
+ "policy_group" => policy_group,
38
+ "data" => data
39
+ }
40
+ end
41
+
42
+ end
43
+
44
+ attr_reader :policy_groups
45
+
46
+ attr_reader :policy_revisions
47
+
48
+ attr_accessor :description
49
+
50
+ def initialize
51
+ reset!
52
+ end
53
+
54
+ def ==(other)
55
+ other.kind_of?(UndoRecord) &&
56
+ other.policy_groups == policy_groups &&
57
+ other.policy_revisions == policy_revisions
58
+ end
59
+
60
+ def add_policy_group(name)
61
+ @policy_groups << name
62
+ end
63
+
64
+ def add_policy_revision(policy_name, policy_group, data)
65
+ @policy_revisions << PolicyGroupRestoreData.new(policy_name, policy_group, data)
66
+ end
67
+
68
+ def load(data)
69
+ reset!
70
+
71
+ unless data.kind_of?(Hash)
72
+ raise InvalidUndoRecord, "Undo data is incorrectly formatted. Must be a Hash, got '#{data}'."
73
+ end
74
+ missing_fields = %w[ format_version description backup_data ].select { |key| !data.key?(key) }
75
+ unless missing_fields.empty?
76
+ raise InvalidUndoRecord, "Undo data is missing mandatory field(s) #{missing_fields.join(', ')}. Undo data: '#{data}'"
77
+ end
78
+
79
+ @description = data["description"]
80
+
81
+ policy_data = data["backup_data"]
82
+ unless policy_data.kind_of?(Hash)
83
+ raise InvalidUndoRecord, "'backup_data' in the undo record is incorrectly formatted. Must be a Hash, got '#{policy_data}'"
84
+ end
85
+ missing_policy_data_fields = %w[ policy_groups policy_revisions ].select { |key| !policy_data.key?(key) }
86
+ unless missing_policy_data_fields.empty?
87
+ raise InvalidUndoRecord,
88
+ "'backup_data' in the undo record is missing mandatory field(s) #{missing_policy_data_fields.join(', ')}. Backup data: #{policy_data}"
89
+ end
90
+
91
+ policy_groups = policy_data["policy_groups"]
92
+
93
+ unless policy_groups.kind_of?(Array)
94
+ raise InvalidUndoRecord,
95
+ "'policy_groups' data in the undo record is incorrectly formatted. Must be an Array, got '#{policy_groups}'"
96
+ end
97
+
98
+ @policy_groups = policy_groups
99
+
100
+ policy_revisions = policy_data["policy_revisions"]
101
+ unless policy_revisions.kind_of?(Array)
102
+ raise InvalidUndoRecord,
103
+ "'policy_revisions' data in the undo record is incorrectly formatted. Must be an Array, got '#{policy_revisions}'"
104
+ end
105
+
106
+ policy_revisions.each do |revision|
107
+ unless revision.kind_of?(Hash)
108
+ raise InvalidUndoRecord,
109
+ "Invalid item in 'policy_revisions' in the undo record. Must be a Hash, got '#{revision}'"
110
+ end
111
+
112
+ @policy_revisions << PolicyGroupRestoreData.new.load(revision)
113
+ end
114
+
115
+ self
116
+ end
117
+
118
+ def for_serialization
119
+ {
120
+ "format_version" => 1,
121
+ "description" => description,
122
+ "backup_data" => {
123
+ "policy_groups" => policy_groups,
124
+ "policy_revisions" => policy_revisions.map(&:for_serialization)
125
+ }
126
+ }
127
+ end
128
+
129
+ private
130
+
131
+ def reset!
132
+ @description = ""
133
+ @policy_groups = []
134
+ @policy_revisions = []
135
+ end
136
+
137
+ end
138
+ end
139
+ end
140
+
141
+
142
+
@@ -0,0 +1,130 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2015 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'fileutils'
19
+
20
+ require 'ffi_yajl'
21
+
22
+ require 'chef-dk/helpers'
23
+ require 'chef-dk/policyfile/undo_record'
24
+
25
+ module ChefDK
26
+ module Policyfile
27
+
28
+ class UndoStack
29
+
30
+ MAX_SIZE = 10
31
+
32
+ include Helpers
33
+
34
+ def undo_dir
35
+ File.join(Helpers.chefdk_home, "undo")
36
+ end
37
+
38
+ def size
39
+ undo_record_files.size
40
+ end
41
+
42
+ def empty?
43
+ size == 0
44
+ end
45
+
46
+ def has_id?(id)
47
+ File.exist?(undo_file_for(id))
48
+ end
49
+
50
+ def each_with_id
51
+ undo_record_files.each do |filename|
52
+ yield File.basename(filename), load_undo_record(filename)
53
+ end
54
+ end
55
+
56
+ def undo_records
57
+ undo_record_files.map { |f| load_undo_record(f) }
58
+ end
59
+
60
+ def push(undo_record)
61
+ ensure_undo_dir_exists
62
+
63
+ record_id = Time.new.utc.strftime("%Y%m%d%H%M%S")
64
+ path = File.join(undo_dir, record_id)
65
+
66
+ with_file(path) do |f|
67
+ f.print(FFI_Yajl::Encoder.encode(undo_record.for_serialization, pretty: true))
68
+ end
69
+
70
+ records_to_delete = undo_record_files.size - MAX_SIZE
71
+ if records_to_delete > 0
72
+ undo_record_files.take(records_to_delete).each do |file|
73
+ File.unlink(file)
74
+ end
75
+ end
76
+
77
+ self
78
+ end
79
+
80
+ def pop
81
+ file_to_pop = undo_record_files.last
82
+ if file_to_pop.nil?
83
+ raise CantUndo, "No undo records exist in #{undo_dir}"
84
+ end
85
+
86
+ record = load_undo_record(file_to_pop)
87
+ # if this hits an exception, we skip unlink
88
+ yield record if block_given?
89
+ File.unlink(file_to_pop)
90
+ record
91
+ end
92
+
93
+ def delete(id)
94
+ undo_file = undo_file_for(id)
95
+ unless File.exist?(undo_file)
96
+ raise UndoRecordNotFound, "No undo record for id '#{id}' exists at #{undo_file}"
97
+ end
98
+
99
+ record = load_undo_record(undo_file)
100
+ yield record if block_given?
101
+ File.unlink(undo_file)
102
+ record
103
+ end
104
+
105
+ private
106
+
107
+ def undo_file_for(id)
108
+ File.join(undo_dir, id)
109
+ end
110
+
111
+ def load_undo_record(file)
112
+ data = FFI_Yajl::Parser.parse(IO.read(file))
113
+ UndoRecord.new.load(data)
114
+ end
115
+
116
+ def undo_record_files
117
+ Dir[File.join(undo_dir, '*')].sort
118
+ end
119
+
120
+ def ensure_undo_dir_exists
121
+ return false if File.directory?(undo_dir)
122
+
123
+
124
+ FileUtils.mkdir_p(undo_dir)
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+