Class: MultiRepo::MergeCommand

Inherits:
Command
  • Object
show all
Defined in:
lib/multirepo/commands/merge-command.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Command

#ensure_in_work_tree, #ensure_multirepo_enabled, #ensure_multirepo_tracked, #install_hooks, #multirepo_enabled_dependencies, report_error, #uninstall_hooks, #update_gitconfig

Constructor Details

#initialize(argv) ⇒ MergeCommand

Returns a new instance of MergeCommand.



37
38
39
40
41
42
43
44
# File 'lib/multirepo/commands/merge-command.rb', line 37

def initialize(argv)
  @ref_name = argv.shift_argument
  @checkout_latest = argv.flag?("latest")
  @checkout_lock = argv.flag?("as-lock")
  @edit = argv.flag?("edit")
  @fast_forward = argv.flag?("ff")
  super
end

Class Method Details

.optionsObject



27
28
29
30
31
32
33
34
35
# File 'lib/multirepo/commands/merge-command.rb', line 27

def self.options
  [
    ['<refname>', 'The main repo tag, branch or commit id to merge.'],
    ['[--latest]', 'Merge the HEAD of each stored dependency branch instead of the commits recorded in the lock file.'],
    ['[--as-lock]', 'Merge the exact specified commits for each repo, as stored in the lock file.'],
    ['[--edit]', 'Open an editor to edit the commit message for each merge operation.'],
    ['[--ff]', 'Perform a fast-forward if possible.']
  ].concat(super)
end

Instance Method Details

#ask_tracking_files_update(all_merges_succeeded) ⇒ Object



230
231
232
233
234
235
236
237
238
# File 'lib/multirepo/commands/merge-command.rb', line 230

def ask_tracking_files_update(all_merges_succeeded)
  unless all_merges_succeeded
    Console.log_warning("Perform a 'multi update' after resolving merge conflicts to ensure lock file contents are valid")
    return
  end
  
  update_command = UpdateCommand.new(CLAide::ARGV.new(["--diff", "--commit", "[multirepo] Post-merge tracking files update"]))
  update_command.update_tracking_files_step(RepoSelection::MAIN)
end

#build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/multirepo/commands/merge-command.rb', line 156

def build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
  descriptors = []
  our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
    our_revision = our_dependency.config_entry.repo.current_revision
    
    their_revision = RevisionSelector.revision_for_mode(mode, ref_name, their_dependency.lock_entry)
    their_name = their_dependency.config_entry.name
    their_repo = their_dependency.config_entry.repo
    
    descriptor = MergeDescriptor.new(their_name, their_repo, our_revision, their_revision)
    
    descriptors.push(descriptor)
  end
  return descriptors
end

#build_merge(main_repo, initial_revision, ref_name, mode) ⇒ Object



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
# File 'lib/multirepo/commands/merge-command.rb', line 130

def build_merge(main_repo, initial_revision, ref_name, mode)
  # List dependencies prior to checkout so that we can compare them later
  our_dependencies = Performer.depth_ordered_dependencies
  
  # Checkout the specified main repo ref to find out which dependency refs to merge
  commit_id = Ref.new(main_repo, ref_name).commit_id # Checkout in floating HEAD
  Performer.perform_main_repo_checkout(main_repo, commit_id, false, "Checked out main repo '#{ref_name}' to inspect to-merge dependencies")
  
  # List dependencies for the ref we're trying to merge
  their_dependencies = Performer.depth_ordered_dependencies
  
  # Checkout the initial revision ASAP
  Performer.perform_main_repo_checkout(main_repo, initial_revision, false, "Checked out initial main repo revision '#{initial_revision}'")
  
  # Auto-merge would be too complex to implement (due to lots of edge cases)
  # if the specified ref does not have the same dependencies. Better perform a manual merge.
  ensure_dependencies_match(our_dependencies, their_dependencies)
  
  # Create a merge descriptor for each would-be merge as well as the main repo.
  # This step MUST be performed in OUR revision for the merge descriptors to be correct!
  descriptors = build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
  descriptors.push(MergeDescriptor.new("Main Repo", main_repo, initial_revision, ref_name))
  
  return descriptors
end

#ensure_dependencies_match(our_dependencies, their_dependencies) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
# File 'lib/multirepo/commands/merge-command.rb', line 172

def ensure_dependencies_match(our_dependencies, their_dependencies)
  our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
    if their_dependency.nil? || their_dependency.config_entry.id != our_dependency.config_entry.id
      fail MultiRepoException, "Dependencies differ, please merge manually"
    end
  end
  
  if their_dependencies.count > our_dependencies.count
    fail MultiRepoException, "There are more dependencies in the specified ref, please merge manually"
  end
end

#ensure_merge_valid(descriptors) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/multirepo/commands/merge-command.rb', line 196

def ensure_merge_valid(descriptors)
  outcome = MergeValidationResult.new
  outcome.outcome = MergeValidationResult::PROCEED
  
  if descriptors.any? { |d| d.state == TheirState::LOCAL_NO_UPSTREAM }
    outcome.message = "Some branches are not remote-tracking! Please review the merge operations above."
  elsif descriptors.any? { |d| d.state == TheirState::LOCAL_UPSTREAM_DIVERGED }
    outcome.outcome = MergeValidationResult::ABORT
    outcome.message = "Some upstream branches have diverged. This warrants a manual merge!"
  elsif descriptors.any? { |d| d.state == TheirState::LOCAL_OUTDATED }
    outcome.outcome = MergeValidationResult::MERGE_UPSTREAM
    outcome.message = "Some local branches are outdated"
  end
  
  return outcome
end

#merge_core(main_repo, initial_revision, mode) ⇒ Object



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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/multirepo/commands/merge-command.rb', line 83

def merge_core(main_repo, initial_revision, mode)
  config_file = ConfigFile.new(".")
  
  # Ensure the main repo is clean
  fail MultiRepoException, "Main repo is not clean; merge aborted" unless main_repo.clean?
  
  # Ensure dependencies are clean
  unless Utils.dependencies_clean?(config_file.load_entries)
    fail MultiRepoException, "Dependencies are not clean; merge aborted"
  end
  
  ref_name = @ref_name
  descriptors = nil
  loop do
    # Gather information about the merges that would occur
    descriptors = build_merge(main_repo, initial_revision, ref_name, mode)
  
    # Preview merge operations in the console
    preview_merge(descriptors, mode, ref_name)
    
    # Validate merge operations
    result = ensure_merge_valid(descriptors)
    
    case result.outcome
    when MergeValidationResult::ABORT
      fail MultiRepoException, result.message
    when MergeValidationResult::PROCEED
      fail MultiRepoException, "Merge aborted" unless Console.ask("Proceed?")
      Console.log_warning(result.message) if result.message
      break
    when MergeValidationResult::MERGE_UPSTREAM
      Console.log_warning(result.message)
      fail MultiRepoException, "Merge aborted" unless Console.ask("Merge upstream instead of local branches?")
      # TODO: Modify operations!
      fail MultiRepoException, "Fallback behavior not implemented. Please merge manually."
      next
    end
    
    fail MultiRepoException, "Merge aborted" unless Console.ask("Proceed?")
  end
  
  Console.log_step("Performing merge...")
  
  all_succeeded = perform_merges(descriptors)
  ask_tracking_files_update(all_succeeded)
end

#message_for_mode(mode, ref_name) ⇒ Object



240
241
242
243
244
245
246
247
248
249
# File 'lib/multirepo/commands/merge-command.rb', line 240

def message_for_mode(mode, ref_name)
  case mode
  when RevisionSelection::AS_LOCK
    "merge specific commits as stored in the lock file for main repo revision #{ref_name}"
  when RevisionSelection::LATEST
    "merge each branch as stored in the lock file of main repo revision #{ref_name}"
  when RevisionSelection::EXACT
    "merge #{ref_name} for each repository, ignoring the contents of the lock file"
  end
end

#perform_merges(descriptors) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/multirepo/commands/merge-command.rb', line 213

def perform_merges(descriptors)
  success = true
  descriptors.each do |descriptor|
    Console.log_substep("#{descriptor.name} : Merging #{descriptor.their_revision} into #{descriptor.our_revision}...")
    GitRunner.run_as_system(descriptor.repo.path, "merge #{descriptor.their_revision}#{@edit ? '' : ' --no-edit'}#{@fast_forward ? '' : ' --no-ff'}")
    success &= GitRunner.last_command_succeeded
  end
  
  if success
    Console.log_info("All merges performed successfully!")
  else
    Console.log_warning("Some merge operations failed. Please review the above.")
  end
  
  return success
end

#preview_merge(descriptors, mode, ref_name) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
# File 'lib/multirepo/commands/merge-command.rb', line 184

def preview_merge(descriptors, mode, ref_name)
  Console.log_info("Merging would #{message_for_mode(mode, ref_name)}:")
  
  table = Terminal::Table.new do |t|
    descriptors.reverse.each_with_index do |descriptor, index|
      t.add_row [descriptor.name.bold, descriptor.merge_description, descriptor.upstream_description]
      t.add_separator unless index == descriptors.count - 1
    end
  end
  puts table
end

#runObject



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/multirepo/commands/merge-command.rb', line 54

def run
  ensure_in_work_tree
  ensure_multirepo_enabled
  
  # Find out the checkout mode based on command-line options
  mode = RevisionSelector.mode_for_args(@checkout_latest, @checkout_lock)
  
  strategy_name = RevisionSelection.name_for_mode(mode)
  Console.log_step("Merging #{@ref_name} with '#{strategy_name}' strategy...")
  
  main_repo = Repo.new(".")
  
  # Keep the initial revision because we're going to need to come back to it later
  initial_revision = main_repo.current_revision
  
  begin
    merge_core(main_repo, initial_revision, mode)
  rescue MultiRepoException => e
    # Revert to the initial revision only if necessary
    unless main_repo.current_revision == initial_revision
      Console.log_warning("Restoring working copy to #{initial_revision}")
      main_repo.checkout(initial_revision)
    end
    raise e
  end
  
  Console.log_step("Done!")
end

#validate!Object



46
47
48
49
50
51
52
# File 'lib/multirepo/commands/merge-command.rb', line 46

def validate!
  super
  help! "You must specify a ref to merge" unless @ref_name
  unless Utils.only_one_true?(@checkout_latest, @checkout_lock)
    help! "You can't provide more than one operation modifier (--latest, --as-lock, etc.)"
  end
end