chef-dk 0.7.0 → 0.8.0

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