xmigra 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
1
  require 'xmigra/console'
2
+ require 'xmigra/migration_chain'
2
3
 
3
4
  module XMigra
4
5
  module GitSpecifics
@@ -7,6 +8,7 @@ module XMigra
7
8
  MASTER_HEAD_ATTRIBUTE = 'xmigra-master'
8
9
  MASTER_BRANCH_SUBDIR = 'xmigra-master'
9
10
  PRODUCTION_CHAIN_EXTENSION_COMMAND = 'xmigra-on-production-chain-extended'
11
+ ATTRIBUTE_UNSPECIFIED = 'unspecified'
10
12
 
11
13
  class AttributesFile
12
14
  def initialize(effect_root, access=:shared)
@@ -161,7 +163,16 @@ module XMigra
161
163
  end
162
164
 
163
165
  def git(*args)
164
- Dir.chdir(self.path) do |pwd|
166
+ _path = begin
167
+ self.path
168
+ rescue NameError
169
+ begin
170
+ self.schema_dir
171
+ rescue NameError
172
+ Pathname(self.file_path).dirname
173
+ end
174
+ end
175
+ Dir.chdir(_path) do |pwd|
165
176
  GitSpecifics.run_git(*args)
166
177
  end
167
178
  end
@@ -211,7 +222,13 @@ module XMigra
211
222
  end
212
223
 
213
224
  def branch_identifier
214
- return (if self.production
225
+ for_production = begin
226
+ self.production
227
+ rescue NameError
228
+ false
229
+ end
230
+
231
+ return (if for_production
215
232
  self.git_branch_info[0]
216
233
  else
217
234
  return @git_branch_identifier if defined? @git_branch_identifier
@@ -227,9 +244,25 @@ module XMigra
227
244
  if commit
228
245
  self.git_fetch_master_branch
229
246
 
230
- # If there are no commits between the master head and *commit*, then
231
- # *commit* is production-ish
232
- return (self.git_commits_in? self.git_master_local_branch..commit) ? :development : :production
247
+ # If there are commits between the master head and *commit*, then
248
+ # *commit* is not production-ish
249
+ if self.git_commits_in? self.git_master_local_branch..commit
250
+ return :development
251
+ end
252
+
253
+ # Otherwise, look to see if all migrations in the migration chain for
254
+ # commit are in the master head with no diffs -- the migration chain
255
+ # is a "prefix" of the chain in the master head:
256
+ migration_chain = RepoStoredMigrationChain.new(
257
+ commit,
258
+ Pathname(path).join(SchemaManipulator::STRUCTURE_SUBDIR),
259
+ )
260
+ return :production if self.git(
261
+ :diff, '--name-only',
262
+ self.git_master_local_branch, commit, '--',
263
+ *migration_chain.map(&:file_path)
264
+ ).empty?
265
+ return :development
233
266
  end
234
267
 
235
268
  return nil unless self.git_master_head(:required=>false)
@@ -253,6 +286,15 @@ module XMigra
253
286
  return nil
254
287
  end
255
288
 
289
+ def vcs_contents(path, options={})
290
+ args = []
291
+
292
+ commit = options.fetch(:revision, 'HEAD')
293
+ args << "#{commit}:#{path}"
294
+
295
+ git(:show, *args)
296
+ end
297
+
256
298
  def vcs_prod_chain_extension_handler
257
299
  attr_val = GitSpecifics.attr_values(
258
300
  PRODUCTION_CHAIN_EXTENSION_COMMAND,
@@ -273,6 +315,124 @@ module XMigra
273
315
  return attr_val
274
316
  end
275
317
 
318
+ def vcs_uncommitted?
319
+ git_status == '??'
320
+ end
321
+
322
+ class VersionComparator
323
+ # vcs_object.kind_of?(GitSpecifics)
324
+ def initialize(vcs_object, options={})
325
+ @object = vcs_object
326
+ @expected_content_method = options[:expected_content_method]
327
+ @path_statuses = Hash.new do |h, file_path|
328
+ file_path = Pathname(file_path).expand_path
329
+ next h[file_path] if h.has_key?(file_path)
330
+ h[file_path] = @object.git_retrieve_status(file_path)
331
+ end
332
+ end
333
+
334
+ def relative_version(file_path)
335
+ # Comparing @object.file_path (a) to file_path (b)
336
+ #
337
+ # returns: :newer, :equal, :older, or :missing
338
+
339
+ b_status = @path_statuses[file_path]
340
+
341
+ return :missing if b_status.nil? || b_status.include?('D')
342
+
343
+ a_status = @path_statuses[@object.file_path]
344
+
345
+ if a_status == '??' || a_status[0] == 'A'
346
+ if b_status == '??' || b_status[0] == 'A' || b_status.include?('M')
347
+ return relative_version_by_content(file_path)
348
+ end
349
+
350
+ return :older
351
+ elsif a_status == ' '
352
+ return :newer unless b_status == ' '
353
+
354
+ return begin
355
+ a_commit = latest_commit(@object.file_path)
356
+ b_commit = latest_commit(file_path)
357
+
358
+ if @object.git_commits_in? a_commit..b_commit, file_path
359
+ :newer
360
+ elsif @object.git_commits_in? b_commit..a_commit, @object.file_path
361
+ :older
362
+ else
363
+ :equal
364
+ end
365
+ end
366
+ elsif b_status == ' '
367
+ return :older
368
+ else
369
+ return relative_version_by_content(file_path)
370
+ end
371
+ end
372
+
373
+ def latest_commit(file_path)
374
+ @object.git(
375
+ :log,
376
+ '--pretty=format:%H',
377
+ '-1',
378
+ '--',
379
+ file_path
380
+ )
381
+ end
382
+
383
+ def relative_version_by_content(file_path)
384
+ ec_method = @expected_content_method
385
+ if !ec_method || @object.send(ec_method, file_path)
386
+ return :equal
387
+ else
388
+ return :newer
389
+ end
390
+ end
391
+ end
392
+
393
+ def vcs_comparator(options={})
394
+ VersionComparator.new(self, options)
395
+ end
396
+
397
+ def vcs_latest_revision(a_file=nil)
398
+ if a_file.nil? && defined? @vcs_latest_revision
399
+ return @vcs_latest_revision
400
+ end
401
+
402
+ git(
403
+ :log,
404
+ '-n1',
405
+ '--pretty=format:%H',
406
+ '--',
407
+ a_file || file_path,
408
+ :quiet=>true
409
+ ).chomp.tap do |val|
410
+ @vcs_latest_revision = val if a_file.nil?
411
+ end
412
+ end
413
+
414
+ def vcs_changes_from(from_commit, file_path)
415
+ git(:diff, from_commit, '--', file_path)
416
+ end
417
+
418
+ def vcs_most_recent_committed_contents(file_path)
419
+ git(:show, "HEAD:#{file_path}", :quiet=>true)
420
+ end
421
+
422
+ def git_status
423
+ @git_status ||= git_retrieve_status(file_path)
424
+ end
425
+
426
+ def git_retrieve_status(a_path)
427
+ return nil unless Pathname(a_path).exist?
428
+
429
+ if git('status', '--porcelain', a_path.to_s) =~ /^.+?(?= \S)/
430
+ $&
431
+ else
432
+ ' '
433
+ end
434
+ end
435
+
276
436
  def production_pattern
277
437
  ".+"
278
438
  end
@@ -333,17 +493,17 @@ module XMigra
333
493
  :required=>options[:required]
334
494
  )
335
495
  return nil if master_head.nil?
336
- return @git_master_head = master_head
496
+ return @git_master_head = (master_head if master_head != GitSpecifics::ATTRIBUTE_UNSPECIFIED)
337
497
  end
338
498
 
339
499
  def git_branch
340
500
  return @git_branch if defined? @git_branch
341
- return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}).chomp
501
+ return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}, :quiet=>true).chomp
342
502
  end
343
503
 
344
504
  def git_schema_commit
345
505
  return @git_commit if defined? @git_commit
346
- reported_commit = git(:log, %w{-n1 --format=%H --}, self.path).chomp
506
+ reported_commit = git(:log, %w{-n1 --format=%H --}, self.path, :quiet=>true).chomp
347
507
  raise VersionControlError, "Schema not committed" if reported_commit.empty?
348
508
  return @git_commit = reported_commit
349
509
  end
@@ -412,5 +572,26 @@ module XMigra
412
572
  path || self.path
413
573
  ) != ''
414
574
  end
575
+
576
+ class RepoStoredMigrationChain < MigrationChain
577
+ def initialize(branch, path, options={})
578
+ @branch = branch
579
+ options[:vcs_specifics] = GitSpecifics
580
+ super(path, options)
581
+ end
582
+
583
+ protected
584
+ def yaml_of_file(fpath)
585
+ fdir, fname = Pathname(fpath).split
586
+ file_contents = Dir.chdir(fdir) do |pwd|
587
+ GitSpecifics.run_git(:show, "#{@branch}:./#{fname}")
588
+ end
589
+ begin
590
+ YAML.load(file_contents, fpath.to_s)
591
+ rescue
592
+ raise XMigra::Error, "Error loading/parsing #{fpath}"
593
+ end
594
+ end
595
+ end
415
596
  end
416
597
  end
@@ -31,7 +31,7 @@ module XMigra
31
31
  )
32
32
  cmd_str = cmd_parts.join(' ')
33
33
 
34
- output = `#{cmd_str}`
34
+ output = `#{cmd_str} 2>/dev/null`
35
35
  raise(VersionControlError, "Subversion command failed with exit code #{$?.exitstatus}") unless $?.success?
36
36
  return output if raw_result && !no_result
37
37
  return REXML::Document.new(output) unless no_result
@@ -222,6 +222,75 @@ END_OF_MESSAGE
222
222
  subversion(:resolve, '--accept=working', path, :get_result=>false)
223
223
  end
224
224
 
225
+ def vcs_uncommitted?
226
+ status = subversion_retrieve_status(file_path).elements['entry/wc-status']
227
+ status.nil? || status.attributes['item'] == 'unversioned'
228
+ end
229
+
230
+ class VersionComparator
231
+ # vcs_object.kind_of?(SubversionSpecifics)
232
+ def initialize(vcs_object, options={})
233
+ @object = vcs_object
234
+ @expected_content_method = options[:expected_content_method]
235
+ @path_status = Hash.new do |h, file_path|
236
+ file_path = Pathname(file_path).expand_path
237
+ next h[file_path] if h.has_key?(file_path)
238
+ h[file_path] = @object.subversion_retrieve_status(file_path)
239
+ end
240
+ end
241
+
242
+ def relative_version(file_path)
243
+ # Comparing @object.file_path (a) to file_path (b)
244
+ #
245
+ # returns: :newer, :equal, :older, or :missing
246
+
247
+ b_status = @path_status[file_path].elements['entry/wc-status']
248
+
249
+ return :missing if b_status.nil? || ['deleted', 'missing'].include?(b_status.attributes['item'])
250
+
251
+ a_status = @path_status[@object.file_path].elements['entry/wc-status']
252
+
253
+ if ['unversioned', 'added'].include? a_status.attributes['item']
254
+ if ['unversioned', 'added', 'modified'].include? b_status.attributes['item']
255
+ return relative_version_by_content(file_path)
256
+ end
257
+
258
+ return :older
259
+ elsif a_status.attributes['item'] == 'normal'
260
+ return :newer unless b_status.attributes['item'] == 'normal'
261
+
262
+ return begin
263
+ a_revision = a_status.elements['commit'].attributes['revision'].to_i
264
+ b_revision = b_status.elements['commit'].attributes['revision'].to_i
265
+
266
+ if a_revision < b_revision
267
+ :newer
268
+ elsif b_revision < a_revision
269
+ :older
270
+ else
271
+ :equal
272
+ end
273
+ end
274
+ elsif b_status.attributes['item'] == 'normal'
275
+ return :older
276
+ else
277
+ return relative_version_by_content(file_path)
278
+ end
279
+ end
280
+
281
+ def relative_version_by_content(file_path)
282
+ ec_method = @expected_content_method
283
+ if !ec_method || @object.send(ec_method, file_path)
284
+ return :equal
285
+ else
286
+ return :newer
287
+ end
288
+ end
289
+ end
290
+
291
+ def vcs_comparator(options={})
292
+ VersionComparator.new(self, options)
293
+ end
225
294
 
226
295
  def vcs_move(old_path, new_path)
227
296
  subversion(:move, old_path, new_path, :get_result=>false)
@@ -264,6 +333,43 @@ END_OF_MESSAGE
264
333
  subversion(:cat, "-r#{tracer.earliest_loaded_revision}", path.to_s)
265
334
  end
266
335
  end
336
+
337
+ def vcs_contents(path, options={})
338
+ args = []
339
+
340
+ if options[:revision]
341
+ args << "-r#{options[:revision]}"
342
+ end
343
+
344
+ args << path.to_s
345
+
346
+ subversion(:cat, *args)
347
+ end
348
+
349
+ def vcs_latest_revision(a_file=nil)
350
+ if a_file.nil? && defined? @vcs_latest_revision
351
+ return @vcs_latest_revision
352
+ end
353
+
354
+ val = subversion(:status, '-v', a_file || file_path).elements[
355
+ 'string(status/target/entry/wc-status/commit/@revision)'
356
+ ]
357
+ (val.nil? ? val : val.to_i).tap do |val|
358
+ @vcs_latest_revision = val if a_file.nil?
359
+ end
360
+ end
361
+
362
+ def vcs_changes_from(from_revision, file_path)
363
+ subversion(:diff, '-r', from_revision, file_path, :raw=>true)
364
+ end
365
+
366
+ def vcs_most_recent_committed_contents(file_path)
367
+ subversion(:cat, file_path)
368
+ end
369
+
370
+ def subversion_retrieve_status(file_path)
371
+ subversion(:status, '-v', file_path).elements['status/target']
372
+ end
267
373
  end
268
374
 
269
375
  class SvnHistoryTracer
@@ -1,3 +1,3 @@
1
1
  module XMigra
2
- VERSION = "1.5.1"
2
+ VERSION = "1.6.0"
3
3
  end
data/lib/xmigra.rb CHANGED
@@ -251,8 +251,47 @@ module XMigra
251
251
  end
252
252
  end
253
253
 
254
- def secure_digest(s)
255
- [Digest::MD5.digest(s)].pack('m0').chomp
254
+ def secure_digest(s, options={:encoding=>:base64})
255
+ digest_value = Digest::MD5.digest(s)
256
+ case options[:encoding]
257
+ when nil
258
+ digest_value
259
+ when :base64
260
+ [digest_value].pack('m0').chomp
261
+ when :base32
262
+ base32encoding(digest_value)
263
+ end
264
+ end
265
+
266
+ BASE_32_ENCODING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
267
+ def base32encoding(bytes)
268
+ carry = 0
269
+ carry_bits = 0
270
+ ''.tap do |result|
271
+ bytes.each_byte do |b|
272
+ # From b we need the (5 - carry_bits) most significant bits
273
+ needed_bits = 5 - carry_bits
274
+ code_unit = (carry << needed_bits) | b >> (8 - needed_bits)
275
+ result << BASE_32_ENCODING[code_unit]
276
+
277
+ if needed_bits <= 3
278
+ # Extra character out of this byte
279
+ code_unit = (b >> (3 - needed_bits)) & 0x1F
280
+ result << BASE_32_ENCODING[code_unit]
281
+ carry_bits = (3 - needed_bits)
282
+ else
283
+ carry_bits = 8 - needed_bits
284
+ end
285
+ carry = b & ((1 << carry_bits) - 1)
286
+ end
287
+
288
+ if carry_bits > 0
289
+ code_unit = carry << (5 - carry_bits)
290
+ result << BASE_32_ENCODING[code_unit]
291
+ end
292
+
293
+ result << '=' * (7 - ((result.length + 7) % 8))
294
+ end
256
295
  end
257
296
  end
258
297
 
@@ -300,6 +339,9 @@ module XMigra
300
339
  ARGV,
301
340
  :error=>proc do |e|
302
341
  STDERR.puts("#{e} (#{e.class})") unless e.is_a?(XMigra::Program::QuietError)
342
+ if e.class.const_defined? :COMMAND_LINE_HELP
343
+ STDERR.puts(XMigra.program_message(e.class::COMMAND_LINE_HELP))
344
+ end
303
345
  exit(2) if e.is_a?(OptionParser::ParseError)
304
346
  exit(2) if e.is_a?(XMigra::Program::ArgumentError)
305
347
  exit(1)
@@ -322,6 +364,7 @@ require 'xmigra/function'
322
364
  require 'xmigra/plugin'
323
365
 
324
366
  require 'xmigra/access_artifact_collection'
367
+ require 'xmigra/impdecl_migration_adder'
325
368
  require 'xmigra/index'
326
369
  require 'xmigra/index_collection'
327
370
  require 'xmigra/migration'
@@ -331,6 +374,8 @@ require 'xmigra/branch_upgrade'
331
374
  require 'xmigra/schema_manipulator'
332
375
  require 'xmigra/schema_updater'
333
376
  require 'xmigra/new_migration_adder'
377
+ require 'xmigra/new_index_adder'
378
+ require 'xmigra/new_access_artifact_adder'
334
379
  require 'xmigra/permission_script_writer'
335
380
  require 'xmigra/source_tree_initializer'
336
381
 
data/test/git_vcs.rb CHANGED
@@ -167,10 +167,6 @@ if GIT_PRESENT
167
167
 
168
168
  `git clone "#{upstream.expand_path}" "#{repo}" 2>/dev/null`
169
169
 
170
- Dir.chdir(upstream) do
171
- commit_a_migration "foo table"
172
- end
173
-
174
170
  Dir.chdir(repo) do
175
171
  commit_a_migration "bar table"
176
172
 
@@ -268,4 +264,68 @@ if GIT_PRESENT
268
264
  end
269
265
  end
270
266
  end
267
+
268
+ run_test "XMigra does not put grants in upgrade by default" do
269
+ 1.temp_dirs do |repo|
270
+ initialize_git_repo(repo)
271
+
272
+ Dir.chdir(repo) do
273
+ commit_a_migration "first table"
274
+ File.open(XMigra::SchemaManipulator::PERMISSIONS_FILE, 'w') do |grants_file|
275
+ YAML.dump(
276
+ {
277
+ "foo" => {
278
+ "alice" => "ALL",
279
+ "bob" => "SELECT",
280
+ "candace" => ["INSERT", "SELECT", "UPDATE"],
281
+ },
282
+ },
283
+ grants_file
284
+ )
285
+ end
286
+
287
+ XMigra::SchemaUpdater.new('.').tap do |tool|
288
+ sql = tool.update_sql
289
+ assert_not_include sql, /GRANT\s+ALL.*?alice/
290
+ assert_not_include sql, /GRANT\s+SELECT.*?bob/
291
+ assert_not_include sql, /GRANT\s+INSERT.*?candace/
292
+ assert_not_include sql, /GRANT\s+SELECT.*?candace/
293
+ assert_not_include sql, /GRANT\s+UPDATE.*?candace/
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ run_test "XMigra puts grants in upgrade when requested" do
300
+ 1.temp_dirs do |repo|
301
+ initialize_git_repo(repo)
302
+
303
+ Dir.chdir(repo) do
304
+ commit_a_migration "first table"
305
+ File.open(XMigra::SchemaManipulator::PERMISSIONS_FILE, 'w') do |grants_file|
306
+ YAML.dump(
307
+ {
308
+ "foo" => {
309
+ "alice" => "ALL",
310
+ "bob" => "SELECT",
311
+ "candace" => ["INSERT", "SELECT", "UPDATE"],
312
+ },
313
+ },
314
+ grants_file
315
+ )
316
+ end
317
+
318
+ XMigra::SchemaUpdater.new('.').tap do |tool|
319
+ tool.include_grants = true
320
+
321
+ sql = tool.update_sql
322
+ assert_include sql, /GRANT\s+ALL.*?alice/
323
+ assert_include sql, /GRANT\s+SELECT.*?bob/
324
+ assert_include sql, /GRANT\s+INSERT.*?candace/
325
+ assert_include sql, /GRANT.*?SELECT.*?candace/
326
+ assert_include sql, /GRANT.*?UPDATE.*?candace/
327
+ end
328
+ end
329
+ end
330
+ end
271
331
  end
data/test/new_files.rb ADDED
@@ -0,0 +1,14 @@
1
+
2
+ XMigra::DatabaseSupportModules.each do |db_module|
3
+ ["migration", "index", "view", "function", "procedure"].each do |file_type_flag|
4
+ run_test "#{db_module::SYSTEM_NAME} support for new #{file_type_flag} file" do
5
+ in_xmigra_schema(:db_info=>{'system'=>db_module::SYSTEM_NAME}) do
6
+ begin
7
+ XMigra::Program.run(["new", "--no-edit", "--#{file_type_flag}", "foobarbaz"])
8
+ rescue XMigra::NewAccessArtifactAdder::UnsupportedArtifactType
9
+ # This is an acceptable error
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
data/test/runner.rb CHANGED
@@ -8,6 +8,8 @@ require 'tmpdir'
8
8
  TESTS = %w[
9
9
  git_vcs
10
10
  reversions
11
+ new_files
12
+ structure_declarative
11
13
  ]
12
14
 
13
15
  $:.unshift Pathname(__FILE__).expand_path.dirname.dirname + 'lib'
@@ -38,7 +40,13 @@ def run_test(name, &block)
38
40
 
39
41
  $test_count += 1
40
42
 
43
+ msg_receiver, msg_sender = IO.pipe
44
+
41
45
  if child_pid = Process.fork
46
+ msg_sender.close
47
+
48
+ # Must read to EOF or child may hang if pipe is filled
49
+ test_message = msg_receiver.read
42
50
  Process.wait(child_pid)
43
51
 
44
52
  if $?.success?
@@ -48,7 +56,14 @@ def run_test(name, &block)
48
56
  print 'F'
49
57
  $tests_failed << name
50
58
  end
59
+
60
+ if test_message.length > 0
61
+ ($test_messages ||= {})[name] = test_message
62
+ end
63
+ msg_receiver.close
51
64
  else
65
+ msg_receiver.close
66
+
52
67
  begin
53
68
  prev_stdout = $stdout
54
69
  $stdout = StringIO.new
@@ -59,11 +74,16 @@ def run_test(name, &block)
59
74
  end
60
75
  exit! 0
61
76
  rescue AssertionFailure
77
+ msg_sender.puts $!
62
78
  exit! 2
63
79
  rescue
64
- puts
65
- puts "Exception: #{$!}"
66
- puts $!.backtrace
80
+ msg_sender.puts "#{$!.class}: #{$!}"
81
+ msg_sender.puts $!.backtrace
82
+ $!.each_causing_exception do |ex|
83
+ msg_sender.puts
84
+ msg_sender.puts "Caused by #{ex.class}: #{ex}"
85
+ msg_sender.puts ex.backtrace
86
+ end
67
87
  exit! 1
68
88
  end
69
89
  end
@@ -96,8 +116,25 @@ def assert_neq(actual, expected)
96
116
  assert(proc {"Value #{actual.inspect} was unexpected"}) {actual != expected}
97
117
  end
98
118
 
119
+ def include_test(container, item)
120
+ case
121
+ when item.kind_of?(Regexp) && container.kind_of?(String)
122
+ item.match container
123
+ else
124
+ container.include? item
125
+ end
126
+ end
127
+
99
128
  def assert_include(container, item)
100
- assert(proc {"#{item.inspect} was not in #{container.inspect}"}) {container.include? item}
129
+ assert(proc {"#{item.inspect} was not in #{container.inspect}"}) do
130
+ include_test container, item
131
+ end
132
+ end
133
+
134
+ def assert_not_include(container, item)
135
+ assert(proc {"#{item.inspect} was in #{container.inspect}"}) do
136
+ !include_test container, item
137
+ end
101
138
  end
102
139
 
103
140
  def assert_raises(expected_exception)
@@ -155,3 +192,11 @@ puts
155
192
  puts "#{$test_successes}/#{$test_count} succeeded" if $test_options.show_counts
156
193
  puts "Failed tests:" unless $tests_failed.empty?
157
194
  $tests_failed.each {|name| puts " #{name}"}
195
+
196
+ ($test_messages || {}).each_pair do |test_name, message|
197
+ puts
198
+ puts "----- #{test_name} -----"
199
+ message.each_line do |msg_line|
200
+ puts msg_line.chomp
201
+ end
202
+ end