class ASF::SVN

Provide access to files stored in Subversion, generally to local working copies that are updated via cronjobs.

Note: svn paths passed to various find methods are resolved relative to https://svn.apache.org/repos/ if they are not full URIs.

Constants

REPOSITORY

path to repository.yml in the source.

Public Class Methods

[](name) click to toggle source

find a local directory corresponding to a path in Subversion. Throws an exception if not found.

# File lib/whimsy/asf/svn.rb, line 66
def self.[](name)
  self.find!(name)
end
[]=(name, path) click to toggle source

set a local directory corresponding to a path in Subversion. Useful as a test data override.

# File lib/whimsy/asf/svn.rb, line 60
def self.[]=(name, path)
  @testdata[name] = File.expand_path(path).untaint
end
find(name) click to toggle source

find a local directory corresponding to a path in Subversion. Returns nil if not found.

# File lib/whimsy/asf/svn.rb, line 72
def self.find(name)
  return @testdata[name] if @testdata[name]

  result = repos[(@mock+name.sub('private/','')).to_s.sub(/\/*$/, '')] ||
    repos[(@base+name).to_s.sub(/\/*$/, '')] # lose trailing slash

  return result if result

  # recursively try parent directory
  if name.include? '/'
    base = File.basename(name).untaint
    result = find(File.dirname(name))
    if result and File.exist?(File.join(result, base))
      File.join(result, base)
    end
  end
end
find!(name) click to toggle source

find a local directory corresponding to a path in Subversion. Throws an exception if not found.

# File lib/whimsy/asf/svn.rb, line 92
def self.find!(name)
  result = self.find(name)

  if not result
    raise Exception.new("Unable to find svn checkout for #{@base + name}")
  end

  result
end
get(path, user=nil, password=nil) click to toggle source

retrieve revision, content for a file in svn

# File lib/whimsy/asf/svn.rb, line 140
def self.get(path, user=nil, password=nil)
  path = (@base + path).to_s

  # build svn info command
  cmd = ['svn', 'info', path, '--non-interactive']

  # password was supplied, add credentials
  if password
    cmd += ['--username', user, '--password', password, '--no-auth-cache']
  end

  # default the values to return
  revision = '0'
  content = nil

  # issue svn info command
  stdout, status = Open3.capture2(*cmd)
  if status.success?
    # extract revision number
    revision = stdout[/^Revision: (\d+)/, 1]

    # extract contents
    cmd[1] = 'cat'
    content, status = Open3.capture2(*cmd)
  end

  # return results
  return revision, content
end
getInfo(path, user=nil, password=nil) click to toggle source

retrieve info, [err] for a path in svn

# File lib/whimsy/asf/svn.rb, line 104
def self.getInfo(path, user=nil, password=nil)
  return nil, 'path must not be nil' unless path

  path = (@base + path).to_s

  # build svn info command
  cmd = ['svn', 'info', path, '--non-interactive']

  # password was supplied, add credentials
  if password
    cmd += ['--username', user, '--password', password, '--no-auth-cache']
  end

  # issue svn info command
  out, err, status = Open3.capture3(*cmd)
  if status.success?
    return out
  else
    return nil, err
  end
end
getRevision(path, user=nil, password=nil) click to toggle source

retrieve revision, [err] for a path in svn

# File lib/whimsy/asf/svn.rb, line 127
def self.getRevision(path, user=nil, password=nil)
  path = (@base + path).to_s

  out, err = getInfo(path, user, password)
  if out
    # extract revision number
    return out[/^Revision: (\d+)/, 1]
  else
    return out, err
  end
end
repos() click to toggle source

a hash of local working copies of Subversion repositories. Keys are subversion paths; values are file paths.

# File lib/whimsy/asf/svn.rb, line 30
def self.repos
  @semaphore.synchronize do
    svn = Array(ASF::Config.get(:svn)).map {|dir| dir.untaint}

    # reload if repository changes
    if File.exist?(REPOSITORY) && @@repository_mtime!=File.mtime(REPOSITORY)
      @repos = nil
    end

    # reuse previous results if already scanned
    unless @repos
      @@repository_mtime = File.exist?(REPOSITORY) && File.mtime(REPOSITORY)

      @repos = Hash[Dir[*svn].map { |name| 
        next unless Dir.exist? name.untaint
        Dir.chdir name.untaint do
          out, err, status = Open3.capture3('svn', 'info')
          if status.success?
            [out[/URL: (.*)/,1].sub(/^http:/,'https:'), Dir.pwd.untaint]
          end
        end
      }.compact]
    end

    @repos
  end
end
update(path, msg, env, _, options={}) { |tmpdir, ''| ... } click to toggle source

update a file or directory in SVN, working entirely in a temporary directory Intended for use from GUI code

# File lib/whimsy/asf/svn.rb, line 188
def self.update(path, msg, env, _, options={})
  if File.directory? path
    dir = path
    basename = nil
  else
    dir = File.dirname(path)
    basename = File.basename(path)
  end

  if path.start_with? '/' and not path.include? '..' and File.exist?(path)
    dir.untaint
    basename.untaint
  end
  
  tmpdir = Dir.mktmpdir.untaint

  # N.B. the extra enclosing [] tell _.system not to show their contents on error
  begin
    # create an empty checkout
    _.system ['svn', 'checkout', '--depth', 'empty', '--non-interactive',
      ['--username', env.user, '--password', env.password],
      %x`svn info #{dir}`[/URL: (.*)/, 1], tmpdir]

    # retrieve the file to be updated (may not exist)
    if basename
      tmpfile = File.join(tmpdir, basename).untaint
      _.system ['svn', 'update', '--non-interactive',
        ['--username', env.user, '--password', env.password],
        tmpfile]
    else
      tmpfile = nil
    end

    # determine the new contents
    if not tmpfile
      # updating a directory
      previous_contents = contents = nil
      yield tmpdir, ''
    elsif File.file? tmpfile
      # updating an existing file
      previous_contents = File.read(tmpfile)
      contents = yield tmpdir, File.read(tmpfile)
    else
      # updating a new file
      previous_contents = nil
      contents = yield tmpdir, ''
      previous_contents = File.read(tmpfile) if File.file? tmpfile
    end
 
    # create/update the temporary copy
    if contents and not contents.empty?
      File.write tmpfile, contents
      if not previous_contents
        _.system ['svn', 'add', '--non-interactive',
          ['--username', env.user, '--password', env.password],
          tmpfile]
      end
    elsif tmpfile and File.file? tmpfile
      File.unlink tmpfile
      _.system ['svn', 'delete', '--non-interactive',
        ['--username', env.user, '--password', env.password],
        tmpfile]
    end

    if options[:dryrun]
      # show what would have been committed
      rc = _.system ['svn', 'diff', tmpfile]
    else
      # commit the changes
      rc = _.system ['svn', 'commit', tmpfile || tmpdir, '--non-interactive',
        ['--username', env.user, '--password', env.password],
        '--message', msg.untaint]
    end

    # fail if there are pending changes
    unless rc == 0 and %x`svn st #{tmpfile || tmpdir}`.empty?
      raise "svn failure #{path.inspect}"
    end
  ensure
    FileUtils.rm_rf tmpdir
  end
end
updateSimple(path) click to toggle source

Updates a working copy, and returns revision number

Note: working copies updated out via cron jobs can only be accessed read only by processes that run under the Apache web server.

# File lib/whimsy/asf/svn.rb, line 174
def self.updateSimple(path)
  cmd = ['svn', 'update', path, '--non-interactive']
  stdout, status = Open3.capture2(*cmd)
  revision = 0
  if status.success?
    # extract revision number
    revision = stdout[/^At revision (\d+)/, 1]
  end
  revision
end