Module: XMigra::GitSpecifics

Defined in:
lib/xmigra/vcs_support/git.rb

Defined Under Namespace

Classes: AttributesFile, RepoStoredMigrationChain, VersionComparator

Constant Summary collapse

MASTER_HEAD_ATTRIBUTE =
'xmigra-master'
MASTER_BRANCH_SUBDIR =
'xmigra-master'
PRODUCTION_CHAIN_EXTENSION_COMMAND =
'xmigra-on-production-chain-extended'
ATTRIBUTE_UNSPECIFIED =
'unspecified'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.attr_values(attr, path, options = {}) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
# 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) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# 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_urlObject



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# 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) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# 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) ⇒ Object



65
66
67
# 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) ⇒ Object



69
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
# 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

Instance Method Details

#branch_identifierObject



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# 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) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# 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!Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 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_infoObject



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# 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) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# 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_branchObject



513
514
515
516
# 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_infoObject



525
526
527
528
529
530
531
532
533
534
535
536
537
# 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) ⇒ Boolean

Returns:

  • (Boolean)


586
587
588
589
590
591
592
593
594
595
# 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_branchObject



545
546
547
548
549
550
551
# 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_pathObject



557
558
559
560
561
562
563
564
565
# 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 = {}) ⇒ Object



539
540
541
542
543
# 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 = {}) ⇒ Object



500
501
502
503
504
505
506
507
508
509
510
511
# 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_branchObject



553
554
555
# File 'lib/xmigra/vcs_support/git.rb', line 553

def git_master_local_branch
  "#{MASTER_BRANCH_SUBDIR}/#{git_branch}"
end

#git_merging_from_master?Boolean

Returns:

  • (Boolean)


579
580
581
582
583
584
# 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?Boolean

Returns:

  • (Boolean)


567
568
569
570
571
572
573
574
575
576
577
# 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) ⇒ Object



439
440
441
442
443
444
445
446
447
# 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_commitObject



518
519
520
521
522
523
# 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_statusObject



435
436
437
# File 'lib/xmigra/vcs_support/git.rb', line 435

def git_status
  @git_status ||= git_retrieve_status(file_path)
end

#production_patternObject



449
450
451
# File 'lib/xmigra/vcs_support/git.rb', line 449

def production_pattern
  ".+"
end

#production_pattern=(pattern) ⇒ Object



453
454
455
# 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) ⇒ Object



496
497
498
# 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) ⇒ Object



422
423
424
# 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 = {}) ⇒ Object



401
402
403
# File 'lib/xmigra/vcs_support/git.rb', line 401

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

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



292
293
294
295
296
297
298
299
# 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) ⇒ Boolean

Returns:

  • (Boolean)


430
431
432
433
# 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_informationObject



216
217
218
219
220
221
222
# 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) ⇒ Object



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# 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) ⇒ Object



426
427
428
# 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) ⇒ Object



273
274
275
# 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_handlerObject



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# 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) ⇒ Object



281
282
283
284
285
286
287
288
289
290
# 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) ⇒ Object



277
278
279
# File 'lib/xmigra/vcs_support/git.rb', line 277

def vcs_remove(path)
  git(:rm, path, :get_result=>false)
end

#vcs_uncommitted?Boolean

Returns:

  • (Boolean)


321
322
323
# File 'lib/xmigra/vcs_support/git.rb', line 321

def vcs_uncommitted?
  git_status == '??' || git_status[0] == 'A'
end