tap 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/History +35 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README +16 -15
  4. data/bin/tap +1 -1
  5. data/cmd/console.rb +4 -3
  6. data/cmd/manifest.rb +2 -2
  7. data/cmd/run.rb +12 -15
  8. data/doc/Class Reference +120 -117
  9. data/doc/Command Reference +27 -27
  10. data/doc/Syntax Reference +55 -111
  11. data/doc/Tutorial +69 -26
  12. data/lib/tap.rb +3 -8
  13. data/lib/tap/app.rb +122 -146
  14. data/lib/tap/constants.rb +2 -2
  15. data/lib/tap/env.rb +178 -252
  16. data/lib/tap/exe.rb +67 -30
  17. data/lib/tap/file_task.rb +224 -411
  18. data/lib/tap/generator/arguments.rb +13 -0
  19. data/lib/tap/generator/base.rb +112 -30
  20. data/lib/tap/generator/destroy.rb +36 -13
  21. data/lib/tap/generator/generate.rb +69 -48
  22. data/lib/tap/generator/generators/command/templates/command.erb +3 -3
  23. data/lib/tap/generator/generators/config/config_generator.rb +82 -10
  24. data/lib/tap/generator/generators/generator/generator_generator.rb +16 -6
  25. data/lib/tap/generator/generators/generator/templates/task.erb +2 -2
  26. data/lib/tap/generator/generators/generator/templates/test.erb +26 -0
  27. data/lib/tap/generator/generators/root/root_generator.rb +24 -13
  28. data/lib/tap/generator/generators/root/templates/Rakefile +4 -4
  29. data/lib/tap/generator/generators/root/templates/{tapfile → Rapfile} +6 -6
  30. data/lib/tap/generator/generators/root/templates/gemspec +0 -1
  31. data/lib/tap/generator/generators/task/task_generator.rb +3 -3
  32. data/lib/tap/generator/generators/task/templates/test.erb +1 -1
  33. data/lib/tap/generator/manifest.rb +7 -1
  34. data/lib/tap/generator/preview.rb +76 -0
  35. data/lib/tap/root.rb +222 -156
  36. data/lib/tap/spec.rb +41 -0
  37. data/lib/tap/support/aggregator.rb +25 -28
  38. data/lib/tap/support/audit.rb +278 -357
  39. data/lib/tap/support/constant.rb +2 -1
  40. data/lib/tap/support/constant_manifest.rb +28 -25
  41. data/lib/tap/support/dependency.rb +1 -1
  42. data/lib/tap/support/executable.rb +52 -183
  43. data/lib/tap/support/executable_queue.rb +50 -20
  44. data/lib/tap/support/gems.rb +1 -1
  45. data/lib/tap/support/intern.rb +0 -6
  46. data/lib/tap/support/join.rb +49 -83
  47. data/lib/tap/support/joins.rb +0 -3
  48. data/lib/tap/support/joins/switch.rb +13 -11
  49. data/lib/tap/support/joins/sync_merge.rb +25 -50
  50. data/lib/tap/support/manifest.rb +1 -0
  51. data/lib/tap/support/node.rb +140 -20
  52. data/lib/tap/support/parser.rb +56 -42
  53. data/lib/tap/support/schema.rb +183 -157
  54. data/lib/tap/support/templater.rb +9 -1
  55. data/lib/tap/support/versions.rb +39 -0
  56. data/lib/tap/task.rb +150 -177
  57. data/lib/tap/tasks/dump.rb +4 -4
  58. data/lib/tap/tasks/load.rb +29 -29
  59. data/lib/tap/test.rb +66 -53
  60. data/lib/tap/test/env_vars.rb +3 -3
  61. data/lib/tap/test/extensions.rb +11 -17
  62. data/lib/tap/test/file_test.rb +74 -132
  63. data/lib/tap/test/file_test_class.rb +4 -1
  64. data/lib/tap/test/regexp_escape.rb +2 -2
  65. data/lib/tap/test/script_test.rb +2 -2
  66. data/lib/tap/test/subset_test.rb +6 -6
  67. data/lib/tap/test/tap_test.rb +28 -154
  68. metadata +30 -51
  69. data/bin/rap +0 -118
  70. data/cgi/run.rb +0 -97
  71. data/lib/tap/declarations.rb +0 -229
  72. data/lib/tap/generator/generators/config/templates/doc.erb +0 -12
  73. data/lib/tap/generator/generators/config/templates/nodoc.erb +0 -8
  74. data/lib/tap/generator/generators/file_task/file_task_generator.rb +0 -27
  75. data/lib/tap/generator/generators/file_task/templates/file.txt +0 -11
  76. data/lib/tap/generator/generators/file_task/templates/result.yml +0 -6
  77. data/lib/tap/generator/generators/file_task/templates/task.erb +0 -33
  78. data/lib/tap/generator/generators/file_task/templates/test.erb +0 -29
  79. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +0 -5
  80. data/lib/tap/patches/optparse/summarize.rb +0 -62
  81. data/lib/tap/support/assignments.rb +0 -173
  82. data/lib/tap/support/class_configuration.rb +0 -182
  83. data/lib/tap/support/combinator.rb +0 -125
  84. data/lib/tap/support/configurable.rb +0 -113
  85. data/lib/tap/support/configurable_class.rb +0 -271
  86. data/lib/tap/support/configuration.rb +0 -170
  87. data/lib/tap/support/gems/rake.rb +0 -111
  88. data/lib/tap/support/instance_configuration.rb +0 -173
  89. data/lib/tap/support/joins/fork.rb +0 -19
  90. data/lib/tap/support/joins/merge.rb +0 -22
  91. data/lib/tap/support/joins/sequence.rb +0 -21
  92. data/lib/tap/support/lazy_attributes.rb +0 -45
  93. data/lib/tap/support/lazydoc.rb +0 -386
  94. data/lib/tap/support/lazydoc/comment.rb +0 -503
  95. data/lib/tap/support/lazydoc/config.rb +0 -17
  96. data/lib/tap/support/lazydoc/definition.rb +0 -36
  97. data/lib/tap/support/lazydoc/document.rb +0 -152
  98. data/lib/tap/support/lazydoc/method.rb +0 -24
  99. data/lib/tap/support/tdoc.rb +0 -409
  100. data/lib/tap/support/tdoc/tdoc_html_generator.rb +0 -38
  101. data/lib/tap/support/tdoc/tdoc_html_template.rb +0 -42
  102. data/lib/tap/support/validation.rb +0 -479
  103. data/lib/tap/tasks/rake.rb +0 -57
@@ -4,44 +4,89 @@ require 'tap/support/schema'
4
4
 
5
5
  module Tap
6
6
  class Exe < Env
7
-
8
7
  class << self
9
- def instantiate(path=Dir.pwd, logger=Tap::App::DEFAULT_LOGGER, &block)
10
- app = Tap::App.instance = Tap::App.new({:root => path}, logger)
11
- exe = super(app, load_config(GLOBAL_CONFIG_FILE), app.logger, &block)
8
+ def instantiate(path=Dir.pwd)
9
+ app = App.instance.reconfigure(:root => path)
10
+ exe = super(app)
12
11
 
13
12
  # add all gems if no gems are specified (Note this is VERY SLOW ~ 1/3 the overhead for tap)
14
- if !File.exists?(Tap::Env::DEFAULT_CONFIG_FILE)
15
- exe.gems = gemspecs(false)
16
- end
17
-
13
+ exe.gems = :all if !File.exists?(Tap::Env::DEFAULT_CONFIG_FILE)
14
+
18
15
  # add the default tap instance
19
- tap = instance_for("#{File.dirname(__FILE__)}/../..")
20
- # tap.tasks.paths = tap.root.glob(:lib, "tap/tasks/*").collect do |task_path|
21
- # [tap.root[:lib], task_path]
22
- # end
23
- exe.push(tap)
16
+ exe.push Env.instantiate("#{File.dirname(__FILE__)}/../..")
24
17
  exe
25
18
  end
26
19
 
27
- def instance_for(path)
28
- path = pathify(path)
29
- instances.has_key?(path) ? instances[path] : Env.instantiate(path)
30
- end
20
+ def load_config(path)
21
+ super(GLOBAL_CONFIG_FILE).merge super(path)
22
+ end
23
+
24
+ # Adapted from Gem.find_home
25
+ def user_home
26
+ ['HOME', 'USERPROFILE'].each do |homekey|
27
+ return ENV[homekey] if ENV[homekey]
28
+ end
29
+
30
+ if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] then
31
+ return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}"
32
+ end
33
+
34
+ begin
35
+ File.expand_path("~")
36
+ rescue
37
+ File::ALT_SEPARATOR ? "C:/" : "/"
38
+ end
39
+ end
40
+ end
41
+
42
+ # The Root directory structure for self.
43
+ nest(:root, Tap::App) do |config|
44
+ case config
45
+ when App then config
46
+ when String then App.new(:root => config)
47
+ else App.new(config)
48
+ end
31
49
  end
32
50
 
33
51
  config :before, nil
34
52
  config :after, nil
53
+ # Specify files to require when self is activated.
54
+ config :requires, [], &c.array_or_nil
55
+
56
+ # Specify files to load when self is activated.
57
+ config :loads, [], &c.array_or_nil
35
58
  config :aliases, {}, &c.hash_or_nil
36
59
 
60
+ # The global home directory
61
+ GLOBAL_HOME = File.join(Exe.user_home, ".tap")
62
+
37
63
  # The global config file path
38
- GLOBAL_CONFIG_FILE = File.join(Gem.user_home, ".tap.yml")
39
-
64
+ GLOBAL_CONFIG_FILE = File.join(GLOBAL_HOME, "tap.yml")
65
+
66
+ def initialize(app=App.instance)
67
+ super(app)
68
+ end
69
+
40
70
  # Alias for root (Exe should have a Tap::App as root)
41
71
  def app
42
72
  root
43
73
  end
44
74
 
75
+ def activate
76
+ if super
77
+
78
+ # perform requires
79
+ requires.each do |path|
80
+ require path
81
+ end
82
+
83
+ # perform loads
84
+ loads.each do |path|
85
+ load path
86
+ end
87
+ end
88
+ end
89
+
45
90
  def handle_error(err)
46
91
  case
47
92
  when $DEBUG
@@ -68,14 +113,14 @@ module Tap
68
113
  load path # run the command, if it exists
69
114
  else
70
115
  puts "Unknown command: '#{command}'"
71
- puts "Type 'tap help' for usage information."
116
+ puts "Type 'tap --help' for usage information."
72
117
  end
73
118
  end
74
119
  end
75
120
 
76
- def build(argv=ARGV)
121
+ def build(argv=ARGV, app=app)
77
122
  schema = argv.kind_of?(Support::Schema) ? argv : Support::Schema.parse(argv)
78
- schema.compact.build(app) do |args|
123
+ schema.build(app) do |args|
79
124
  task = args.shift
80
125
  const = tasks.search(task)
81
126
 
@@ -131,13 +176,5 @@ module Tap
131
176
  end
132
177
  end
133
178
  end
134
-
135
- def run(queues)
136
- queues.each_with_index do |queue, i|
137
- app.queue.concat(queue)
138
- app.run
139
- end
140
- end
141
-
142
179
  end
143
180
  end
@@ -1,118 +1,54 @@
1
1
  require 'tap/support/shell_utils'
2
- autoload(:FileUtils, "fileutils")
3
2
 
4
3
  module Tap
5
4
 
6
- # FileTask provides methods for creating/modifying files such that you can
7
- # rollback changes if an error occurs.
5
+ # FileTask is a base class for tasks that work with a file system. FileTask
6
+ # tracks changes it makes so they may be rolled back to their original state.
7
+ # Rollback automatically occurs on an execute error.
8
8
  #
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"}
9
+ # File.open("file.txt", "w") {|file| file << "original content"}
23
10
  #
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")
11
+ # t = FileTask.intern do |task, raise_error|
12
+ # task.mkdir_p("some/dir") # marked for rollback
13
+ # task.prepare("file.txt") do |file| # marked for rollback
14
+ # file << "new content"
15
+ # end
30
16
  #
31
17
  # # raise an error to start rollback
32
- # raise "error!"
18
+ # raise "error!" if raise_error
33
19
  # end
34
20
  #
35
21
  # begin
36
- # File.exists?("some/dir") # => false
37
- # File.exists?("path/to/file.txt") # => false
38
- # t.execute(nil)
22
+ # t.execute(true)
39
23
  # rescue
40
24
  # $!.message # => "error!"
41
25
  # File.exists?("some/dir") # => false
42
- # File.exists?("path/to/file.txt") # => false
43
26
  # File.read("file.txt") # => "original content"
44
27
  # end
45
28
  #
29
+ # t.execute(false)
30
+ # File.exists?("some/dir") # => true
31
+ # File.read("file.txt") # => "new content"
32
+ #
46
33
  class FileTask < Task
47
34
  include Tap::Support::ShellUtils
48
35
 
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
36
+ # The backup directory
59
37
  config_attr :backup_dir, 'backup' # the backup directory
60
38
 
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
39
+ # A flag indicating whether or track changes
40
+ # for rollback on execution error
67
41
  config :rollback_on_error, true, &c.switch # rollback changes on error
68
42
 
69
43
  def initialize(config={}, name=nil, app=App.instance)
70
44
  super
71
-
72
- @backed_up_files = {}
73
- @added_files = []
45
+ @actions = []
74
46
  end
75
47
 
48
+ # Initializes a copy that will rollback independent of self.
76
49
  def initialize_copy(orig)
77
50
  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
51
+ @actions = []
116
52
  end
117
53
 
118
54
  # Returns the path, exchanging the extension with extname.
@@ -130,13 +66,9 @@ module Tap
130
66
  # Compare to basename.
131
67
  def basepath(path, extname=false)
132
68
  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}"
69
+ when false, nil then path.chomp(File.extname(path))
70
+ when true then path
71
+ else Root.exchange(path, extname)
140
72
  end
141
73
  end
142
74
 
@@ -167,27 +99,26 @@ module Tap
167
99
  app.filepath(dir, name, *paths)
168
100
  end
169
101
 
170
- # Makes a backup filepath relative to backup_dir by using self.name, the
171
- # basename of filepath plus a timestamp.
102
+ # Makes a backup filepath relative to backup_dir by using name, the
103
+ # basename of filepath, and an index.
172
104
  #
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"
105
+ # t = FileTask.new({:backup_dir => "/backup"}, "name")
106
+ # t.backup_filepath("path/to/file.txt", time) # => "/backup/name/file.0.txt"
178
107
  #
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)
108
+ def backup_filepath(path)
109
+ extname = File.extname(path)
110
+ backup_path = filepath(backup_dir, File.basename(path).chomp(extname))
111
+ next_indexed_path(backup_path, 0, extname)
183
112
  end
184
113
 
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.
114
+ # Returns true if all of the targets are up to date relative to all of the
115
+ # listed sources. Single values or arrays can be provided for both targets
116
+ # and sources.
117
+ #
118
+ # Returns false (ie 'not up to date') if app.force is true.
188
119
  #
189
- #---
190
- # Returns false (ie 'not up to date') if +force?+ is true.
120
+ #--
121
+ # TODO: add check vs date reference (ex config_file date)
191
122
  def uptodate?(targets, sources=[])
192
123
  if app.force
193
124
  log_basename(:force, *targets)
@@ -196,9 +127,6 @@ module Tap
196
127
  targets = [targets] unless targets.kind_of?(Array)
197
128
  sources = [sources] unless sources.kind_of?(Array)
198
129
 
199
- # should be able to specify this somehow, externally set
200
- # sources << config_file unless config_file == nil
201
-
202
130
  targets.each do |target|
203
131
  return false unless FileUtils.uptodate?(target, sources)
204
132
  end
@@ -206,367 +134,252 @@ module Tap
206
134
  end
207
135
  end
208
136
 
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.
137
+ # Makes a backup of path to backup_filepath(path) and returns the backup path.
138
+ # If backup_using_copy is true, the backup is a copy of path, otherwise the
139
+ # file or directory at path is moved to the backup path. Raises an error if
140
+ # the backup path already exists.
214
141
  #
215
- # Returns a list of the backup_filepaths.
142
+ # Backups are restored on rollback.
216
143
  #
217
144
  # file = "file.txt"
218
145
  # File.open(file, "w") {|f| f << "file content"}
219
146
  #
220
147
  # t = FileTask.new
221
- # backed_up_file = t.backup(file).first
148
+ # backup_file = t.backup(file)
222
149
  #
223
150
  # File.exists?(file) # => false
224
- # File.exists?(backed_up_file) # => true
225
- # File.read(backed_up_file) # => "file content"
151
+ # File.exists?(backup_file) # => true
152
+ # File.read(backup_file) # => "file content"
226
153
  #
227
154
  # File.open(file, "w") {|f| f << "new content"}
228
- # t.restore(file)
155
+ # t.rollback
229
156
  #
230
157
  # File.exists?(file) # => true
231
- # File.exists?(backed_up_file) # => false
158
+ # File.exists?(backup_file ) # => false
232
159
  # File.read(file) # => "file content"
233
160
  #
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
161
+ def backup(path, backup_using_copy=false)
162
+ return nil unless File.exists?(path)
242
163
 
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
164
+ source = File.expand_path(path)
165
+ target = backup_filepath(source)
166
+ raise "backup already exists: #{target}" if File.exists?(target)
167
+
168
+ mkdir_p File.dirname(target)
169
+
170
+ log :backup, "#{source} to #{target}", Logger::DEBUG
171
+ if backup_using_copy
172
+ FileUtils.cp(source, target)
173
+ else
174
+ FileUtils.mv(source, target)
258
175
  end
176
+
177
+ actions << [:backup, source, target]
178
+ target
259
179
  end
260
180
 
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)
181
+ # Creates a directory and all its parent directories. Directories created
182
+ # by mkdir_p removed on rollback.
183
+ def mkdir_p(dir)
184
+ dir = File.expand_path(dir)
292
185
 
293
- log :restore, "#{target} to #{filepath}", Logger::DEBUG
294
- FileUtils.mv(target, filepath, :force => true)
295
-
296
- dir = File.dirname(target)
297
- rmdir(dir)
186
+ dirs = []
187
+ while !File.exists?(dir)
188
+ dirs.unshift(dir)
189
+ dir = File.dirname(dir)
190
+ end
298
191
 
299
- filepath
300
- end.compact
192
+ dirs.each {|d| mkdir(d) }
301
193
  end
302
194
 
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
195
+ # Creates a directory. Directories created by mkdir removed on rollback.
196
+ def mkdir(dir)
197
+ dir = File.expand_path(dir)
339
198
 
340
- make_paths.reverse_each do |path|
341
- log :mkdir, path, Logger::DEBUG
342
- FileUtils.mkdir(path)
343
- added_files << path
344
- end
199
+ unless File.exists?(dir)
200
+ log :mkdir, dir, Logger::DEBUG
201
+ FileUtils.mkdir(dir)
202
+ actions << [:make, dir]
345
203
  end
346
204
  end
347
205
 
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)
206
+ # Prepares the path by backing up any existing file and ensuring that
207
+ # the parent directory for path exists. If a block is given, a file
208
+ # is opened and yielded to it (as in File.open). Prepared paths are
209
+ # removed and the backups restored on rollback.
210
+ #
211
+ # Returns the expanded path.
212
+ def prepare(path, backup_using_copy=false)
213
+ raise "not a file: #{path}" if File.directory?(path)
214
+ path = File.expand_path(path)
215
+
216
+ if File.exists?(path)
217
+ # backup or remove existing files
218
+ backup(path, backup_using_copy)
219
+ else
220
+ # ensure the parent directory exists
221
+ # for non-existant files
222
+ mkdir_p File.dirname(path)
223
+ end
224
+ log :prepare, path, Logger::DEBUG
225
+ actions << [:make, path]
369
226
 
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
227
+ if block_given?
228
+ File.open(path, "w") {|file| yield(file) }
383
229
  end
384
- removed
230
+
231
+ path
385
232
  end
386
233
 
387
- def dir_empty?(dir)
388
- Dir.entries(dir).delete_if {|d| d == "." || d == ".."}.empty?
234
+ # Removes a file. If a directory is provided, it's contents are removed
235
+ # recursively. Files and directories removed by rm_r are restored
236
+ # upon an execution error.
237
+ def rm_r(path)
238
+ path = File.expand_path(path)
239
+
240
+ backup(path, false)
241
+ log :rm_r, path, Logger::DEBUG
389
242
  end
390
243
 
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
244
+ # Removes an empty directory. Directories removed by rmdir are restored
245
+ # upon an execution error.
246
+ def rmdir(dir)
247
+ dir = File.expand_path(dir)
427
248
 
428
- # backup existing files
429
- existing_files.each do |filepath|
430
- backup(filepath, backup_using_copy)
249
+ unless Root.empty?(dir)
250
+ raise "not an empty directory: #{dir}"
431
251
  end
432
252
 
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
253
+ backup(dir, false)
254
+ log :rmdir, dir, Logger::DEBUG
255
+ end
256
+
257
+ # Removes a file. Directories cannot be removed by this method.
258
+ # Files removed by rm are restored upon an execution error.
259
+ def rm(path)
260
+ path = File.expand_path(path)
439
261
 
440
- list.each do |filepath|
441
- added_files << File.expand_path(filepath)
262
+ unless File.file?(path)
263
+ raise "not a file: #{path}"
442
264
  end
443
-
444
- list
265
+
266
+ backup(path, false)
267
+ log :rm, path, Logger::DEBUG
445
268
  end
446
269
 
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
270
+ # Copies source to target. Files and directories copied by cp are
271
+ # restored upon an execution error.
272
+ def cp(source, target)
273
+ target = File.join(target, File.basename(source)) if File.directory?(target)
274
+ prepare(target)
275
+
276
+ log :cp, "#{source} to #{target}", Logger::DEBUG
277
+ FileUtils.cp(source, target)
480
278
  end
481
279
 
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 $!
280
+ # Copies source to target. If source is a directory, the contents
281
+ # are copied recursively. If target is a directory, copies source
282
+ # to target/source. Files and directories copied by cp are restored
283
+ # upon an execution error.
284
+ def cp_r(source, target)
285
+ target = File.join(target, File.basename(source)) if File.directory?(target)
286
+ prepare(target)
287
+
288
+ log :cp_r, "#{source} to #{target}", Logger::DEBUG
289
+ FileUtils.cp_r(source, target)
290
+ end
291
+
292
+ # Moves source to target. Files and directories moved by mv are
293
+ # restored upon an execution error.
294
+ def mv(source, target, backup_source=true)
295
+ backup(source, true) if backup_source
296
+ prepare(target)
297
+
298
+ log :mv, "#{source} to #{target}", Logger::DEBUG
299
+ FileUtils.mv(source, target)
300
+ end
301
+
302
+ # Rolls back any actions capable of being rolled back.
303
+ #
304
+ # Rollback is forceful. For instance if you make a folder using
305
+ # mkdir, rollback will remove the folder and all files within it
306
+ # even if they were not added by self.
307
+ def rollback
308
+ while !actions.empty?
309
+ action, source, target = actions.pop
310
+
311
+ case action
312
+ when :make
313
+ log :rollback, "#{source}", Logger::DEBUG
314
+ FileUtils.rm_r(source)
315
+ when :backup
316
+ log :rollback, "#{target} to #{source}", Logger::DEBUG
317
+ dir = File.dirname(source)
318
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
319
+ FileUtils.mv(target, source, :force => true)
320
+ else
321
+ raise "unknown action: #{[action, source, target].inspect}"
501
322
  end
502
323
  end
503
-
504
- backed_up_files.keys.each do |filepath|
505
- begin
506
- restore(filepath)
507
- rescue
508
- yield $!
324
+ end
325
+
326
+ # Removes backup files. Cleanup cannot be rolled back and prevents
327
+ # rollback of actions up to when cleanup is called. If cleanup_dirs
328
+ # is true, empty directories containing the backup files will be
329
+ # removed.
330
+ def cleanup(cleanup_dirs=true)
331
+ actions.each do |action, source, target|
332
+ if action == :backup
333
+ log :cleanup, target, Logger::DEBUG
334
+ FileUtils.rm_r(target) if File.exists?(target)
335
+ cleanup_dir(File.dirname(target)) if cleanup_dirs
509
336
  end
510
337
  end
338
+ actions.clear
511
339
  end
512
340
 
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
341
+ # Removes the directory if empty, and all empty parent directories. This
342
+ # method cannot be rolled back.
343
+ def cleanup_dir(dir)
344
+ while Root.empty?(dir)
345
+ log :rmdir, dir, Logger::DEBUG
346
+ FileUtils.rmdir(dir)
347
+ dir = File.dirname(dir)
348
+ end
524
349
  end
525
350
 
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
-
351
+ # Logs the given action, with the basenames of the input paths.
352
+ def log_basename(action, paths, level=Logger::INFO)
353
+ msg = [paths].flatten.collect {|path| File.basename(path) }.join(',')
534
354
  log(action, msg, level)
535
355
  end
536
356
 
537
357
  protected
538
-
539
- attr_writer :backed_up_files, :added_files
540
358
 
541
- # Clears added_files and backed_up_files so that
542
- # a failure will not affect previous executions
359
+ # An array tracking actions (backup, rm, mv, etc) performed by self,
360
+ # allowing rollback on an execution error. Not intended to be
361
+ # modified manually.
362
+ attr_reader :actions
363
+
364
+ # Clears actions so that a failure will not affect previous executions
543
365
  def before_execute
544
- added_files.clear
545
- backed_up_files.clear
366
+ actions.clear
546
367
  end
547
368
 
548
369
  # 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.
370
+ # an execute error.
551
371
  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
372
+ rollback if rollback_on_error
373
+ raise original_error
565
374
  end
566
375
 
567
- # Lifted from FileUtils
568
- def fu_list(arg)
569
- [arg].flatten.map {|path| path.to_str }
376
+ private
377
+
378
+ # utility method for backup_filepath; increments index until the
379
+ # path base.indexext does not exist.
380
+ def next_indexed_path(base, index, ext) # :nodoc:
381
+ path = sprintf('%s.%d%s', base, index, ext)
382
+ File.exists?(path) ? next_indexed_path(base, index + 1, ext) : path
570
383
  end
571
384
  end
572
385
  end