git-multirepo 1.0.0.beta49 → 1.0.0.beta50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +2 -2
  3. data/.gitbugtraq +3 -3
  4. data/.gitignore +38 -38
  5. data/.multirepo.meta +2 -2
  6. data/.rspec +2 -2
  7. data/.rubocop.yml +79 -79
  8. data/CHANGELOG.md +29 -25
  9. data/Gemfile +4 -4
  10. data/Gemfile.lock +42 -42
  11. data/LICENSE +22 -22
  12. data/README.md +154 -154
  13. data/Rakefile +1 -1
  14. data/bin/multi +11 -11
  15. data/docs/bug-repros/91565510-repro.sh +20 -20
  16. data/git-multirepo.gemspec +31 -31
  17. data/lib/git-multirepo.rb +3 -3
  18. data/lib/multirepo/commands/add-command.rb +51 -51
  19. data/lib/multirepo/commands/branch-command.rb +82 -82
  20. data/lib/multirepo/commands/checkout-command.rb +120 -120
  21. data/lib/multirepo/commands/clone-command.rb +68 -68
  22. data/lib/multirepo/commands/command.rb +90 -90
  23. data/lib/multirepo/commands/commands.rb +15 -0
  24. data/lib/multirepo/commands/do-command.rb +103 -103
  25. data/lib/multirepo/commands/graph-command.rb +43 -43
  26. data/lib/multirepo/commands/init-command.rb +121 -121
  27. data/lib/multirepo/commands/inspect-command.rb +39 -39
  28. data/lib/multirepo/commands/install-command.rb +153 -153
  29. data/lib/multirepo/commands/merge-command.rb +249 -225
  30. data/lib/multirepo/commands/open-command.rb +57 -57
  31. data/lib/multirepo/commands/remove-command.rb +48 -48
  32. data/lib/multirepo/commands/uninit-command.rb +18 -18
  33. data/lib/multirepo/commands/update-command.rb +106 -94
  34. data/lib/multirepo/config.rb +16 -16
  35. data/lib/multirepo/files/config-entry.rb +39 -39
  36. data/lib/multirepo/files/config-file.rb +46 -46
  37. data/lib/multirepo/files/lock-entry.rb +29 -29
  38. data/lib/multirepo/files/lock-file.rb +56 -56
  39. data/lib/multirepo/files/meta-file.rb +41 -41
  40. data/lib/multirepo/files/tracking-file.rb +9 -9
  41. data/lib/multirepo/files/tracking-files.rb +47 -47
  42. data/lib/multirepo/git/branch.rb +32 -32
  43. data/lib/multirepo/git/change.rb +11 -11
  44. data/lib/multirepo/git/commit.rb +7 -7
  45. data/lib/multirepo/git/git-runner.rb +56 -56
  46. data/lib/multirepo/git/git.rb +10 -10
  47. data/lib/multirepo/git/ref.rb +38 -38
  48. data/lib/multirepo/git/remote.rb +17 -17
  49. data/lib/multirepo/git/repo.rb +124 -124
  50. data/lib/multirepo/hooks/post-commit-hook.rb +23 -23
  51. data/lib/multirepo/hooks/pre-commit-hook.rb +35 -35
  52. data/lib/{info.rb → multirepo/info.rb} +5 -5
  53. data/lib/multirepo/logic/dependency.rb +6 -6
  54. data/lib/multirepo/logic/merge-descriptor.rb +95 -95
  55. data/lib/multirepo/logic/node.rb +72 -72
  56. data/lib/multirepo/logic/performer.rb +55 -55
  57. data/lib/multirepo/logic/revision-selector.rb +35 -35
  58. data/lib/multirepo/multirepo-exception.rb +6 -6
  59. data/lib/multirepo/utility/console.rb +52 -52
  60. data/lib/multirepo/utility/popen-runner.rb +27 -27
  61. data/lib/multirepo/utility/system-runner.rb +14 -14
  62. data/lib/multirepo/utility/utils.rb +95 -95
  63. data/lib/multirepo/utility/verbosity.rb +6 -6
  64. data/resources/.gitconfig +2 -2
  65. data/resources/post-commit +6 -6
  66. data/resources/pre-commit +6 -6
  67. data/spec/integration/init_spec.rb +19 -19
  68. data/spec/spec_helper.rb +89 -89
  69. metadata +33 -33
  70. data/lib/commands.rb +0 -15
@@ -1,225 +1,249 @@
1
- require "terminal-table"
2
-
3
- require "multirepo/utility/console"
4
- require "multirepo/logic/node"
5
- require "multirepo/logic/revision-selector"
6
- require "multirepo/logic/performer"
7
- require "multirepo/logic/merge-descriptor"
8
-
9
- module MultiRepo
10
- class MergeValidationResult
11
- ABORT = 0
12
- PROCEED = 1
13
- MERGE_UPSTREAM = 2
14
-
15
- attr_accessor :outcome
16
- attr_accessor :message
17
- end
18
-
19
- class MergeCommand < Command
20
- self.command = "merge"
21
- self.summary = "Performs a git merge on all dependencies and the main repo, in the proper order."
22
-
23
- def self.options
24
- [
25
- ['<refname>', 'The main repo tag, branch or commit id to merge.'],
26
- ['[--latest]', 'Merge the HEAD of each stored dependency branch instead of the commits recorded in the lock file.'],
27
- ['[--exact]', 'Merge the exact specified ref for each repo, regardless of what\'s stored in the lock file.']
28
- ].concat(super)
29
- end
30
-
31
- def initialize(argv)
32
- @ref_name = argv.shift_argument
33
- @checkout_latest = argv.flag?("latest")
34
- @checkout_exact = argv.flag?("exact")
35
- super
36
- end
37
-
38
- def validate!
39
- super
40
- help! "You must specify a ref to merge" unless @ref_name
41
- unless validate_only_one_flag(@checkout_latest, @checkout_exact)
42
- help! "You can't provide more than one operation modifier (--latest, --exact, etc.)"
43
- end
44
- end
45
-
46
- def run
47
- ensure_in_work_tree
48
- ensure_multirepo_enabled
49
-
50
- # Find out the checkout mode based on command-line options
51
- mode = RevisionSelector.mode_for_args(@checkout_latest, @checkout_exact)
52
-
53
- strategy_name = RevisionSelectionMode.name_for_mode(mode)
54
- Console.log_step("Merging #{@ref_name} with '#{strategy_name}' strategy...")
55
-
56
- main_repo = Repo.new(".")
57
-
58
- # Keep the initial revision because we're going to need to come back to it later
59
- initial_revision = main_repo.current_revision
60
-
61
- begin
62
- merge_core(main_repo, initial_revision, mode)
63
- rescue MultiRepoException => e
64
- # Revert to the initial revision only if necessary
65
- unless main_repo.current_revision == initial_revision
66
- Console.log_warning("Restoring working copy to #{initial_revision}")
67
- main_repo.checkout(initial_revision)
68
- end
69
- raise e
70
- end
71
-
72
- Console.log_step("Done!")
73
- end
74
-
75
- def merge_core(main_repo, initial_revision, mode)
76
- config_file = ConfigFile.new(".")
77
-
78
- # Ensure the main repo is clean
79
- fail MultiRepoException, "Main repo is not clean; merge aborted" unless main_repo.clean?
80
-
81
- # Ensure dependencies are clean
82
- unless Utils.dependencies_clean?(config_file.load_entries)
83
- fail MultiRepoException, "Dependencies are not clean; merge aborted"
84
- end
85
-
86
- ref_name = @ref_name
87
- descriptors = nil
88
- loop do
89
- # Gather information about the merges that would occur
90
- descriptors = build_merge(main_repo, initial_revision, ref_name, mode)
91
-
92
- # Preview merge operations in the console
93
- preview_merge(descriptors, mode, ref_name)
94
-
95
- # Validate merge operations
96
- result = ensure_merge_valid(descriptors)
97
-
98
- case result.outcome
99
- when MergeValidationResult::ABORT
100
- fail MultiRepoException, result.message
101
- when MergeValidationResult::PROCEED
102
- fail MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Proceed?")
103
- Console.log_warning(result.message) if result.message
104
- break
105
- when MergeValidationResult::MERGE_UPSTREAM
106
- Console.log_warning(result.message)
107
- fail MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Merge upstream instead of local branches?")
108
- # TODO: Modify operations!
109
- fail MultiRepoException, "Fallback behavior not implemented. Please merge manually."
110
- next
111
- end
112
-
113
- fail MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Proceed?")
114
- end
115
-
116
- Console.log_step("Performing merge...")
117
-
118
- # Merge dependencies and the main repo
119
- perform_merges(descriptors)
120
- end
121
-
122
- def build_merge(main_repo, initial_revision, ref_name, mode)
123
- # List dependencies prior to checkout so that we can compare them later
124
- our_dependencies = Performer.dependencies
125
-
126
- # Checkout the specified main repo ref to find out which dependency refs to merge
127
- Performer.perform_main_repo_checkout(main_repo, ref_name, "Checked out main repo '#{ref_name}' to inspect to-merge dependencies")
128
-
129
- # List dependencies for the ref we're trying to merge
130
- their_dependencies = Performer.dependencies
131
-
132
- # Checkout the initial revision ASAP
133
- Performer.perform_main_repo_checkout(main_repo, initial_revision, "Checked out initial main repo revision '#{initial_revision}'")
134
-
135
- # Auto-merge would be too complex to implement (due to lots of edge cases)
136
- # if the specified ref does not have the same dependencies. Better perform a manual merge.
137
- ensure_dependencies_match(our_dependencies, their_dependencies)
138
-
139
- # Create a merge descriptor for each would-be merge as well as the main repo.
140
- # This step MUST be performed in OUR revision for the merge descriptors to be correct!
141
- descriptors = build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
142
- descriptors.push(MergeDescriptor.new("Main Repo", main_repo, initial_revision, ref_name))
143
-
144
- return descriptors
145
- end
146
-
147
- def build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
148
- descriptors = []
149
- our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
150
- our_revision = our_dependency.config_entry.repo.current_revision
151
-
152
- their_revision = RevisionSelector.revision_for_mode(mode, ref_name, their_dependency.lock_entry)
153
- their_name = their_dependency.config_entry.name
154
- their_repo = their_dependency.config_entry.repo
155
-
156
- descriptor = MergeDescriptor.new(their_name, their_repo, our_revision, their_revision)
157
-
158
- descriptors.push(descriptor)
159
- end
160
- return descriptors
161
- end
162
-
163
- def ensure_dependencies_match(our_dependencies, their_dependencies)
164
- our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
165
- if their_dependency.nil? || their_dependency.config_entry.id != our_dependency.config_entry.id
166
- fail MultiRepoException, "Dependencies differ, please merge manually"
167
- end
168
- end
169
-
170
- if their_dependencies.count > our_dependencies.count
171
- fail MultiRepoException, "There are more dependencies in the specified ref, please merge manually"
172
- end
173
- end
174
-
175
- def preview_merge(descriptors, mode, ref_name)
176
- Console.log_info("Merging would #{message_for_mode(mode, ref_name)}:")
177
-
178
- table = Terminal::Table.new do |t|
179
- descriptors.reverse.each_with_index do |descriptor, index|
180
- t.add_row [descriptor.name.bold, descriptor.merge_description, descriptor.upstream_description]
181
- t.add_separator unless index == descriptors.count - 1
182
- end
183
- end
184
- puts table
185
- end
186
-
187
- def ensure_merge_valid(descriptors)
188
- outcome = MergeValidationResult.new
189
- outcome.outcome = MergeValidationResult::PROCEED
190
-
191
- if descriptors.any? { |d| d.state == TheirState::LOCAL_NO_UPSTREAM }
192
- outcome.message = "Some branches are not remote-tracking! Please review the merge operations above."
193
- elsif descriptors.any? { |d| d.state == TheirState::LOCAL_UPSTREAM_DIVERGED }
194
- outcome.outcome = MergeValidationResult::ABORT
195
- outcome.message = "Some upstream branches have diverged. This warrants a manual merge!"
196
- elsif descriptors.any? { |d| d.state == TheirState::LOCAL_OUTDATED }
197
- outcome.outcome = MergeValidationResult::MERGE_UPSTREAM
198
- outcome.message = "Some local branches are outdated."
199
- end
200
-
201
- return outcome
202
- end
203
-
204
- def perform_merges(descriptors)
205
- success = true
206
- descriptors.each do |descriptor|
207
- Console.log_substep("#{descriptor.name} : Merging #{descriptor.their_revision} into #{descriptor.our_revision}...")
208
- GitRunner.run_as_system(descriptor.repo.path, "merge #{descriptor.their_revision}")
209
- success &= GitRunner.last_command_succeeded
210
- end
211
- Console.log_warning("Some merge operations failed. Please review the above results.") unless success
212
- end
213
-
214
- def message_for_mode(mode, ref_name)
215
- case mode
216
- when RevisionSelectionMode::AS_LOCK
217
- "merge specific commits as stored in the lock file for main repo revision #{ref_name}"
218
- when RevisionSelectionMode::LATEST
219
- "merge each branch as stored in the lock file of main repo revision #{ref_name}"
220
- when RevisionSelectionMode::EXACT
221
- "merge #{ref_name} for each repository, ignoring the contents of the lock file"
222
- end
223
- end
224
- end
225
- end
1
+ require "terminal-table"
2
+
3
+ require "multirepo/utility/console"
4
+ require "multirepo/logic/node"
5
+ require "multirepo/logic/revision-selector"
6
+ require "multirepo/logic/performer"
7
+ require "multirepo/logic/merge-descriptor"
8
+ require "multirepo/files/tracking-files"
9
+
10
+ module MultiRepo
11
+ class MergeValidationResult
12
+ ABORT = 0
13
+ PROCEED = 1
14
+ MERGE_UPSTREAM = 2
15
+
16
+ attr_accessor :outcome
17
+ attr_accessor :message
18
+ end
19
+
20
+ class MergeCommand < Command
21
+ self.command = "merge"
22
+ self.summary = "Performs a git merge on all dependencies and the main repo, in the proper order."
23
+
24
+ def self.options
25
+ [
26
+ ['<refname>', 'The main repo tag, branch or commit id to merge.'],
27
+ ['[--latest]', 'Merge the HEAD of each stored dependency branch instead of the commits recorded in the lock file.'],
28
+ ['[--exact]', 'Merge the exact specified ref for each repo, regardless of what\'s stored in the lock file.']
29
+ ].concat(super)
30
+ end
31
+
32
+ def initialize(argv)
33
+ @ref_name = argv.shift_argument
34
+ @checkout_latest = argv.flag?("latest")
35
+ @checkout_exact = argv.flag?("exact")
36
+ super
37
+ end
38
+
39
+ def validate!
40
+ super
41
+ help! "You must specify a ref to merge" unless @ref_name
42
+ unless validate_only_one_flag(@checkout_latest, @checkout_exact)
43
+ help! "You can't provide more than one operation modifier (--latest, --exact, etc.)"
44
+ end
45
+ end
46
+
47
+ def run
48
+ ensure_in_work_tree
49
+ ensure_multirepo_enabled
50
+
51
+ # Find out the checkout mode based on command-line options
52
+ mode = RevisionSelector.mode_for_args(@checkout_latest, @checkout_exact)
53
+
54
+ strategy_name = RevisionSelectionMode.name_for_mode(mode)
55
+ Console.log_step("Merging #{@ref_name} with '#{strategy_name}' strategy...")
56
+
57
+ main_repo = Repo.new(".")
58
+
59
+ # Keep the initial revision because we're going to need to come back to it later
60
+ initial_revision = main_repo.current_revision
61
+
62
+ begin
63
+ merge_core(main_repo, initial_revision, mode)
64
+ rescue MultiRepoException => e
65
+ # Revert to the initial revision only if necessary
66
+ unless main_repo.current_revision == initial_revision
67
+ Console.log_warning("Restoring working copy to #{initial_revision}")
68
+ main_repo.checkout(initial_revision)
69
+ end
70
+ raise e
71
+ end
72
+
73
+ Console.log_step("Done!")
74
+ end
75
+
76
+ def merge_core(main_repo, initial_revision, mode)
77
+ config_file = ConfigFile.new(".")
78
+
79
+ # Ensure the main repo is clean
80
+ fail MultiRepoException, "Main repo is not clean; merge aborted" unless main_repo.clean?
81
+
82
+ # Ensure dependencies are clean
83
+ unless Utils.dependencies_clean?(config_file.load_entries)
84
+ fail MultiRepoException, "Dependencies are not clean; merge aborted"
85
+ end
86
+
87
+ ref_name = @ref_name
88
+ descriptors = nil
89
+ loop do
90
+ # Gather information about the merges that would occur
91
+ descriptors = build_merge(main_repo, initial_revision, ref_name, mode)
92
+
93
+ # Preview merge operations in the console
94
+ preview_merge(descriptors, mode, ref_name)
95
+
96
+ # Validate merge operations
97
+ result = ensure_merge_valid(descriptors)
98
+
99
+ case result.outcome
100
+ when MergeValidationResult::ABORT
101
+ fail MultiRepoException, result.message
102
+ when MergeValidationResult::PROCEED
103
+ fail MultiRepoException, "Merge aborted" unless Console.ask("Proceed?")
104
+ Console.log_warning(result.message) if result.message
105
+ break
106
+ when MergeValidationResult::MERGE_UPSTREAM
107
+ Console.log_warning(result.message)
108
+ fail MultiRepoException, "Merge aborted" unless Console.ask("Merge upstream instead of local branches?")
109
+ # TODO: Modify operations!
110
+ fail MultiRepoException, "Fallback behavior not implemented. Please merge manually."
111
+ next
112
+ end
113
+
114
+ fail MultiRepoException, "Merge aborted" unless Console.ask("Proceed?")
115
+ end
116
+
117
+ Console.log_step("Performing merge...")
118
+
119
+ all_succeeded = perform_merges(descriptors)
120
+ ask_tracking_files_update(all_succeeded)
121
+ end
122
+
123
+ def build_merge(main_repo, initial_revision, ref_name, mode)
124
+ # List dependencies prior to checkout so that we can compare them later
125
+ our_dependencies = Performer.dependencies
126
+
127
+ # Checkout the specified main repo ref to find out which dependency refs to merge
128
+ commit_id = Ref.new(main_repo, ref_name).commit_id # Checkout in floating HEAD
129
+ Performer.perform_main_repo_checkout(main_repo, commit_id, "Checked out main repo '#{ref_name}' to inspect to-merge dependencies")
130
+
131
+ # List dependencies for the ref we're trying to merge
132
+ their_dependencies = Performer.dependencies
133
+
134
+ # Checkout the initial revision ASAP
135
+ Performer.perform_main_repo_checkout(main_repo, initial_revision, "Checked out initial main repo revision '#{initial_revision}'")
136
+
137
+ # Auto-merge would be too complex to implement (due to lots of edge cases)
138
+ # if the specified ref does not have the same dependencies. Better perform a manual merge.
139
+ ensure_dependencies_match(our_dependencies, their_dependencies)
140
+
141
+ # Create a merge descriptor for each would-be merge as well as the main repo.
142
+ # This step MUST be performed in OUR revision for the merge descriptors to be correct!
143
+ descriptors = build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
144
+ descriptors.push(MergeDescriptor.new("Main Repo", main_repo, initial_revision, ref_name))
145
+
146
+ return descriptors
147
+ end
148
+
149
+ def build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
150
+ descriptors = []
151
+ our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
152
+ our_revision = our_dependency.config_entry.repo.current_revision
153
+
154
+ their_revision = RevisionSelector.revision_for_mode(mode, ref_name, their_dependency.lock_entry)
155
+ their_name = their_dependency.config_entry.name
156
+ their_repo = their_dependency.config_entry.repo
157
+
158
+ descriptor = MergeDescriptor.new(their_name, their_repo, our_revision, their_revision)
159
+
160
+ descriptors.push(descriptor)
161
+ end
162
+ return descriptors
163
+ end
164
+
165
+ def ensure_dependencies_match(our_dependencies, their_dependencies)
166
+ our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
167
+ if their_dependency.nil? || their_dependency.config_entry.id != our_dependency.config_entry.id
168
+ fail MultiRepoException, "Dependencies differ, please merge manually"
169
+ end
170
+ end
171
+
172
+ if their_dependencies.count > our_dependencies.count
173
+ fail MultiRepoException, "There are more dependencies in the specified ref, please merge manually"
174
+ end
175
+ end
176
+
177
+ def preview_merge(descriptors, mode, ref_name)
178
+ Console.log_info("Merging would #{message_for_mode(mode, ref_name)}:")
179
+
180
+ table = Terminal::Table.new do |t|
181
+ descriptors.reverse.each_with_index do |descriptor, index|
182
+ t.add_row [descriptor.name.bold, descriptor.merge_description, descriptor.upstream_description]
183
+ t.add_separator unless index == descriptors.count - 1
184
+ end
185
+ end
186
+ puts table
187
+ end
188
+
189
+ def ensure_merge_valid(descriptors)
190
+ outcome = MergeValidationResult.new
191
+ outcome.outcome = MergeValidationResult::PROCEED
192
+
193
+ if descriptors.any? { |d| d.state == TheirState::LOCAL_NO_UPSTREAM }
194
+ outcome.message = "Some branches are not remote-tracking! Please review the merge operations above."
195
+ elsif descriptors.any? { |d| d.state == TheirState::LOCAL_UPSTREAM_DIVERGED }
196
+ outcome.outcome = MergeValidationResult::ABORT
197
+ outcome.message = "Some upstream branches have diverged. This warrants a manual merge!"
198
+ elsif descriptors.any? { |d| d.state == TheirState::LOCAL_OUTDATED }
199
+ outcome.outcome = MergeValidationResult::MERGE_UPSTREAM
200
+ outcome.message = "Some local branches are outdated"
201
+ end
202
+
203
+ return outcome
204
+ end
205
+
206
+ def perform_merges(descriptors)
207
+ success = true
208
+ descriptors.each do |descriptor|
209
+ Console.log_substep("#{descriptor.name} : Merging #{descriptor.their_revision} into #{descriptor.our_revision}...")
210
+ GitRunner.run_as_system(descriptor.repo.path, "merge #{descriptor.their_revision}")
211
+ success &= GitRunner.last_command_succeeded
212
+ end
213
+
214
+ if success
215
+ Console.log_info("All merges performed successfully!")
216
+ else
217
+ Console.log_warning("Some merge operations failed. Please review the above.")
218
+ end
219
+
220
+ return success
221
+ end
222
+
223
+ def ask_tracking_files_update(all_merges_succeeded)
224
+ unless all_merges_succeeded
225
+ Console.log_warning("Perform a manual update using 'multi update' after resolving merge conflicts")
226
+ return
227
+ end
228
+
229
+ return unless Console.ask("Update main repo tracking files (important for continuous integration)?")
230
+
231
+ tracking_files = TrackingFiles.new(".")
232
+ tracking_files.update
233
+ tracking_files.commit("[multirepo] Post-merge tracking files update")
234
+
235
+ Console.log_info("Updated and committed tracking files in the main repo")
236
+ end
237
+
238
+ def message_for_mode(mode, ref_name)
239
+ case mode
240
+ when RevisionSelectionMode::AS_LOCK
241
+ "merge specific commits as stored in the lock file for main repo revision #{ref_name}"
242
+ when RevisionSelectionMode::LATEST
243
+ "merge each branch as stored in the lock file of main repo revision #{ref_name}"
244
+ when RevisionSelectionMode::EXACT
245
+ "merge #{ref_name} for each repository, ignoring the contents of the lock file"
246
+ end
247
+ end
248
+ end
249
+ end