Module: XMigra::SubversionSpecifics

Included in:
SvnHistoryTracer
Defined in:
lib/xmigra/vcs_support/svn.rb

Defined Under Namespace

Classes: VersionComparator

Constant Summary collapse

PRODUCTION_PATH_PROPERTY =
'xmigra:production-path'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.init_schema(schema_config) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/xmigra/vcs_support/svn.rb', line 40

def init_schema(schema_config)
  Console.output_section "Subversion Integration" do
    puts "Establishing a \"production pattern,\" a regular expression for"
    puts "recognizing branch identifiers of branches used for production"
    puts "script generation, simplifies the process of resolving conflicts"
    puts "in the migration chain, should any arise."
    puts
    puts "No escaping (either shell or Ruby) of the regular expression is"
    puts "necessary when entered here."
    puts
    puts "Common choices are:"
    puts "    ^trunk/"
    puts "    ^version/"
    puts
    print "Production pattern (empty to skip): "
    
    production_pattern = $stdin.gets.chomp
    return if production_pattern.empty?
    schema_config.after_dbinfo_creation do
      tool = SchemaManipulator.new(schema_config.root_path).extend(WarnToStderr)
      tool.production_pattern = production_pattern
    end
  end
end

.manages(path) ⇒ Object



10
11
12
13
14
15
16
17
18
19
# File 'lib/xmigra/vcs_support/svn.rb', line 10

def manages(path)
  begin
    return true if File.directory?(File.join(path, '.svn'))
  rescue TypeError
    return false
  end
  
  `svn info "#{path}" 2>&1`
  return $?.success?
end

.run_svn(subcmd, *args) ⇒ Object

Run the svn command line client in XML mode and return a REXML::Document



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/xmigra/vcs_support/svn.rb', line 22

def run_svn(subcmd, *args)
  options = (Hash === args[-1]) ? args.pop : {}
  no_result = !options.fetch(:get_result, true)
  raw_result = options.fetch(:raw, false) || subcmd.to_s == 'cat'
  
  cmd_parts = ["svn", subcmd.to_s]
  cmd_parts << "--xml" unless no_result || raw_result
  cmd_parts.concat(
    args.collect {|a| '""'.insert(1, a.to_s)}
  )
  cmd_str = cmd_parts.join(' ')
  
  output = `#{cmd_str} 2>#{XMigra::NULL_FILE}`
  raise(VersionControlError, "Subversion command failed with exit code #{$?.exitstatus}") unless $?.success?
  return output if raw_result && !no_result
  return REXML::Document.new(output) unless no_result
end

Instance Method Details

#branch_identifierObject



206
207
208
209
210
211
212
# File 'lib/xmigra/vcs_support/svn.rb', line 206

def branch_identifier
  return @subversion_branch_id if defined? @subversion_branch_id
  dir_info = subversion_info
  return @subversion_branch_id = dir_info.elements['info/entry/url'].text[
    dir_info.elements['info/entry/repository/root'].text.length..-1
  ]
end

#branch_useObject



192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/xmigra/vcs_support/svn.rb', line 192

def branch_use
    # Look for xmigra:production-path on the database directory (self.path)
  return nil unless prod_path_element = subversion(:propget, PRODUCTION_PATH_PROPERTY, self.path).elements['properties/target/property']
  
  prod_path_pattern = Regexp.new(prod_path_element.text)
  
  use = prod_path_pattern.match(branch_identifier) ? :production : :development
  if block_given?
    yield use
  else
    return use
  end
end

#check_working_copy!Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/xmigra/vcs_support/svn.rb', line 70

def check_working_copy!
  return unless production
  
  schema_info = subversion_info
  file_paths = Array.from_generator(method(:each_file_path))
  status = subversion(:status, '--no-ignore', path)
  unversioned_files = status.elements.each("status/target/entry/@path")
  unversioned_files = unversioned_files.collect {|a| File.expand_path(a.to_s)}
  
  unless (file_paths & unversioned_files).empty?
    raise VersionControlError, "Some source files are not versions found in the repository"
  end
  status = nil
  
  wc_rev = {}
  working_rev = schema_info.elements["info/entry/@revision"].value.to_i
  file_paths.each do |fp|
    fp_info = subversion(:info, fp)
    wc_rev[fp] = fp_wc_rev = fp_info.elements["info/entry/@revision"].value.to_i
    if working_rev != fp_wc_rev
      raise VersionControlError, "The working copy contains objects at multiple revisions"
    end
  end
  
  migrations.each do |m|
    fpath = m.file_path
    
    log = subversion(:log, "-r#{wc_rev[fpath]}:1", "--stop-on-copy", fpath)
    if log.elements["log/logentry[2]"]
      raise VersionControlError, "'#{fpath}' has been modified in the repository since it was created or copied"
    end
  end
  
  # Since a production script was requested, warn if we are not generating
  # from a production branch
  if branch_use != :production and self.respond_to? :warning
    self.warning(<<END_OF_MESSAGE)
The branch backing the target working copy is not marked as a production branch.
END_OF_MESSAGE
  end
end

#get_conflict_infoObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/xmigra/vcs_support/svn.rb', line 120

def get_conflict_info
  # Check if the structure head is conflicted
  structure_dir = Pathname.new(self.path) + SchemaManipulator::STRUCTURE_SUBDIR
  status = subversion(:status, structure_dir + MigrationChain::HEAD_FILE)
  return nil if status.elements["status/target/entry/wc-status/@item"].value != "conflicted"
  
  chain_head = lambda do |extension|
    pattern = MigrationChain::HEAD_FILE + extension
    if extension.include? '*'
      files = structure_dir.glob(MigrationChain::HEAD_FILE + extension)
      raise VersionControlError, "Multiple #{pattern} files in structure directory" if files.length > 1
      raise VersionControlError, "#{pattern} file missing from structure directory" if files.length < 1
    else
      files = [structure_dir.join(pattern)]
    end
    
    # Using YAML.parse_file and YAML::Syck::Node#transform rerenders
    # scalars in the same style they were read from the source file:
    return YAML.parse_file(files[0]).transform
  end
  
  if (structure_dir + (MigrationChain::HEAD_FILE + ".working")).exist?
    # This is a merge conflict
    
    # structure/head.yaml.working is from the current branch
    # structure/head.yaml.merge-left.r* is the branch point
    # structure/head.yaml.merge-right.r* is from the merged-in branch
    this_head = chain_head.call(".working")
    other_head = chain_head.call(".merge-right.r*")
    branch_point = chain_head.call(".merge-left.r*")[MigrationChain::LATEST_CHANGE]
    
    conflict = MigrationConflict.new(structure_dir, branch_point, [other_head, this_head])
    
    branch_use {|u| conflict.branch_use = u}
  else
    # This is an update conflict
    
    # structure/head.yaml.mine is from the working copy
    # structure/head.yaml.r<lower> is the common ancestor
    # structure/head.yaml.r<higher> is the newer revision
    working_head = chain_head.call('.mine')
    oldrev, newrev = nil, 0
    structure_dir.glob(MigrationChain::HEAD_FILE + '.r*') do |fn|
      if fn.to_s =~ /.r(\d+)$/
        rev = $1.to_i
        if oldrev.nil? or rev < oldrev
          oldrev = rev
        end
        if newrev < rev
          newrev = rev
        end
      end
    end
    repo_head = chain_head.call(".r#{newrev}")
    branch_point = chain_head.call(".r#{oldrev}")[MigrationChain::LATEST_CHANGE]
    
    conflict = MigrationConflict.new(structure_dir, branch_point, [repo_head, working_head])
    branch_use {|u| conflict.branch_use = u}
    
    fix_target, = conflict.migration_tweak
    fix_target_st = subversion(:status, fix_target)
    if fix_target_st.elements['status/target/entry/wc-status/@item'].value == 'modified'
      conflict.scope = :working_copy
    end
  end
  
  tool = self
  conflict.after_fix = proc {tool.resolve_conflict!(structure_dir + MigrationChain::HEAD_FILE)}
  
  return conflict
end

#production_patternObject



214
215
216
# File 'lib/xmigra/vcs_support/svn.rb', line 214

def production_pattern
  subversion(:propget, PRODUCTION_PATH_PROPERTY, self.path, :raw=>true)
end

#production_pattern=(pattern) ⇒ Object



217
218
219
# File 'lib/xmigra/vcs_support/svn.rb', line 217

def production_pattern=(pattern)
  subversion(:propset, PRODUCTION_PATH_PROPERTY, pattern, self.path, :get_result=>false)
end

#resolve_conflict!(path) ⇒ Object



221
222
223
# File 'lib/xmigra/vcs_support/svn.rb', line 221

def resolve_conflict!(path)
  subversion(:resolve, '--accept=working', path, :get_result=>false)
end

#subversion(*args) ⇒ Object



66
67
68
# File 'lib/xmigra/vcs_support/svn.rb', line 66

def subversion(*args)
  SubversionSpecifics.run_svn(*args)
end

#subversion_infoObject



303
304
305
306
# File 'lib/xmigra/vcs_support/svn.rb', line 303

def subversion_info
  return @subversion_info if defined? @subversion_info
  return @subversion_info = subversion(:info, self.path)
end

#subversion_retrieve_status(file_path) ⇒ Object



370
371
372
# File 'lib/xmigra/vcs_support/svn.rb', line 370

def subversion_retrieve_status(file_path)
  subversion(:status, '-v', file_path).elements['status/target']
end

#vcs_changes_from(from_revision, file_path) ⇒ Object



362
363
364
# File 'lib/xmigra/vcs_support/svn.rb', line 362

def vcs_changes_from(from_revision, file_path)
  subversion(:diff, '-r', from_revision, file_path, :raw=>true)
end

#vcs_comparator(options = {}) ⇒ Object



291
292
293
# File 'lib/xmigra/vcs_support/svn.rb', line 291

def vcs_comparator(options={})
  VersionComparator.new(self, options)
end

#vcs_contents(path, options = {}) ⇒ Object



337
338
339
340
341
342
343
344
345
346
347
# File 'lib/xmigra/vcs_support/svn.rb', line 337

def vcs_contents(path, options={})
  args = []
  
  if options[:revision]
    args << "-r#{options[:revision]}"
  end
  
  args << path.to_s
  
  subversion(:cat, *args)
end

#vcs_informationObject



112
113
114
115
116
117
118
# File 'lib/xmigra/vcs_support/svn.rb', line 112

def vcs_information
  info = subversion_info
  return [
    "Repository URL: #{info.elements["info/entry/url"].text}",
    "Revision: #{info.elements["info/entry/@revision"].value}"
  ].join("\n")
end

#vcs_latest_revision(a_file = nil) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/xmigra/vcs_support/svn.rb', line 349

def vcs_latest_revision(a_file=nil)
  if a_file.nil? && defined? @vcs_latest_revision
    return @vcs_latest_revision
  end
  
  val = subversion(:status, '-v', a_file || file_path).elements[
    'string(status/target/entry/wc-status/commit/@revision)'
  ]
  (val.nil? ? val : val.to_i).tap do |val|
    @vcs_latest_revision = val if a_file.nil?
  end
end

#vcs_most_recent_committed_contents(file_path) ⇒ Object



366
367
368
# File 'lib/xmigra/vcs_support/svn.rb', line 366

def vcs_most_recent_committed_contents(file_path)
  subversion(:cat, file_path)
end

#vcs_move(old_path, new_path) ⇒ Object



295
296
297
# File 'lib/xmigra/vcs_support/svn.rb', line 295

def vcs_move(old_path, new_path)
  subversion(:move, old_path, new_path, :get_result=>false)
end

#vcs_production_contents(path) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/xmigra/vcs_support/svn.rb', line 308

def vcs_production_contents(path)
  path = Pathname(path)
  
  # Check for a production pattern.  If none exists, there is no way to
  # identify which branches are production, so essentially no production
  # content:
  prod_pat = self.production_pattern
  return nil if prod_pat.nil?
  prod_pat = Regexp.compile(prod_pat.chomp)
  
  # Is the current branch a production branch?  If so, cat the committed
  # version:
  if branch_identifier =~ prod_pat
    return svn(:cat, path.to_s)
  end
  
  # Use an SvnHistoryTracer to walk back through the history of self.path
  # looking for a copy from a production branch.
  tracer = SvnHistoryTracer.new(self.path)
  
  while !(match = tracer.earliest_loaded_repopath =~ prod_pat) && tracer.load_parent_commit
    # loop
  end
  
  if match
    subversion(:cat, "-r#{tracer.earliest_loaded_revision}", path.to_s)
  end
end

#vcs_remove(path) ⇒ Object



299
300
301
# File 'lib/xmigra/vcs_support/svn.rb', line 299

def vcs_remove(path)
  subversion(:remove, path, :get_result=>false)
end

#vcs_uncommitted?Boolean

Returns:

  • (Boolean)


225
226
227
228
# File 'lib/xmigra/vcs_support/svn.rb', line 225

def vcs_uncommitted?
  status = subversion_retrieve_status(file_path).elements['entry/wc-status']
  status.nil? || status.attributes['item'] == 'unversioned'
end