Module: Subversion

Includes:
Extensions
Defined in:
lib/subwrap/subversion.rb,
lib/subwrap/subversion.rb,
lib/subwrap/svn_command.rb,
lib/subwrap/svn_command.rb,
lib/subwrap/svn_command.rb,
lib/subwrap/svn_command.rb,
lib/subwrap/subversion_extensions.rb

Overview

These are methods used by the SvnCommand for filtering and whatever else it needs… It could probably be moved into SvnCommand, but I thought it might be good to at least make it possible to use them apart from SvnCommand. Rename to Subversion::Filters ? Then each_unadded would be an odd man out.

Defined Under Namespace

Modules: Extensions Classes: Diff, Diffs, DiffsParser, ExternalsContainer, RevisionProperty, SvnCommand

Constant Summary collapse

@@color =

True if you want output from svn to be colorized (useful if output is for human eyes, but not useful if using the output programatically)

false
@@dry_run =

If true, will only output which command would have been executed but will not actually execute it.

false
false
@@cached_commands =
{}

Constants included from Extensions

Extensions::Interesting_status_flags, Extensions::Status_flags, Extensions::Uninteresting_status_flags

Class Method Summary collapse

Class Method Details

.add(*args) ⇒ Object

Adds the given items to the repository. Items may contain wildcards.



63
64
65
# File 'lib/subwrap/subversion.rb', line 63

def self.add(*args)
  execute "add #{args.join ' '}"
end

.add_to_property(property, path, *new_lines) ⇒ Object

It’s easy to get/set properties, but less easy to add to a property. This method uses get/set to simulate add. It will uniquify lines, removing duplicates. (:todo: what if we want to set a property to have some duplicate lines?)



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/subwrap/subversion.rb', line 258

def self.add_to_property(property, path, *new_lines)
  # :todo: I think it's possible to have properties other than svn:* ... so if property contains a prefix (something:), use it; else default to 'svn:'
  
  # Get the current properties
  lines = self.get_property(property, path).split "\n"
  puts "Existing lines: #{lines.inspect}" if $debug

  # Add the new lines, delete empty lines, and uniqueify all elements
  lines.concat(new_lines).uniq!
  puts "After concat(new_lines).uniq!: #{lines.inspect}" if $debug

  lines.delete ''
  # Set the property
  puts "About to set propety to: #{lines.inspect}" if $debug
  self.set_property property, lines.join("\n"), path
end

.base_url(path_or_url = './') ⇒ Object

:todo: needs some serious unit-testing love



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/subwrap/subversion.rb', line 379

def self.base_url(path_or_url = './')
  matches = info(path_or_url).match(/^Repository Root: (.+)/)
  matches && matches[1]

   # It appears that we might need to use this old way (which looks at 'URL'), since there is actually a handy property called "Repository Root" that we can look at.
#    base_url = nil    # needed so that base_url variable isn't local to loop block (and reset during next iteration)!
#    started_using_dot_dots = false
#    loop do
#      matches = /^URL: (.+)/.match(info(path_or_url))
#      if matches && matches[1]
#        base_url = matches[1]
#      else
#        break base_url
#      end
#
#      # Keep going up the path, one directory at a time, until `svn info` no longer returns a URL (will probably eventually return 'svn: PROPFIND request failed')
#      if path_or_url.include?('/') && !started_using_dot_dots
#        path_or_url = File.dirname(path_or_url)
#      else
#        started_using_dot_dots = true
#        path_or_url = File.join(path_or_url, '..')
#      end
#      #puts 'going up to ' + path_or_url
#    end
end

.cat(*args) ⇒ Object



251
252
253
254
# File 'lib/subwrap/subversion.rb', line 251

def self.cat(*args)
  args = ['./'] if args.empty?
  execute("cat #{args.join ' '}")
end

.commit(*args) ⇒ Object



177
178
179
180
# File 'lib/subwrap/subversion.rb', line 177

def self.commit(*args)
  args = ['./'] if args.empty?
  execute("commit #{args.join ' '}")
end

.delete_property(property, path = './') ⇒ Object



283
284
285
# File 'lib/subwrap/subversion.rb', line 283

def self.delete_property(property, path = './')
  execute "propdel svn:#{property} #{path}"
end

.delete_revision_property(property_name, rev) ⇒ Object



286
287
288
# File 'lib/subwrap/subversion.rb', line 286

def self.delete_revision_property(property_name, rev)
  execute("propdel --revprop #{property_name} -r #{rev}").chomp
end

.diff(*args) ⇒ Object

Returns the modifications to the working directory or URL specified in args.



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/subwrap/subversion.rb', line 220

def self.diff(*args)
  args = ['./'] if args.empty?
  diff = execute("diff #{"--diff-cmd colordiff" if color?} #{args.join ' '}")

  # Fix annoyance: You can't seem to do a diff on a file that was *added*. If you do -r 1:2 for a file that was *added* in 2, it will say it can't find the repository location for that file in r1.
  if diff =~ /Unable to find repository location for '.*' in revision/ and @allow_diffs_for_added_files != false
    args.map!(&:to_s)
    args.map_with_index! do |arg, i| 
      if args[i-1].in? ['--revision', '-r']
        arg.gsub(/\d+:/, '') 
      elsif arg.in? ['--change', '-c']
        arg.gsub(/-c|--change/, '-r')
      else
        arg
      end
    end
    diff = execute("cat #{args.join ' '}") #.to_enum(:each_line).map(&:chomp).map(&:green).join("\n")
  end

  diff
end

.diffs(*args) ⇒ Object

Parses the output from diff and returns an array of Diff objects.



242
243
244
245
246
247
248
249
# File 'lib/subwrap/subversion.rb', line 242

def self.diffs(*args)
  args = ['./'] if args.empty?
  raw_diffs = nil
  with_color! false do
    raw_diffs = diff(*args)
  end
  DiffsParser.new(raw_diffs).parse
end

.executableObject

The location of the executable to be used to do: Is there a smarter/faster way to do this? (Could cache this result in .subwrap or somewhere, so we don’t have to do all this work on every invocation…)



442
443
444
445
446
447
448
449
450
451
452
# File 'lib/subwrap/subversion.rb', line 442

def self.executable
  # FileUtils.which('svn') would return our Ruby *wrapper* script for svn. We actually want to return here the binary/executable that we are
  # *wrapping* so we have to use whereis and then use the first one that is ''not'' a Ruby script.
  @@executable ||=
    FileUtils.whereis('svn') do |executable|
      if !self.ruby_script?(executable)               # We want to wrap the svn binary provided by Subversion, not our custom replacement for that.
        return windows_platform? ? %{"#{executable}"} : executable
      end
    end
  raise 'svn binary not found'
end

.export(path_or_url, target) ⇒ Object



108
109
110
# File 'lib/subwrap/subversion.rb', line 108

def self.export(path_or_url, target)
  execute "export #{path_or_url} #{target}"
end

.externalize(repo_url, options = {}) ⇒ Object

Adds the given repository URL (svn.yourcompany.com/path/to/something) as an svn:externals.

Options may include:

  • :as - overrides the default behavior of naming the checkout based on the last component of the repo path

  • :local_path - specifies where to set the externals property. Defaults to ‘.’ or the dirname of as if as is specified (for example, vendor/plugins if as is vendor/plugins/plugin_name).



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/subwrap/subversion.rb', line 93

def self.externalize(repo_url, options = {})

  options[:as] ||= File.basename(repo_url)
  #options[:as] = options[:as].ljust(29)

  # You can't set the externals of './' to 'vendor/plugins/foo http://example.com/foo'
  # Instead, you have to set the externals of 'vendor/plugins/' to 'foo http://example.com/foo'
  # This will make that correction for you automatically.
  options[:local_path] ||= File.dirname(options[:as])   # Will be '.' if options[:as] has no dirname component.
                                                        # Will be 'vendor/plugins' if options[:as] is 'vendor/plugins/plugin_name'.
  options[:as] = File.basename(options[:as])

  add_to_property 'externals', options[:local_path], "#{options[:as]} #{repo_url}"
end

.externals_containers(path = './') ⇒ Object

Returns an array of ExternalsContainer objects representing all externals containers in the working directory specified by path.



208
209
210
211
212
213
214
215
216
217
# File 'lib/subwrap/subversion.rb', line 208

def self.externals_containers(path = './')
  # Using self.externals_items is kind of a cheap way to do this, and it results in some redundancy that we have to filter out
  # (using uniq_by), but it seemed more efficient than the alternative (traversing the entire directory tree and querying for
  # `svn prepget svn:externals` at each stop to see if the directory is an externals container).
  self.externals_items(path).map { |external_dir|
    ExternalsContainer.new(external_dir + '/..')
  }.uniq_by { |external|
    external.container_dir
  }
end

.externals_items(path = './') ⇒ Object

Returns an array of externals items. These are the actual externals listed in an svn:externals property. Example:

vendor/a
vendor/b

Where ‘vendor’ is an ExternalsContainer containing external items ‘a’ and ‘b’.



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/subwrap/subversion.rb', line 195

def self.externals_items(path = './')
  status = status_the_section_before_externals(path)
  return [] if status.nil?
  status.select { |line|
    line =~ /^X/
  }.map { |line|
    # Just keep the filename part
    line =~ /^X\s+(.+)/
    $1
  }
end

.get_property(property, path = './') ⇒ Object

:todo: Stop assuming the svn: namespace. What’s the point of a namespace if you only allow one of them?



276
277
278
# File 'lib/subwrap/subversion.rb', line 276

def self.get_property(property, path = './')
  execute "propget svn:#{property} #{path}"
end

.get_revision_property(property_name, rev) ⇒ Object



279
280
281
# File 'lib/subwrap/subversion.rb', line 279

def self.get_revision_property(property_name, rev)
  execute("propget --revprop #{property_name} -r #{rev}").chomp
end

.help(*args) ⇒ Object



321
322
323
# File 'lib/subwrap/subversion.rb', line 321

def self.help(*args)
  execute "help #{args.join(' ')}"
end

.ignore(*patterns) ⇒ Object

Sets the svn:ignore property based on the given patterns. Each pattern is both the path (where the property gets set) and the property itself. For instance:

"log/*.log" would add "*.log" to the svn:ignore property on the log/ directory.
"log" would add "log" to the svn:ignore property on the ./ directory.


72
73
74
75
76
77
78
79
80
81
# File 'lib/subwrap/subversion.rb', line 72

def self.ignore(*patterns)
  
  patterns.each do |pattern|
    path = File.dirname(pattern)
    path += '/' if path == '.'
    pattern = File.basename(pattern)
    add_to_property 'ignore', path, pattern
  end
  nil
end

.info(*args) ⇒ Object



368
369
370
371
# File 'lib/subwrap/subversion.rb', line 368

def self.info(*args)
  args = ['./'] if args.empty?
  execute "info #{args.join(' ')}"
end

.latest_revision(path = './') ⇒ Object

Returns the revision number for head.



332
333
334
335
336
337
338
339
340
# File 'lib/subwrap/subversion.rb', line 332

def self.latest_revision(path = './')
  (cached = @@cached_commands[:latest_revision][path]) and return cached
  url = url(path)

  #puts "Fetching latest revision from repository: #{url}"
  result = latest_revision_for_path(url).to_i
  @@cached_commands[:latest_revision][path] = result
  result
end

.latest_revision_for_path(path) ⇒ Object

Returns the revision number for the working directory(/file?) specified by path



343
344
345
346
347
348
349
350
351
# File 'lib/subwrap/subversion.rb', line 343

def self.latest_revision_for_path(path)
  # The revision returned by svn info seems to be a pretty reliable way to get this. Does anyone know of a better way?
  matches = info(path).match(/^Revision: (\d+)/)
  if matches
    matches[1].to_i
  else
    raise "Could not extract revision from #{info(path)}"
  end
end

.log(*args) ⇒ Object

Returns the raw output from svn log



326
327
328
329
# File 'lib/subwrap/subversion.rb', line 326

def self.log(*args)
  args = ['./'] if args.empty?
  execute "log #{args.join(' ')}"
end

.make_directory(dir) ⇒ Object



317
318
319
# File 'lib/subwrap/subversion.rb', line 317

def self.make_directory(dir)
  execute "mkdir #{dir}"
end

.make_executable(*paths) ⇒ Object

Marks the given items as being executable. Items may not contain wildcards.



150
151
152
153
154
# File 'lib/subwrap/subversion.rb', line 150

def self.make_executable(*paths)
  paths.each do |path|
    self.set_property 'executable', '', path
  end
end

.make_not_executable(*paths) ⇒ Object



155
156
157
158
159
# File 'lib/subwrap/subversion.rb', line 155

def self.make_not_executable(*paths)
  paths.each do |path|
    self.delete_property 'executable', path
  end
end

.proplist(rev) ⇒ Object

Gets raw output of proplist command



298
299
300
# File 'lib/subwrap/subversion.rb', line 298

def self.proplist(rev)
  execute("proplist --revprop -r #{rev}")
end

.remove(*args) ⇒ Object

Removes the given items from the repository and the disk. Items may contain wildcards.



113
114
115
# File 'lib/subwrap/subversion.rb', line 113

def self.remove(*args)
  execute "rm #{args.join ' '}"
end

.remove_force(*args) ⇒ Object

Removes the given items from the repository and the disk. Items may contain wildcards. To do: add a :force => true option to remove



119
120
121
# File 'lib/subwrap/subversion.rb', line 119

def self.remove_force(*args)
  execute "rm --force #{args.join ' '}"
end

.remove_without_delete(*args) ⇒ Object

Removes the given items from the repository BUT NOT THE DISK. Items may contain wildcards.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/subwrap/subversion.rb', line 124

def self.remove_without_delete(*args)
  # resolve the wildcards before iterating
  args.collect {|path| Dir[path]}.flatten.each do |path|
    entries_file = "#{File.dirname(path)}/.svn/entries"
    File.chmod(0644, entries_file)

    xmldoc = REXML::Document.new(IO.read(entries_file))
    # first attempt to delete a matching entry with schedule == add
    unless xmldoc.root.elements.delete "//entry[@name='#{File.basename(path)}'][@schedule='add']"
      # then attempt to alter a missing schedule to schedule=delete
      entry = REXML::XPath.first(xmldoc, "//entry[@name='#{File.basename(path)}']")
      entry.attributes['schedule'] ||= 'delete' if entry
    end
    # write back to the file
    File.open(entries_file, 'w') { |f| xmldoc.write f, 0 }

    File.chmod(0444, entries_file)
  end
end

.repository_root(*args) ⇒ Object



405
# File 'lib/subwrap/subversion.rb', line 405

def self.repository_root(*args); base_url(*args); end

.repository_uuid(path_or_url = './') ⇒ Object



408
409
410
411
# File 'lib/subwrap/subversion.rb', line 408

def self.repository_uuid(path_or_url = './')
  matches = info(path_or_url).match(/^Repository UUID: (.+)/)
  matches && matches[1]
end

.revert(*args) ⇒ Object

Reverts the given items in the working copy. Items may contain wildcards.



145
146
147
# File 'lib/subwrap/subversion.rb', line 145

def self.revert(*args)
  execute "revert #{args.join ' '}"
end

.revision_properties(rev) ⇒ Object

Returns an array of RevisionProperty objects (name, value) for revisions currently set on the given rev Tessted by: ../../test/subversion_test.rb:test_revision_properties



311
312
313
314
315
# File 'lib/subwrap/subversion.rb', line 311

def self.revision_properties(rev)
  revision_properties_names(rev).map { |property_name|
    RevisionProperty.new(property_name, get_revision_property(property_name, rev))
  }
end

.revision_properties_names(rev) ⇒ Object

Returns an array of the names of all revision properties currently set on the given rev Tessted by: ../../test/subversion_test.rb:test_revision_properties_names



303
304
305
306
307
308
# File 'lib/subwrap/subversion.rb', line 303

def self.revision_properties_names(rev)
  raw_list = proplist(rev)
  raw_list.scan(/^ +([^ ]+)$/).map { |matches|
    matches.first.chomp
  }
end

.revisions(*args) ⇒ Object

Returns an array of RSCM::Revision objects



354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/subwrap/subversion.rb', line 354

def self.revisions(*args)
  # Tried using this, but it seems to expect you to pass in a starting date or accept the default starting date of right now, which is silly if you actually just want *all* revisions...
  #@rscm = ::RSCM::Subversion.new
  #@rscm.revisions

  args = (['-v'] + args)
  log_output = Subversion.log(*args)
  parser = ::RSCM::SubversionLogParser.new(io = StringIO.new(log_output), url = 'http://ignore.me.com')
  # :todo: svn revisions -r 747 -- chops off line
  revisions = parser.parse_revisions
  revisions
end

.root_url(*args) ⇒ Object

base_url = nil # needed so that base_url variable isn’t local to loop block (and reset during next iteration)!

started_using_dot_dots = false
loop do
  matches = /^URL: (.+)/.match(info(path_or_url))
  if matches && matches[1]
    base_url = matches[1]
  else
    break base_url
  end

  # Keep going up the path, one directory at a time, until `svn info` no longer returns a URL (will probably eventually return 'svn: PROPFIND request failed')
  if path_or_url.include?('/') && !started_using_dot_dots
    path_or_url = File.dirname(path_or_url)
  else
    started_using_dot_dots = true
    path_or_url = File.join(path_or_url, '..')
  end
  #puts 'going up to ' + path_or_url
end


404
# File 'lib/subwrap/subversion.rb', line 404

def self.root_url(*args);        base_url(*args); end

.ruby_script?(file_path) ⇒ Boolean

Returns:

  • (Boolean)


454
455
456
457
458
459
460
461
# File 'lib/subwrap/subversion.rb', line 454

def self.ruby_script?(file_path)
  if windows_platform?
    # The 'file' command, we assume, is not available
    File.readlines(file_path)[0] =~ /ruby/
  else
    `file #{file_path}` =~ /ruby/
  end
end

.set_property(property, value, path = './') ⇒ Object



290
291
292
# File 'lib/subwrap/subversion.rb', line 290

def self.set_property(property, value, path = './')
  execute "propset svn:#{property} '#{value}' #{path}"
end

.set_revision_property(property_name, rev) ⇒ Object



293
294
295
# File 'lib/subwrap/subversion.rb', line 293

def self.set_revision_property(property_name, rev)
  execute("propset --revprop #{property_name} -r #{rev}").chomp
end

.status(*args) ⇒ Object

Returns the status of items in the working directories paths. Returns the raw output from svn (use split("\n") if you want an array).



162
163
164
165
# File 'lib/subwrap/subversion.rb', line 162

def self.status(*args)
  args = ['./'] if args.empty?
  execute("status #{args.join ' '}")
end

.status_against_server(*args) ⇒ Object



167
168
169
170
# File 'lib/subwrap/subversion.rb', line 167

def self.status_against_server(*args)
  args = ['./'] if args.empty?
  self.status('-u', *args)
end

.status_the_section_before_externals(path = './') ⇒ Object

The output from ‘svn status` is nicely divided into two “sections”: the section which pertains to the current working copy (not counting externals as part of the working copy) and then the section with status of all of the externals. This method returns the first section.



185
186
187
188
# File 'lib/subwrap/subversion.rb', line 185

def self.status_the_section_before_externals(path = './')
  status = status(path) || ''
  status.sub!(/(Performing status.*)/m, '')
end

.under_version_control?(file = './', strict = false) ⇒ Boolean

By default, if you query a directory that is scheduled for addition but hasn’t been committed yet (node doesn’t have a UUID), then we will still return true, because it is scheduled to be under version control. If you want a stricter definition, and only want it to return true if the file exists in the repository (has a UUID)@ then pass strict = true

Returns:

  • (Boolean)


416
417
418
419
420
421
422
# File 'lib/subwrap/subversion.rb', line 416

def self.under_version_control?(file = './', strict = false)
  if strict
    !!repository_uuid(file)
  else # (scheduled_for_addition_counts_as_true)
    !!url(file)
  end
end

.unignore(*patterns) ⇒ Object

Raises:

  • (NotImplementedError)


82
83
84
# File 'lib/subwrap/subversion.rb', line 82

def self.unignore(*patterns)
  raise NotImplementedError
end

.update(*args) ⇒ Object



172
173
174
175
# File 'lib/subwrap/subversion.rb', line 172

def self.update(*args)
  args = ['./'] if args.empty?
  execute("update #{args.join ' '}")
end

.url(path_or_url = './') ⇒ Object



373
374
375
376
# File 'lib/subwrap/subversion.rb', line 373

def self.url(path_or_url = './')
  matches = info(path_or_url).match(/^URL: (.+)/)
  matches && matches[1]
end

.working_copy_root(directory = './') ⇒ Object



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/subwrap/subversion.rb', line 423

def self.working_copy_root(directory = './')
  uuid = repository_uuid(directory)
  return nil if uuid.nil?

  loop do
    # Keep going up, one level at a time, ...
    new_directory = File.expand_path(File.join(directory, '..'))
    new_uuid = repository_uuid(new_directory)

    # Until we get back a uuid that is nil (it's not a working copy at all) or different (you can have a working copy A inside of a different WC B)...
    break if new_uuid.nil? or new_uuid != uuid

    directory = new_directory
  end
  directory
end