module XMigra::GitSpecifics

Constants

ATTRIBUTE_UNSPECIFIED
MASTER_BRANCH_SUBDIR
MASTER_HEAD_ATTRIBUTE
PRODUCTION_CHAIN_EXTENSION_COMMAND

Public Class Methods

attr_values(attr, path, options={}) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 97
def attr_values(attr, path, options={})
  value_list = run_git('check-attr', attr, '--', path).each_line.map do |line|
    line.chomp.split(/: /, 3)[2]
  end
  return value_list unless options[:single]
  raise VersionControlError, options[:single] + ' ambiguous' if value_list.length > 1
  if (value_list.empty? || value_list == ['unspecified']) && options[:required]
    raise VersionControlError, options[:single] + ' undefined'
  end
  return value_list[0]
end
attributes_file_paths(path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 109
def attributes_file_paths(path)
  wdroot = Dir.chdir path do
    Pathname(run_git('rev-parse', '--show-toplevel').strip).realpath
  end
  pwd = Pathname.pwd
  
  [].tap do |result|
    path.realpath.ascend do |dirpath|
      result << AttributesFile.new(dirpath)
      break if (wdroot <=> dirpath) >= 0
    end
    
    result << AttributesFile.new(wdroot, :local)
  end
end
get_master_url() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 125
def get_master_url
  print "Master repository URL (empty for none): "
  master_repo = $stdin.gets.strip
  return nil if master_repo.empty?
  
  Console.validated_input "Master branch name" do |master_branch|
    if master_branch.empty?
      raise Console::InvalidInput.new(
        "Master branch name required to set 'xmigra-master' attribute --"
      )
    end
    "#{master_repo}##{master_branch}"
  end
end
init_schema(schema_config) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 140
def init_schema(schema_config)
  Console.output_section "Git Integration" do
    if master_url = get_master_url
      # Select locations for .gitattributes or .git/info/attributes
      attribs_file = Console::Menu.new(
        "Git Attributes Files",
        attributes_file_paths(schema_config.root_path),
        "File for storing 'xmigra-master' attribute",
        :get_name => lambda {|af| af.description}
      ).get_selection
      
      dbinfo_path = schema_config.root_path + SchemaManipulator::DBINFO_FILE
      attribute_pattern = "/#{dbinfo_path.relative_path_from(attribs_file.effect_root)}"
      
      schema_config.after_dbinfo_creation do
        attribs_file.open('a') do |attribs_io|
          attribs_io.puts "#{attribute_pattern} xmigra-master=#{master_url}"
        end
        schema_config.created_file! attribs_file.file_path
      end
    end
  end
end
manages(path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 65
def manages(path)
  run_git(:status, :check_exit=>true, :quiet=>true)
end
run_git(subcmd, *args) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 69
def run_git(subcmd, *args)
  options = (Hash === args[-1]) ? args.pop : {}
  check_exit = options.fetch(:check_exit, false)
  no_result = !options.fetch(:get_result, true)
  
  cmd_parts = ["git", subcmd.to_s]
  cmd_parts.concat(
    args.flatten.collect {|a| '""'.insert(1, a.to_s)}
  )
  case PLATFORM
  when :unix
    cmd_parts << "2>#{XMigra::NULL_FILE}"
  end if options[:quiet]
  
  cmd_str = cmd_parts.join(' ')
  
  output = begin
    `#{cmd_str}`
  rescue
    return false if check_exit
    raise
  end
  return ($?.success? ? output : nil) if options[:get_result] == :on_success
  return $?.success? if check_exit
  raise(VersionControlError, "Git command failed with exit code #{$?.exitstatus}\n        Command: #{cmd_str}") unless $?.success?
  return output unless no_result
end

Public Instance Methods

branch_identifier() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 224
def branch_identifier
  for_production = begin
    self.production
  rescue NameError
    false
  end
  
  return (if for_production
    self.git_branch_info[0]
  else
    return @git_branch_identifier if defined? @git_branch_identifier
    
    @git_branch_identifier = (
      self.git_master_head(:required=>false) ||
      self.git_local_branch_identifier(:note_modifications=>true)
    )
  end)
end
branch_use(commit=nil) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 243
def branch_use(commit=nil)
  if commit
    self.git_fetch_master_branch
    
    # If there are commits between the master head and *commit*, then
    # *commit* is not production-ish
    if self.git_commits_in? self.git_master_local_branch..commit
      return :development
    end
    
    # Otherwise, look to see if all migrations in the migration chain for
    # commit are in the master head with no diffs -- the migration chain
    # is a "prefix" of the chain in the master head:
    migration_chain = RepoStoredMigrationChain.new(
      commit,
      Pathname(path).join(SchemaManipulator::STRUCTURE_SUBDIR),
    )
    return :production if self.git(
      :diff, '--name-only',
      self.git_master_local_branch, commit, '--',
      *migration_chain.map(&:file_path)
    ).empty?
    return :development
  end
  
  return nil unless self.git_master_head(:required=>false)
  
  return self.git_branch_info[1]
end
check_working_copy!() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 180
def check_working_copy!
  return unless production
  
  file_paths = Array.from_generator(method(:each_file_path))
  unversioned_files = git(
    'diff-index',
    %w{-z --no-commit-id --name-only HEAD},
    '--',
    self.path
  ).split("\000").collect do |path|
    File.expand_path(self.path + path)
  end
  
  # Check that file_paths and unversioned_files are disjoint
  unless (file_paths & unversioned_files).empty?
    raise VersionControlError, "Some source files differ from their committed versions"
  end
  
  git_fetch_master_branch
  migrations.each do |m|
    # Check that the migration in the working tree is the same as in head of the central master branch
    fpath = m.file_path
    unless git(:diff, '--exit-code', self.git_master_local_branch, '--', fpath, check_exit: true)
      master_url, remote_branch = self.git_master_head.split('#', 2)
      raise VersionControlError, "'#{fpath}' is different locally than on '#{remote_branch}' in #{master_url}"
    end
  end
  
  # Since a production script was requested, warn if we are not generating
  # from a production branch
  if branch_use != :production
    master_url, remote_branch = self.git_master_head.split('#', 2)
    raise VersionControlError, "The working tree is not a commit in the history of '#{remote_branch}' in #{master_url}"
  end
end
get_conflict_info() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 457
def get_conflict_info
  structure_dir = Pathname.new(self.path) + SchemaManipulator::STRUCTURE_SUBDIR
  head_file = structure_dir + MigrationChain::HEAD_FILE
  stage_numbers = []
  git('ls-files', '-uz', '--', head_file).split("\000").each {|ref|
    if m = /[0-7]{6} [0-9a-f]{40} (\d)\t\S*/.match(ref)
      stage_numbers |= [m[1].to_i]
    end
  }
  return nil unless stage_numbers.sort == [1, 2, 3]
  
  chain_head = lambda do |stage_number|
    head_file_relative = head_file.relative_path_from(self.path)
    return YAML.parse(
      git(:show, ":#{stage_number}:./#{head_file_relative}")
    ).transform
  end
  
  # Ours (2) before theirs (3)...
  heads = [2, 3].collect(&chain_head)
  # ... unless merging from upstream or the master branch
  if self.git_merging_from_upstream? || self.git_merging_from_master?
    heads.reverse!
  end
  
  branch_point = chain_head.call(1)[MigrationChain::LATEST_CHANGE]
  
  conflict = MigrationConflict.new(structure_dir, branch_point, heads)
  
  # Standard git usage never commits directly to the master branch, and
  # there is no effective way to tell if this is happening.
  conflict.branch_use = :development
  
  tool = self
  conflict.after_fix = proc {tool.resolve_conflict!(head_file)}
  
  return conflict
end
git(*args) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 165
def git(*args)
  _path = begin
    self.path
  rescue NameError
    begin
      self.schema_dir
    rescue NameError
      Pathname(self.file_path).dirname
    end
  end
  Dir.chdir(_path) do |pwd|
    GitSpecifics.run_git(*args)
  end
end
git_branch() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 513
def git_branch
  return @git_branch if defined? @git_branch
  return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}, :quiet=>true).chomp
end
git_branch_info() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 525
def git_branch_info
  return @git_branch_info if defined? @git_branch_info
  
  self.git_fetch_master_branch
  
  # If there are no commits between the master head and HEAD, this working
  # copy is production-ish
  return (@git_branch_info = if self.branch_use('HEAD') == :production
    [self.git_master_head, :production]
  else
    [self.git_local_branch_identifier, :development]
  end)
end
git_commits_in?(range, path=nil) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 586
def git_commits_in?(range, path=nil)
  git(
    :log,
    '--pretty=format:%H',
    '-1',
    "#{range.begin.strip}..#{range.end.strip}",
    '--',
    path || self.path
  ) != ''
end
git_fetch_master_branch() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 545
def git_fetch_master_branch
  return if @git_master_branch_fetched
  master_url, remote_branch = self.git_master_head.split('#', 2)
  
  git(:fetch, '-f', master_url, "#{remote_branch}:#{git_master_local_branch}", :get_result=>false, :quiet=>true)
  @git_master_branch_fetched = true
end
git_internal_path() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 557
def git_internal_path
  return @git_internal_path if defined? @git_internal_path
  path_prefix = git('rev-parse', %w{--show-prefix}).chomp[0..-2]
  internal_path = '.'
  if path_prefix.length > 0
    internal_path += '/' + path_prefix
  end
  return @git_internal_path = internal_path
end
git_local_branch_identifier(options={}) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 539
def git_local_branch_identifier(options={})
  host = `hostname`
  path = git('rev-parse', '--show-toplevel')
  return "#{git_branch} of #{path} on #{host} (commit #{git_schema_commit})"
end
git_master_head(options={}) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 500
def git_master_head(options={})
  options = {:required=>true}.merge(options)
  return @git_master_head if defined? @git_master_head
  master_head = GitSpecifics.attr_values(
    MASTER_HEAD_ATTRIBUTE,
    self.path + SchemaManipulator::DBINFO_FILE,
    :single=>'Master branch',
    :required=>options[:required]
  )
  return nil if master_head.nil?
  return @git_master_head = (master_head if master_head != GitSpecifics::ATTRIBUTE_UNSPECIFIED)
end
git_master_local_branch() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 553
def git_master_local_branch
  "#{MASTER_BRANCH_SUBDIR}/#{git_branch}"
end
git_merging_from_master?() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 579
def git_merging_from_master?
  git_fetch_master_branch
  return !(self.git_commits_in? git_master_local_branch..'MERGE_HEAD')
rescue VersionControlError
  return false
end
git_merging_from_upstream?() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 567
def git_merging_from_upstream?
  upstream = git('rev-parse', '@{u}', :get_result=>:on_success, :quiet=>true)
  return false if upstream.nil?
  
  # Check if there are any commits in #{upstream}..MERGE_HEAD
  begin
    return !(self.git_commits_in? upstream..'MERGE_HEAD')
  rescue VersionControlError
    return false
  end
end
git_retrieve_status(a_path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 439
def git_retrieve_status(a_path)
  return nil unless Pathname(a_path).exist?
  
  if git('status', '--porcelain', a_path.to_s) =~ /^.+?(?= \S)/
    $&
  else
    '  '
  end
end
git_schema_commit() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 518
def git_schema_commit
  return @git_commit if defined? @git_commit
  reported_commit = git(:log, %w{-n1 --format=%H --}, self.path, :quiet=>true).chomp
  raise VersionControlError, "Schema not committed" if reported_commit.empty?
  return @git_commit = reported_commit
end
git_status() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 435
def git_status
  @git_status ||= git_retrieve_status(file_path)
end
production_pattern() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 449
def production_pattern
  ".+"
end
production_pattern=(pattern) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 453
def production_pattern=(pattern)
  raise VersionControlError, "Under version control by git, XMigra does not support production patterns."
end
resolve_conflict!(path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 496
def resolve_conflict!(path)
  git(:add, '--', path, :get_result=>false)
end
vcs_changes_from(from_commit, file_path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 422
def vcs_changes_from(from_commit, file_path)
  git(:diff, from_commit, '--', file_path)
end
vcs_comparator(options={}) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 401
def vcs_comparator(options={})
  VersionComparator.new(self, options)
end
vcs_contents(path, options={}) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 292
def vcs_contents(path, options={})
  args = []
  
  commit = options.fetch(:revision, 'HEAD')
  args << "#{commit}:./#{path}"
  
  git(:show, *args)
end
vcs_file_modified?(file_path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 430
def vcs_file_modified?(file_path)
  gstat = git_retrieve_status(file_path)
  gstat[0] != ' '
end
vcs_information() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 216
def vcs_information
  return [
    "Branch: #{branch_identifier}",
    "Path: #{git_internal_path}",
    "Commit: #{git_schema_commit}"
  ].join("\n")
end
vcs_latest_revision(a_file=nil) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 405
def vcs_latest_revision(a_file=nil)
  if a_file.nil? && defined? @vcs_latest_revision
    return @vcs_latest_revision
  end
  
  git(
    :log,
    '-n1',
    '--pretty=format:%H',
    '--',
    a_file || file_path,
    :quiet=>true
  ).chomp.tap do |val|
    @vcs_latest_revision = val if a_file.nil?
  end
end
vcs_most_recent_committed_contents(file_path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 426
def vcs_most_recent_committed_contents(file_path)
  git(:show, "HEAD:#{file_path}", :quiet=>true)
end
vcs_move(old_path, new_path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 273
def vcs_move(old_path, new_path)
  git(:mv, old_path, new_path, :get_result=>false)
end
vcs_prod_chain_extension_handler() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 301
def vcs_prod_chain_extension_handler
  attr_val = GitSpecifics.attr_values(
    PRODUCTION_CHAIN_EXTENSION_COMMAND,
    self.path + SchemaManipulator::DBINFO_FILE,
    :required=>false,
  )[0]
  
  # Check for special value
  return nil if attr_val == 'unspecified'
  
  handler_path = Pathname(attr_val)
  if handler_path.absolute?
    return handler_path if handler_path.exist?
  else
    handler_path = self.path + handler_path
    return handler_path if handler_path.exist?
  end
  return attr_val
end
vcs_production_contents(path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 281
def vcs_production_contents(path)
  return nil unless git_master_head(:required => false)
  git_fetch_master_branch
  # Skip the first two characters after the join to leave off the "./" prefix,
  # which makes git consider the current directory
  target_path = [git_internal_path, Pathname(path).relative_path_from(self.path)].join('/')[2..-1]
  git(:show, [git_master_local_branch, target_path].join(':'), :quiet=>true)
rescue VersionControlError
  return nil
end
vcs_remove(path) click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 277
def vcs_remove(path)
  git(:rm, path, :get_result=>false)
end
vcs_uncommitted?() click to toggle source
# File lib/xmigra/vcs_support/git.rb, line 321
def vcs_uncommitted?
  git_status == '??' || git_status[0] == 'A'
end