bahuvrihi-tap 0.11.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/bin/rap +2 -3
  2. data/bin/tap +1 -1
  3. data/cmd/console.rb +2 -2
  4. data/cmd/manifest.rb +2 -2
  5. data/cmd/run.rb +7 -9
  6. data/cmd/server.rb +5 -5
  7. data/doc/Class Reference +17 -20
  8. data/doc/Tutorial +5 -7
  9. data/lib/tap.rb +2 -0
  10. data/lib/tap/app.rb +21 -31
  11. data/lib/tap/constants.rb +2 -2
  12. data/lib/tap/declarations.rb +85 -97
  13. data/lib/tap/declarations/declaration_task.rb +58 -0
  14. data/lib/tap/declarations/description.rb +24 -0
  15. data/lib/tap/env.rb +20 -16
  16. data/lib/tap/exe.rb +2 -2
  17. data/lib/tap/file_task.rb +224 -410
  18. data/lib/tap/generator/arguments.rb +9 -0
  19. data/lib/tap/generator/base.rb +105 -28
  20. data/lib/tap/generator/destroy.rb +29 -12
  21. data/lib/tap/generator/generate.rb +55 -39
  22. data/lib/tap/generator/generators/command/templates/command.erb +3 -3
  23. data/lib/tap/generator/generators/config/config_generator.rb +34 -3
  24. data/lib/tap/generator/generators/root/root_generator.rb +6 -9
  25. data/lib/tap/generator/generators/root/templates/Rakefile +4 -4
  26. data/lib/tap/generator/generators/task/templates/test.erb +1 -1
  27. data/lib/tap/root.rb +211 -156
  28. data/lib/tap/support/aggregator.rb +6 -9
  29. data/lib/tap/support/audit.rb +278 -357
  30. data/lib/tap/support/constant_manifest.rb +24 -21
  31. data/lib/tap/support/dependency.rb +1 -1
  32. data/lib/tap/support/executable.rb +26 -48
  33. data/lib/tap/support/join.rb +44 -19
  34. data/lib/tap/support/joins/sync_merge.rb +3 -5
  35. data/lib/tap/support/parser.rb +1 -1
  36. data/lib/tap/task.rb +195 -150
  37. data/lib/tap/tasks/dump.rb +2 -2
  38. data/lib/tap/test/extensions.rb +11 -13
  39. data/lib/tap/test/file_test.rb +71 -129
  40. data/lib/tap/test/file_test_class.rb +4 -1
  41. data/lib/tap/test/tap_test.rb +26 -154
  42. metadata +15 -22
  43. data/lib/tap/patches/optparse/summarize.rb +0 -62
  44. data/lib/tap/support/assignments.rb +0 -173
  45. data/lib/tap/support/class_configuration.rb +0 -182
  46. data/lib/tap/support/configurable.rb +0 -113
  47. data/lib/tap/support/configurable_class.rb +0 -271
  48. data/lib/tap/support/configuration.rb +0 -170
  49. data/lib/tap/support/instance_configuration.rb +0 -173
  50. data/lib/tap/support/lazydoc.rb +0 -386
  51. data/lib/tap/support/lazydoc/attributes.rb +0 -48
  52. data/lib/tap/support/lazydoc/comment.rb +0 -503
  53. data/lib/tap/support/lazydoc/config.rb +0 -17
  54. data/lib/tap/support/lazydoc/definition.rb +0 -36
  55. data/lib/tap/support/lazydoc/document.rb +0 -152
  56. data/lib/tap/support/lazydoc/method.rb +0 -24
  57. data/lib/tap/support/tdoc.rb +0 -409
  58. data/lib/tap/support/tdoc/tdoc_html_generator.rb +0 -38
  59. data/lib/tap/support/tdoc/tdoc_html_template.rb +0 -42
  60. data/lib/tap/support/validation.rb +0 -479
@@ -0,0 +1,58 @@
1
+ autoload(:OpenStruct, 'ostruct')
2
+
3
+ module Tap
4
+ module Declarations
5
+ # Dependency tasks are a singleton version of tasks. Dependency tasks only
6
+ # have one instance (DeclarationTask.instance) and the instance is
7
+ # registered as a dependency, so it will only execute once.
8
+ class DeclarationTask < Tap::Task
9
+ class << self
10
+ attr_writer :blocks
11
+
12
+ def blocks
13
+ @blocks ||= []
14
+ end
15
+
16
+ attr_writer :arg_names
17
+
18
+ def arg_names
19
+ @arg_names ||= []
20
+ end
21
+
22
+ # Initializes instance and registers it as a dependency.
23
+ def new(*args)
24
+ @instance ||= super
25
+ @instance.app.dependencies.register(@instance)
26
+ @instance
27
+ end
28
+
29
+ def args
30
+ args = Lazydoc::Arguments.new
31
+ arg_names.each {|name| args.arguments << name.to_s }
32
+ args
33
+ end
34
+ end
35
+
36
+ def process(*inputs)
37
+ # collect inputs to make a rakish-args object
38
+ args = {}
39
+ self.class.arg_names.each do |arg_name|
40
+ break if inputs.empty?
41
+ args[arg_name] = inputs.shift
42
+ end
43
+ args = OpenStruct.new(args)
44
+
45
+ # execute each block assciated with this task
46
+ self.class.blocks.each do |task_block|
47
+ case task_block.arity
48
+ when 0 then task_block.call()
49
+ when 1 then task_block.call(self)
50
+ else task_block.call(self, args)
51
+ end
52
+ end
53
+
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ module Tap
2
+ module Declarations
3
+
4
+ # A special type of Lazydoc::Comment designed to handle the comment syntax
5
+ # for task declarations.
6
+ class Description < Lazydoc::Comment
7
+ attr_accessor :desc
8
+
9
+ def prepend(line)
10
+ if line =~ /::desc\s+(.*?)\s*$/
11
+ self.desc = $1
12
+ false
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ def to_s
19
+ resolve
20
+ desc.to_s
21
+ end
22
+ end
23
+ end
24
+ end
@@ -8,7 +8,7 @@ module Tap
8
8
  # whenever these configs are reset.
9
9
  class Env
10
10
  include Enumerable
11
- include Support::Configurable
11
+ include Configurable
12
12
  include Support::Minimap
13
13
 
14
14
  class << self
@@ -38,16 +38,17 @@ module Tap
38
38
  # # File.expand_path("./path/to/config.yml") => e1,
39
39
  # # File.expand_path("./path/to/dir/#{Tap::Env::DEFAULT_CONFIG_FILE}") => e2 }
40
40
  #
41
- # The Env is initialized using configurations read from the env config file using
42
- # load_config, and a Root initialized to the config file directory. An instance
43
- # will be initialized regardless of whether the config file or directory exists.
41
+ # The Env is initialized using configurations read from the env config
42
+ # file, and a Root initialized to the config file directory. An instance
43
+ # will be initialized regardless of whether the config file or directory
44
+ # exists.
44
45
  def instantiate(path_or_root, default_config={}, logger=nil, &block)
45
46
  path = path_or_root.kind_of?(Root) ? path_or_root.root : path_or_root
46
47
  path = pathify(path)
47
48
 
48
49
  begin
49
50
  root = path_or_root.kind_of?(Root) ? path_or_root : Root.new(File.dirname(path))
50
- config = default_config.merge(load_config(path))
51
+ config = default_config.merge(load_file(path))
51
52
 
52
53
  # note the assignment of env to instances MUST occur before
53
54
  # reconfigure to prevent infinite looping
@@ -102,6 +103,12 @@ module Tap
102
103
  instance_variable_set(instance_variable, [*input].compact.collect {|path| root[path]}.uniq)
103
104
  end
104
105
  end
106
+
107
+ # helper to load path as YAML. load_file returns a hash if the path
108
+ # loads to nil or false (as happens for empty files)
109
+ def load_file(path) # :nodoc:
110
+ Root.trivial?(path) ? {} : (YAML.load_file(path) || {})
111
+ end
105
112
  end
106
113
 
107
114
  @@instance = nil
@@ -375,9 +382,8 @@ module Tap
375
382
 
376
383
  # freeze array configs like load_paths
377
384
  config.each_pair do |key, value|
378
- case value
379
- when Array then value.freeze
380
- end
385
+ next unless value.kind_of?(Array)
386
+ value.freeze
381
387
  end
382
388
 
383
389
  # activate nested envs
@@ -418,18 +424,16 @@ module Tap
418
424
  $LOAD_PATH.delete(path)
419
425
  end
420
426
 
421
- # unfreeze array configs by duplicating
422
- self.config.class_config.each_pair do |key, value|
423
- value = send(key)
424
- case value
425
- when Array then instance_variable_set("@#{key}", value.dup)
426
- end
427
- end
428
-
429
427
  @active = false
430
428
  @manifests.clear
431
429
  @@instance = nil if @@instance == self
432
430
 
431
+ # unfreeze array configs by duplicating
432
+ config.each_pair do |key, value|
433
+ next unless value.kind_of?(Array)
434
+ instance_variable_set("@#{key}", value.dup)
435
+ end
436
+
433
437
  # dectivate nested envs
434
438
  envs.reverse_each do |env|
435
439
  env.deactivate
@@ -8,7 +8,7 @@ module Tap
8
8
  class << self
9
9
  def instantiate(path=Dir.pwd, logger=Tap::App::DEFAULT_LOGGER, &block)
10
10
  app = Tap::App.instance = Tap::App.new({:root => path}, logger)
11
- exe = super(app, load_config(GLOBAL_CONFIG_FILE), app.logger, &block)
11
+ exe = super(app, load_file(GLOBAL_CONFIG_FILE), app.logger, &block)
12
12
 
13
13
  # add all gems if no gems are specified (Note this is VERY SLOW ~ 1/3 the overhead for tap)
14
14
  if !File.exists?(Tap::Env::DEFAULT_CONFIG_FILE)
@@ -68,7 +68,7 @@ module Tap
68
68
  load path # run the command, if it exists
69
69
  else
70
70
  puts "Unknown command: '#{command}'"
71
- puts "Type 'tap help' for usage information."
71
+ puts "Type 'tap --help' for usage information."
72
72
  end
73
73
  end
74
74
  end
@@ -3,116 +3,53 @@ autoload(:FileUtils, "fileutils")
3
3
 
4
4
  module Tap
5
5
 
6
- # FileTask provides methods for creating/modifying files such that you can
7
- # rollback changes if an error occurs.
6
+ # FileTask is a base class for tasks that work with a file system. FileTask
7
+ # tracks changes it makes so they may be rolled back to their original state.
8
+ # Rollback automatically occurs on an execute error.
8
9
  #
9
- # === Creating Files/Rolling Back Changes
10
- #
11
- # FileTask tracks which files to roll back using the added_files array
12
- # and the backed_up_files hash. On an execute error, all added files are
13
- # removed and then all backed up files (backed_up_files.keys) are restored
14
- # using the corresponding backup files (backed_up_files.values).
15
- #
16
- # For consistency, all filepaths in added_files and backed_up_files should
17
- # be expanded using File.expand_path. The easiest way to ensure files are
18
- # properly set up for rollback is to use prepare before working with files
19
- # and to create directories with mkdir.
20
- #
21
- # # this file will be backed up and restored
22
- # File.open("file.txt", "w") {|f| f << "original content"}
10
+ # File.open("file.txt", "w") {|file| file << "original content"}
23
11
  #
24
- # t = FileTask.intern do |task|
25
- # task.mkdir("some/dir") # marked for rollback
26
- # task.prepare("file.txt", "path/to/file.txt") # marked for rollback
27
- #
28
- # File.open("file.txt", "w") {|f| f << "new content"}
29
- # File.touch("path/to/file.txt")
12
+ # t = FileTask.intern do |task, raise_error|
13
+ # task.mkdir_p("some/dir") # marked for rollback
14
+ # task.prepare("file.txt") do |file| # marked for rollback
15
+ # file << "new content"
16
+ # end
30
17
  #
31
18
  # # raise an error to start rollback
32
- # raise "error!"
19
+ # raise "error!" if raise_error
33
20
  # end
34
21
  #
35
22
  # begin
36
- # File.exists?("some/dir") # => false
37
- # File.exists?("path/to/file.txt") # => false
38
- # t.execute(nil)
23
+ # t.execute(true)
39
24
  # rescue
40
25
  # $!.message # => "error!"
41
26
  # File.exists?("some/dir") # => false
42
- # File.exists?("path/to/file.txt") # => false
43
27
  # File.read("file.txt") # => "original content"
44
28
  # end
45
29
  #
30
+ # t.execute(false)
31
+ # File.exists?("some/dir") # => true
32
+ # File.read("file.txt") # => "new content"
33
+ #
46
34
  class FileTask < Task
47
35
  include Tap::Support::ShellUtils
48
36
 
49
- # A hash of backup [source, target] pairs, such that the
50
- # backed-up files are backed_up_files.keys and the actual
51
- # backup files are backed_up_files.values. All filepaths
52
- # in backed_up_files should be expanded.
53
- attr_reader :backed_up_files
54
-
55
- # An array of files added during task execution.
56
- attr_reader :added_files
57
-
58
- # The backup directory, defaults to the class backup_dir
37
+ # The backup directory
59
38
  config_attr :backup_dir, 'backup' # the backup directory
60
39
 
61
- # A timestamp format used to mark backup files, defaults
62
- # to the class backup_timestamp
63
- config :timestamp, "%Y%m%d_%H%M%S" # the backup timestamp format
64
-
65
- # A flag indicating whether or not to rollback changes on
66
- # error, defaults to the class rollback_on_error
40
+ # A flag indicating whether or track changes
41
+ # for rollback on execution error
67
42
  config :rollback_on_error, true, &c.switch # rollback changes on error
68
43
 
69
44
  def initialize(config={}, name=nil, app=App.instance)
70
45
  super
71
-
72
- @backed_up_files = {}
73
- @added_files = []
46
+ @actions = []
74
47
  end
75
48
 
49
+ # Initializes a copy that will rollback independent of self.
76
50
  def initialize_copy(orig)
77
51
  super
78
- @backed_up_files = {}
79
- @added_files = []
80
- end
81
-
82
- # A batch File.open method. If a block is given, each file in the list will be
83
- # opened the open files passed to the block. Files are automatically closed when
84
- # the block returns. If no block is given, the open files are returned.
85
- #
86
- # t = FileTask.new
87
- # t.open(["one.txt", "two.txt"], "w") do |one, two|
88
- # one << "one"
89
- # two << "two"
90
- # end
91
- #
92
- # File.read("one.txt") # => "one"
93
- # File.read("two.txt") # => "two"
94
- #
95
- # Note that open normally takes and passes a list (ie an Array). If you provide
96
- # a single argument, it will be translated into an Array, and passed AS AN ARRAY
97
- # to the block.
98
- #
99
- # t.open("file.txt", "w") do |array|
100
- # array.first << "content"
101
- # end
102
- #
103
- # File.read("file.txt") # => "content"
104
- #
105
- def open(list, mode="rb")
106
- open_files = []
107
- begin
108
- [list].flatten.map {|path| path.to_str }.each do |filepath|
109
- open_files << File.open(filepath, mode)
110
- end
111
-
112
- block_given? ? yield(open_files) : open_files
113
- ensure
114
- open_files.each {|file| file.close } if block_given?
115
- end
52
+ @actions = []
116
53
  end
117
54
 
118
55
  # Returns the path, exchanging the extension with extname.
@@ -130,13 +67,9 @@ module Tap
130
67
  # Compare to basename.
131
68
  def basepath(path, extname=false)
132
69
  case extname
133
- when false, nil
134
- path.chomp(File.extname(path))
135
- when true
136
- path
137
- else
138
- extname = extname[1, extname.length-1] if extname[0] == ?.
139
- "#{path.chomp(File.extname(path))}.#{extname}"
70
+ when false, nil then path.chomp(File.extname(path))
71
+ when true then path
72
+ else Root.exchange(path, extname)
140
73
  end
141
74
  end
142
75
 
@@ -167,27 +100,26 @@ module Tap
167
100
  app.filepath(dir, name, *paths)
168
101
  end
169
102
 
170
- # Makes a backup filepath relative to backup_dir by using self.name, the
171
- # basename of filepath plus a timestamp.
103
+ # Makes a backup filepath relative to backup_dir by using name, the
104
+ # basename of filepath, and an index.
172
105
  #
173
- # t = FileTask.new({:timestamp => "%Y%m%d"}, 'name')
174
- # t.app['backup', true] = "/backup"
175
- # time = Time.utc(2008,8,8)
176
- #
177
- # t.backup_filepath("path/to/file.txt", time) # => "/backup/name/file_20080808.txt"
106
+ # t = FileTask.new({:backup_dir => "/backup"}, "name")
107
+ # t.backup_filepath("path/to/file.txt", time) # => "/backup/name/file.0.txt"
178
108
  #
179
- def backup_filepath(filepath, time=Time.now)
180
- extname = File.extname(filepath)
181
- backup_path = "#{File.basename(filepath).chomp(extname)}_#{time.strftime(timestamp)}#{extname}"
182
- filepath(backup_dir, backup_path)
109
+ def backup_filepath(path)
110
+ extname = File.extname(path)
111
+ backup_path = filepath(backup_dir, File.basename(path).chomp(extname))
112
+ next_indexed_path(backup_path, 0, extname)
183
113
  end
184
114
 
185
- # Returns true if all of the targets are up to date relative to all of the listed
186
- # sources AND date_reference. Single values or arrays can be provided for both
187
- # targets and sources.
115
+ # Returns true if all of the targets are up to date relative to all of the
116
+ # listed sources. Single values or arrays can be provided for both targets
117
+ # and sources.
118
+ #
119
+ # Returns false (ie 'not up to date') if app.force is true.
188
120
  #
189
- #---
190
- # Returns false (ie 'not up to date') if +force?+ is true.
121
+ #--
122
+ # TODO: add check vs date reference (ex config_file date)
191
123
  def uptodate?(targets, sources=[])
192
124
  if app.force
193
125
  log_basename(:force, *targets)
@@ -196,9 +128,6 @@ module Tap
196
128
  targets = [targets] unless targets.kind_of?(Array)
197
129
  sources = [sources] unless sources.kind_of?(Array)
198
130
 
199
- # should be able to specify this somehow, externally set
200
- # sources << config_file unless config_file == nil
201
-
202
131
  targets.each do |target|
203
132
  return false unless FileUtils.uptodate?(target, sources)
204
133
  end
@@ -206,367 +135,252 @@ module Tap
206
135
  end
207
136
  end
208
137
 
209
- # Makes a backup of each file in list to backup_filepath(file) and registers
210
- # the files so that they can be restored using restore. If backup_using_copy
211
- # is true, the files will be copied to backup_filepath, otherwise the file is
212
- # moved to backup_filepath. Raises an error if the file is already listed
213
- # in backed_up_files.
138
+ # Makes a backup of path to backup_filepath(path) and returns the backup path.
139
+ # If backup_using_copy is true, the backup is a copy of path, otherwise the
140
+ # file or directory at path is moved to the backup path. Raises an error if
141
+ # the backup file already exists.
214
142
  #
215
- # Returns a list of the backup_filepaths.
143
+ # Backups are restored on rollback.
216
144
  #
217
145
  # file = "file.txt"
218
146
  # File.open(file, "w") {|f| f << "file content"}
219
147
  #
220
148
  # t = FileTask.new
221
- # backed_up_file = t.backup(file).first
149
+ # backup_file = t.backup(file)
222
150
  #
223
151
  # File.exists?(file) # => false
224
- # File.exists?(backed_up_file) # => true
225
- # File.read(backed_up_file) # => "file content"
152
+ # File.exists?(backup_file) # => true
153
+ # File.read(backup_file) # => "file content"
226
154
  #
227
155
  # File.open(file, "w") {|f| f << "new content"}
228
- # t.restore(file)
156
+ # t.rollback
229
157
  #
230
158
  # File.exists?(file) # => true
231
- # File.exists?(backed_up_file) # => false
159
+ # File.exists?(backup_file ) # => false
232
160
  # File.read(file) # => "file content"
233
161
  #
234
- def backup(list, backup_using_copy=false)
235
- fu_list(list).collect do |filepath|
236
- next unless File.exists?(filepath)
237
-
238
- filepath = File.expand_path(filepath)
239
- if backed_up_files.include?(filepath)
240
- raise "Backup for #{filepath} already exists."
241
- end
162
+ def backup(path, backup_using_copy=false)
163
+ return nil unless File.exists?(path)
242
164
 
243
- target = File.expand_path(backup_filepath(filepath))
244
- dir = File.dirname(target)
245
- mkdir(dir)
246
-
247
- if backup_using_copy
248
- log :cp, "#{filepath} to #{target}", Logger::DEBUG
249
- FileUtils.cp(filepath, target)
250
- else
251
- log :mv, "#{filepath} to #{target}", Logger::DEBUG
252
- FileUtils.mv(filepath, target)
253
- end
254
-
255
- # track the target for restores
256
- backed_up_files[filepath] = target
257
- target
165
+ source = File.expand_path(path)
166
+ target = backup_filepath(source)
167
+ raise "backup file already exists: #{target}" if File.exists?(target)
168
+
169
+ mkdir_p File.dirname(target)
170
+
171
+ log :backup, "#{source} to #{target}", Logger::DEBUG
172
+ if backup_using_copy
173
+ FileUtils.cp(source, target)
174
+ else
175
+ FileUtils.mv(source, target)
258
176
  end
177
+
178
+ actions << [:backup, source, target]
179
+ target
259
180
  end
260
181
 
261
- # Restores each file in the input list using the backup file from
262
- # backed_up_files. The backup directory is removed if it is empty.
263
- #
264
- # Returns a list of the restored files.
265
- #
266
- # file = "file.txt"
267
- # File.open(file, "w") {|f| f << "file content"}
268
- #
269
- # t = FileTask.new
270
- # backed_up_file = t.backup(file).first
271
- #
272
- # File.exists?(file) # => true
273
- # File.exists?(backed_up_file) # => true
274
- # File.read(backed_up_file) # => "file content"
275
- #
276
- # File.open(file, "w") {|f| f << "new content"}
277
- # t.restore(file)
278
- #
279
- # File.exists?(file) # => true
280
- # File.exists?(backed_up_file) # => false
281
- # File.read(file) # => "file content"
282
- #
283
- def restore(list)
284
- fu_list(list).collect do |filepath|
285
- filepath = File.expand_path(filepath)
286
- next unless backed_up_files.has_key?(filepath)
287
-
288
- target = backed_up_files.delete(filepath)
289
-
290
- dir = File.dirname(filepath)
291
- mkdir(dir)
182
+ # Creates a directory and all its parent directories. Directories created
183
+ # by mkdir_p removed on rollback.
184
+ def mkdir_p(dir)
185
+ dir = File.expand_path(dir)
292
186
 
293
- log :restore, "#{target} to #{filepath}", Logger::DEBUG
294
- FileUtils.mv(target, filepath, :force => true)
295
-
296
- dir = File.dirname(target)
297
- rmdir(dir)
187
+ dirs = []
188
+ while !File.exists?(dir)
189
+ dirs.unshift(dir)
190
+ dir = File.dirname(dir)
191
+ end
298
192
 
299
- filepath
300
- end.compact
193
+ dirs.each {|dir| mkdir(dir) }
301
194
  end
302
195
 
303
- # Creates the directories in list if they do not exist and adds
304
- # them to added_files so they can be removed using rmdir. Creating
305
- # directories in this way causes them to be rolled back upon an
306
- # execution error.
307
- #
308
- # Returns the made directories.
309
- #
310
- # t = FileTask.new do |task, inputs|
311
- # File.exists?("path") # => false
312
- #
313
- # task.mkdir("path/to/dir") # will be rolled back
314
- # File.exists?("path/to/dir") # => true
315
- #
316
- # FileUtils.mkdir("path/to/another") # will not be rolled back
317
- # File.exists?("path/to/another") # => true
318
- #
319
- # raise "error!"
320
- # end
321
- #
322
- # begin
323
- # t.execute(nil)
324
- # rescue
325
- # $!.message # => "error!"
326
- # File.exists?("path/to/dir") # => false
327
- # File.exists?("path/to/another") # => true
328
- # end
329
- #
330
- def mkdir(list)
331
- fu_list(list).each do |dir|
332
- dir = File.expand_path(dir)
333
-
334
- make_paths = []
335
- while !File.exists?(dir)
336
- make_paths << dir
337
- dir = File.dirname(dir)
338
- end
196
+ # Creates a directory. Directories created by mkdir removed on rollback.
197
+ def mkdir(dir)
198
+ dir = File.expand_path(dir)
339
199
 
340
- make_paths.reverse_each do |path|
341
- log :mkdir, path, Logger::DEBUG
342
- FileUtils.mkdir(path)
343
- added_files << path
344
- end
200
+ unless File.exists?(dir)
201
+ log :mkdir, dir, Logger::DEBUG
202
+ FileUtils.mkdir(dir)
203
+ actions << [:make, dir]
345
204
  end
346
205
  end
347
206
 
348
- # Removes each directory in the input list, provided the directory is in
349
- # added_files and the directory is empty. When checking if the directory
350
- # is empty, rmdir checks for regular files and hidden files. Removed
351
- # directories are removed from added_files.
352
- #
353
- # Returns a list of the removed directories.
354
- #
355
- # t = FileTask.new
356
- # File.exists?("path") # => false
357
- # FileUtils.mkdir("path") # will not be removed
358
- #
359
- # t.mkdir("path/to/dir")
360
- # File.exists?("path/to/dir") # => true
361
- #
362
- # t.rmdir("path/to/dir")
363
- # File.exists?("path") # => true
364
- # File.exists?("path/to") # => false
365
- def rmdir(list)
366
- removed = []
367
- fu_list(list).each do |dir|
368
- dir = File.expand_path(dir)
207
+ # Prepares the path by backing up any existing file and ensuring that
208
+ # the parent directory for path exists. If a block is given, a file
209
+ # is opened and yielded to it (as in File.open). Prepared paths are
210
+ # removed and the backups restored on rollback.
211
+ #
212
+ # Returns the expanded path.
213
+ def prepare(path, backup_using_copy=false)
214
+ raise "not a file: #{path}" if File.directory?(path)
215
+ path = File.expand_path(path)
216
+
217
+ if File.exists?(path)
218
+ # backup or remove existing files
219
+ backup(path, backup_using_copy)
220
+ else
221
+ # ensure the parent directory exists
222
+ # for non-existant files
223
+ mkdir_p File.dirname(path)
224
+ end
225
+ log :prepare, path, Logger::DEBUG
226
+ actions << [:make, path]
369
227
 
370
- # remove directories and parents until the
371
- # directory was not made by the task
372
- while added_files.include?(dir)
373
- break unless dir_empty?(dir)
374
-
375
- if File.exists?(dir)
376
- log :rmdir, dir, Logger::DEBUG
377
- FileUtils.rmdir(dir)
378
- end
379
-
380
- removed << added_files.delete(dir)
381
- dir = File.dirname(dir)
382
- end
228
+ if block_given?
229
+ File.open(path, "w") {|file| yield(file) }
383
230
  end
384
- removed
231
+
232
+ path
385
233
  end
386
234
 
387
- def dir_empty?(dir)
388
- Dir.entries(dir).delete_if {|d| d == "." || d == ".."}.empty?
235
+ # Removes a file. If a directory is provided, it's contents are removed
236
+ # recursively. Files and directories removed by rm_r are restored
237
+ # upon an execution error.
238
+ def rm_r(path)
239
+ path = File.expand_path(path)
240
+
241
+ backup(path, false)
242
+ log :rm_r, path, Logger::DEBUG
389
243
  end
390
244
 
391
- # Prepares the input list of files by backing them up (if they exist),
392
- # ensuring that the parent directory for the file exists, and adding
393
- # each file to added_files. As a result the files can be removed
394
- # using rm, restored using restore, and will be rolled back upon an
395
- # execution error.
396
- #
397
- # Returns the prepared files.
398
- #
399
- # File.open("file.txt", "w") {|f| f << "original content"}
400
- #
401
- # t = FileTask.new do |task, inputs|
402
- # File.exists?("path") # => false
403
- #
404
- # # backup... make parent dirs... prepare for restore
405
- # task.prepare(["file.txt", "path/to/file.txt"])
406
- #
407
- # File.open("file.txt", "w") {|f| f << "new content"}
408
- # File.touch("path/to/file.txt")
409
- #
410
- # raise "error!"
411
- # end
412
- #
413
- # begin
414
- # t.execute(nil)
415
- # rescue
416
- # $!.message # => "error!"
417
- # File.exists?("file.txt") # => true
418
- # File.read("file.txt") # => "original content"
419
- # File.exists?("path") # => false
420
- # end
421
- #
422
- def prepare(list, backup_using_copy=false)
423
- list = fu_list(list)
424
- existing_files, non_existant_files = list.partition do |filepath|
425
- File.exists?(filepath)
426
- end
245
+ # Removes an empty directory. Directories removed by rmdir are restored
246
+ # upon an execution error.
247
+ def rmdir(dir)
248
+ dir = File.expand_path(dir)
427
249
 
428
- # backup existing files
429
- existing_files.each do |filepath|
430
- backup(filepath, backup_using_copy)
250
+ unless Root.empty?(dir)
251
+ raise "not an empty directory: #{dir}"
431
252
  end
432
253
 
433
- # ensure the parent directory exists
434
- # for non-existant files
435
- non_existant_files.each do |filepath|
436
- dir = File.dirname(filepath)
437
- mkdir(dir)
438
- end
254
+ backup(dir, false)
255
+ log :rmdir, dir, Logger::DEBUG
256
+ end
257
+
258
+ # Removes a file. Directories cannot be removed by this method.
259
+ # Files removed by rm are restored upon an execution error.
260
+ def rm(path)
261
+ path = File.expand_path(path)
439
262
 
440
- list.each do |filepath|
441
- added_files << File.expand_path(filepath)
263
+ unless File.file?(path)
264
+ raise "not a file: #{path}"
442
265
  end
443
-
444
- list
266
+
267
+ backup(path, false)
268
+ log :rm, path, Logger::DEBUG
445
269
  end
446
270
 
447
- # Removes each file in the input list, provided the file is in added_files.
448
- # The parent directory of each file is removed using rmdir. Removed files
449
- # are removed from added_files.
450
- #
451
- # Returns the removed files and directories.
452
- #
453
- # t = FileTask.new
454
- # File.exists?("path") # => false
455
- # FileUtils.mkdir("path") # will not be removed
456
- #
457
- # t.prepare("path/to/file.txt")
458
- # FileUtils.touch("path/to/file.txt")
459
- # File.exists?("path/to/file.txt") # => true
460
- #
461
- # t.rm("path/to/file.txt")
462
- # File.exists?("path") # => true
463
- # File.exists?("path/to") # => false
464
- def rm(list)
465
- removed = []
466
- fu_list(list).each do |filepath|
467
- filepath = File.expand_path(filepath)
468
- next unless added_files.include?(filepath)
469
-
470
- # if the file exists, remove it
471
- if File.exists?(filepath)
472
- log :rm, filepath, Logger::DEBUG
473
- FileUtils.rm(filepath, :force => true)
474
- end
475
-
476
- removed << added_files.delete(filepath)
477
- removed.concat rmdir(File.dirname(filepath))
478
- end
479
- removed
271
+ # Copies source to target. Files and directories copied by cp are
272
+ # restored upon an execution error.
273
+ def cp(source, target)
274
+ target = File.join(target, File.basename(source)) if File.directory?(target)
275
+ prepare(target)
276
+
277
+ log :cp, "#{source} to #{target}", Logger::DEBUG
278
+ FileUtils.cp(source, target)
480
279
  end
481
280
 
482
- # Rolls back changes by removing added_files and restoring backed_up_files.
483
- # Rollback is performed on an execute error if rollback_on_error == true,
484
- # but is provided as a separate method for flexibility when needed.
485
- # Yields errors to the block, which must be provided.
486
- def rollback # :yields: error
487
- added_files.dup.each do |filepath|
488
- begin
489
- case
490
- when File.file?(filepath)
491
- rm(filepath)
492
- when File.directory?(filepath)
493
- rmdir(filepath)
494
- else
495
- # assures non-existant files are cleared from added_files
496
- # this is automatically done by rm and rmdir for existing files
497
- added_files.delete(filepath)
498
- end
499
- rescue
500
- yield $!
281
+ # Copies source to target. If source is a directory, the contents
282
+ # are copied recursively. If target is a directory, copies source
283
+ # to target/source. Files and directories copied by cp are restored
284
+ # upon an execution error.
285
+ def cp_r(source, target)
286
+ target = File.join(target, File.basename(source)) if File.directory?(target)
287
+ prepare(target)
288
+
289
+ log :cp_r, "#{source} to #{target}", Logger::DEBUG
290
+ FileUtils.cp_r(source, target)
291
+ end
292
+
293
+ # Moves source to target. Files and directories moved by mv are
294
+ # restored upon an execution error.
295
+ def mv(source, target, backup_source=true)
296
+ backup(source, true) if backup_source
297
+ prepare(target)
298
+
299
+ log :mv, "#{source} to #{target}", Logger::DEBUG
300
+ FileUtils.mv(source, target)
301
+ end
302
+
303
+ # Rolls back any actions capable of being rolled back. Rollback
304
+ # is forceful; for instance if you make a folder using mkdir
305
+ # rollback removes that directory using FileUtils.rm_r. Any
306
+ # files added to the folder will be removed even if they were
307
+ # not added by self.
308
+ def rollback
309
+ while !actions.empty?
310
+ action, source, target = actions.pop
311
+
312
+ case action
313
+ when :make
314
+ log :rollback, "#{source}", Logger::DEBUG
315
+ FileUtils.rm_r(source)
316
+ when :backup
317
+ log :rollback, "#{target} to #{source}", Logger::DEBUG
318
+ dir = File.dirname(source)
319
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
320
+ FileUtils.mv(target, source, :force => true)
321
+ else
322
+ raise "unknown action: #{[action, source, target].inspect}"
501
323
  end
502
324
  end
503
-
504
- backed_up_files.keys.each do |filepath|
505
- begin
506
- restore(filepath)
507
- rescue
508
- yield $!
325
+ end
326
+
327
+ # Removes backup files. Cleanup cannot be rolled back and prevents
328
+ # rollback of actions up to when cleanup is called. If cleanup_dirs
329
+ # is true, empty directories containing the backup files will be
330
+ # removed.
331
+ def cleanup(cleanup_dirs=true)
332
+ actions.each do |action, source, target|
333
+ if action == :backup
334
+ log :cleanup, target, Logger::DEBUG
335
+ FileUtils.rm_r(target) if File.exists?(target)
336
+ cleanup_dir(File.dirname(target)) if cleanup_dirs
509
337
  end
510
338
  end
339
+ actions.clear
511
340
  end
512
341
 
513
- # Removes backed-up files matching the pattern.
514
- def cleanup(pattern=/.*/)
515
- backed_up_files.each do |filepath, target|
516
- next unless target =~ pattern
517
-
518
- # the filepath needs to be added to added_files
519
- # before it can be removed by rm
520
- added_files << target
521
- rm(target)
522
- backed_up_files.delete(filepath)
523
- end
342
+ # Removes the directory if empty, and all empty parent directories. This
343
+ # method cannot be rolled back.
344
+ def cleanup_dir(dir)
345
+ while Root.empty?(dir)
346
+ log :rmdir, dir, Logger::DEBUG
347
+ FileUtils.rmdir(dir)
348
+ dir = File.dirname(dir)
349
+ end
524
350
  end
525
351
 
526
- # Logs the given action, with the basenames of the input filepaths.
527
- def log_basename(action, filepaths, level=Logger::INFO)
528
- msg = case filepaths
529
- when Array then filepaths.collect {|filepath| File.basename(filepath) }.join(',')
530
- else
531
- File.basename(filepaths)
532
- end
533
-
352
+ # Logs the given action, with the basenames of the input paths.
353
+ def log_basename(action, paths, level=Logger::INFO)
354
+ msg = [paths].flatten.collect {|path| File.basename(path) }.join(',')
534
355
  log(action, msg, level)
535
356
  end
536
357
 
537
358
  protected
538
-
539
- attr_writer :backed_up_files, :added_files
540
359
 
541
- # Clears added_files and backed_up_files so that
542
- # a failure will not affect previous executions
360
+ # An array tracking actions (backup, rm, mv, etc) performed by self,
361
+ # allowing rollback on an execution error. Not intended to be
362
+ # modified manually.
363
+ attr_reader :actions
364
+
365
+ # Clears actions so that a failure will not affect previous executions
543
366
  def before_execute
544
- added_files.clear
545
- backed_up_files.clear
367
+ actions.clear
546
368
  end
547
369
 
548
370
  # Removes made files/dirs and restores backed-up files upon
549
- # an execute error. Collects any errors raised along the way
550
- # and raises them in a Tap::Support::RunError.
371
+ # an execute error.
551
372
  def on_execute_error(original_error)
552
- rollback_errors = []
553
- if rollback_on_error
554
- rollback {|error| rollback_errors << error}
555
- end
556
-
557
- # Re-raise the error if no rollback errors occured,
558
- # otherwise, raise a RunError tracking the errors.
559
- if rollback_errors.empty?
560
- raise original_error
561
- else
562
- rollback_errors.unshift(original_error)
563
- raise Support::RunError.new(rollback_errors)
564
- end
373
+ rollback if rollback_on_error
374
+ raise original_error
565
375
  end
566
376
 
567
- # Lifted from FileUtils
568
- def fu_list(arg)
569
- [arg].flatten.map {|path| path.to_str }
377
+ private
378
+
379
+ # utility method for backup_filepath; increments index until the
380
+ # path base.indexext does not exist.
381
+ def next_indexed_path(base, index, ext) # :nodoc:
382
+ path = sprintf('%s.%d%s', base, index, ext)
383
+ File.exists?(path) ? next_indexed_path(base, index + 1, ext) : path
570
384
  end
571
385
  end
572
386
  end