git-multirepo 1.0.0.beta46 → 1.0.0.beta47

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 (68) 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/Gemfile +4 -4
  8. data/Gemfile.lock +42 -42
  9. data/LICENSE +22 -22
  10. data/README.md +154 -154
  11. data/Rakefile +2 -2
  12. data/bin/multi +10 -10
  13. data/docs/bug-repros/91565510-repro.sh +20 -20
  14. data/git-multirepo.gemspec +31 -31
  15. data/lib/commands.rb +14 -14
  16. data/lib/git-multirepo.rb +2 -2
  17. data/lib/info.rb +4 -4
  18. data/lib/multirepo/commands/add-command.rb +50 -50
  19. data/lib/multirepo/commands/branch-command.rb +81 -81
  20. data/lib/multirepo/commands/checkout-command.rb +119 -119
  21. data/lib/multirepo/commands/clone-command.rb +67 -67
  22. data/lib/multirepo/commands/command.rb +89 -89
  23. data/lib/multirepo/commands/do-command.rb +102 -102
  24. data/lib/multirepo/commands/graph-command.rb +42 -42
  25. data/lib/multirepo/commands/init-command.rb +119 -119
  26. data/lib/multirepo/commands/inspect-command.rb +38 -38
  27. data/lib/multirepo/commands/install-command.rb +147 -146
  28. data/lib/multirepo/commands/merge-command.rb +225 -225
  29. data/lib/multirepo/commands/open-command.rb +56 -56
  30. data/lib/multirepo/commands/remove-command.rb +47 -47
  31. data/lib/multirepo/commands/uninit-command.rb +17 -17
  32. data/lib/multirepo/commands/update-command.rb +55 -55
  33. data/lib/multirepo/config.rb +15 -15
  34. data/lib/multirepo/files/config-entry.rb +38 -38
  35. data/lib/multirepo/files/config-file.rb +45 -45
  36. data/lib/multirepo/files/lock-entry.rb +28 -28
  37. data/lib/multirepo/files/lock-file.rb +55 -55
  38. data/lib/multirepo/files/meta-file.rb +40 -40
  39. data/lib/multirepo/files/tracking-file.rb +8 -8
  40. data/lib/multirepo/files/tracking-files.rb +46 -46
  41. data/lib/multirepo/git/branch.rb +31 -31
  42. data/lib/multirepo/git/change.rb +10 -10
  43. data/lib/multirepo/git/commit.rb +6 -6
  44. data/lib/multirepo/git/git-runner.rb +57 -46
  45. data/lib/multirepo/git/git.rb +9 -9
  46. data/lib/multirepo/git/ref.rb +37 -37
  47. data/lib/multirepo/git/remote.rb +16 -16
  48. data/lib/multirepo/git/repo.rb +122 -122
  49. data/lib/multirepo/hooks/post-commit-hook.rb +22 -22
  50. data/lib/multirepo/hooks/pre-commit-hook.rb +34 -34
  51. data/lib/multirepo/logic/dependency.rb +5 -5
  52. data/lib/multirepo/logic/merge-descriptor.rb +94 -94
  53. data/lib/multirepo/logic/node.rb +71 -71
  54. data/lib/multirepo/logic/performer.rb +56 -56
  55. data/lib/multirepo/logic/revision-selector.rb +34 -34
  56. data/lib/multirepo/multirepo-exception.rb +5 -5
  57. data/lib/multirepo/utility/console.rb +51 -51
  58. data/lib/multirepo/utility/popen-runner.rb +27 -0
  59. data/lib/multirepo/utility/system-runner.rb +14 -0
  60. data/lib/multirepo/utility/utils.rb +94 -94
  61. data/lib/multirepo/utility/verbosity.rb +6 -0
  62. data/resources/.gitconfig +2 -2
  63. data/resources/post-commit +5 -5
  64. data/resources/pre-commit +5 -5
  65. data/spec/integration/init_spec.rb +18 -18
  66. data/spec/spec_helper.rb +89 -89
  67. metadata +6 -4
  68. data/lib/multirepo/utility/runner.rb +0 -35
@@ -1,39 +1,39 @@
1
- require "multirepo/utility/console"
2
- require "multirepo/utility/utils"
3
-
4
- module MultiRepo
5
- class InspectCommand < Command
6
- self.command = "inspect"
7
- self.summary = "Outputs various information about multirepo-enabled repos. For use in scripting and CI scenarios."
8
-
9
- def self.options
10
- [
11
- ['[--version]', 'Outputs the multirepo version that was used to track this revision.'],
12
- ['[--tracked]', 'Whether the current revision is tracked by multirepo or not.']
13
- ].concat(super)
14
- end
15
-
16
- def initialize(argv)
17
- @version = argv.flag?("version")
18
- @tracked = argv.flag?("tracked")
19
- super
20
- end
21
-
22
- def validate!
23
- super
24
- unless validate_only_one_flag(@version, @tracked)
25
- help! "You can't provide more than one operation modifier (--version, --tracked, etc.)"
26
- end
27
- end
28
-
29
- def run
30
- ensure_in_work_tree
31
-
32
- if @version
33
- puts MetaFile.new(".").load.version
34
- elsif @tracked
35
- puts Utils.is_multirepo_tracked(".").to_s
36
- end
37
- end
38
- end
1
+ require "multirepo/utility/console"
2
+ require "multirepo/utility/utils"
3
+
4
+ module MultiRepo
5
+ class InspectCommand < Command
6
+ self.command = "inspect"
7
+ self.summary = "Outputs various information about multirepo-enabled repos. For use in scripting and CI scenarios."
8
+
9
+ def self.options
10
+ [
11
+ ['[--version]', 'Outputs the multirepo version that was used to track this revision.'],
12
+ ['[--tracked]', 'Whether the current revision is tracked by multirepo or not.']
13
+ ].concat(super)
14
+ end
15
+
16
+ def initialize(argv)
17
+ @version = argv.flag?("version")
18
+ @tracked = argv.flag?("tracked")
19
+ super
20
+ end
21
+
22
+ def validate!
23
+ super
24
+ unless validate_only_one_flag(@version, @tracked)
25
+ help! "You can't provide more than one operation modifier (--version, --tracked, etc.)"
26
+ end
27
+ end
28
+
29
+ def run
30
+ ensure_in_work_tree
31
+
32
+ if @version
33
+ puts MetaFile.new(".").load.version
34
+ elsif @tracked
35
+ puts Utils.is_multirepo_tracked(".").to_s
36
+ end
37
+ end
38
+ end
39
39
  end
@@ -1,147 +1,148 @@
1
- require "terminal-table"
2
-
3
- require "multirepo/utility/console"
4
- require "multirepo/utility/utils"
5
- require "multirepo/git/repo"
6
- require "multirepo/logic/performer"
7
- require "multirepo/commands/checkout-command"
8
-
9
- module MultiRepo
10
- class InstallCommand < Command
11
- self.command = "install"
12
- self.summary = "Clones and checks out dependencies as defined in the version-controlled multirepo metadata files and installs git-multirepo's local git hooks."
13
-
14
- def self.options
15
- [
16
- ['[--hooks]', 'Only install local git hooks.'],
17
- ['[--ci]', 'Perform a continuous-integration-aware install (such as on a CI build server or agent).']
18
- ].concat(super)
19
- end
20
-
21
- def initialize(argv)
22
- @hooks = argv.flag?("hooks")
23
- @ci = argv.flag?("ci")
24
- super
25
- end
26
-
27
- def validate!
28
- super
29
- unless validate_only_one_flag(@hooks, @ci)
30
- help! "You can't provide more than one operation modifier (--hooks, --ci, etc.)"
31
- end
32
- end
33
-
34
- def run
35
- ensure_in_work_tree unless @ci
36
- ensure_multirepo_tracked
37
-
38
- if @hooks
39
- Console.log_step("Installing hooks in main repo and all dependencies...")
40
- install_hooks_step
41
- else
42
- Console.log_step("Installing dependencies...")
43
- log_ci_info if @ci
44
- full_install
45
- end
46
-
47
- Console.log_step("Done!")
48
- end
49
-
50
- def log_ci_info
51
- Console.log_warning("Performing continuous-integration-aware install")
52
-
53
- main_repo = Repo.new(".")
54
- main_repo_branch = main_repo.current_branch
55
- meta_file = MetaFile.new(".").load
56
-
57
- table = Terminal::Table.new do |t|
58
- t.title = "Revision Info"
59
- t.add_row ["git-multirepo version", meta_file.version]
60
- t.add_separator
61
- t.add_row ["Main Repo", commit_info(main_repo.head.commit_id, (main_repo_branch.name rescue nil))]
62
- t.add_separator
63
- LockFile.new(".").load_entries.each do |lock_entry|
64
- branch_name = lock_entry.branch
65
- t.add_row [lock_entry.name, commit_info(lock_entry.head, branch_name)]
66
- end
67
- end
68
- puts table
69
- end
70
-
71
- def commit_info(commit_id, branch_name)
72
- commit_id + (branch_name ? " (on branch #{branch_name})" : "")
73
- end
74
-
75
- def full_install
76
- install_dependencies_step
77
- install_hooks_step unless @ci
78
- update_gitconfigs_step unless @ci
79
- end
80
-
81
- def install_dependencies_step
82
- # Read config entries as-is on disk, without prior checkout
83
- config_entries = ConfigFile.new(".").load_entries
84
- Console.log_substep("Installing #{config_entries.count} dependencies...");
85
-
86
- # Clone or fetch all configured dependencies to make sure nothing is missing locally
87
- Performer.dependencies.each { |d| clone_or_fetch(d) }
88
-
89
- # Checkout the appropriate branches as specified in the lock file
90
- checkout_command = CheckoutCommand.new(CLAide::ARGV.new([]))
91
- mode = @ci ? RevisionSelectionMode::AS_LOCK : RevisionSelectionMode::LATEST
92
- checkout_command.dependencies_checkout_step(mode)
93
- end
94
-
95
- def install_hooks_step
96
- perform_in_main_repo_and_dependencies("Installed git hooks") { |repo| install_hooks(repo) }
97
- end
98
-
99
- def update_gitconfigs_step
100
- perform_in_main_repo_and_dependencies("Updated .git/config file") { |repo| update_gitconfig(repo) }
101
- end
102
-
103
- def perform_in_main_repo_and_dependencies(message_prefix, &operation)
104
- operation.call(".")
105
- Console.log_substep("#{message_prefix} in main repo")
106
-
107
- multirepo_enabled_dependencies.each do |config_entry|
108
- operation.call(config_entry.repo.path)
109
- Console.log_substep("#{message_prefix} in multirepo-enabled dependency '#{config_entry.repo.path}'")
110
- end
111
- end
112
-
113
- # Repo operations
114
-
115
- def clone_or_fetch(dependency)
116
- if dependency.config_entry.repo.exists?
117
- check_repo_validity(dependency)
118
-
119
- Console.log_substep("Working copy '#{dependency.config_entry.repo.path}' already exists, fetching...")
120
- fetch_repo(dependency)
121
- else
122
- Console.log_substep("Cloning #{dependency.config_entry.url} into '#{dependency.config_entry.repo.path}'")
123
- clone_repo(dependency)
124
- end
125
- end
126
-
127
- def fetch_repo(dependency)
128
- unless dependency.config_entry.repo.fetch
129
- raise MultiRepoException, "Could not fetch from remote #{dependency.config_entry.repo.remote('origin').url}"
130
- end
131
- end
132
-
133
- def clone_repo(dependency)
134
- unless dependency.config_entry.repo.clone(dependency.config_entry.url, dependency.lock_entry.branch)
135
- raise MultiRepoException, "Could not clone remote #{dependency.config_entry.url} with branch #{dependency.config_entry.branch}"
136
- end
137
- end
138
-
139
- # Validation
140
-
141
- def check_repo_validity(dependency)
142
- unless dependency.config_entry.repo.remote("origin").url == dependency.config_entry.url
143
- raise MultiRepoException, "'#{dependency.config_entry.path}' origin URL (#{dependency.config_entry.repo.remote('origin').url}) does not match entry (#{dependency.config_entry.url})!"
144
- end
145
- end
146
- end
1
+ require "terminal-table"
2
+
3
+ require "multirepo/utility/console"
4
+ require "multirepo/utility/utils"
5
+ require "multirepo/git/repo"
6
+ require "multirepo/logic/performer"
7
+ require "multirepo/commands/checkout-command"
8
+
9
+ module MultiRepo
10
+ class InstallCommand < Command
11
+ self.command = "install"
12
+ self.summary = "Clones and checks out dependencies as defined in the version-controlled multirepo metadata files and installs git-multirepo's local git hooks."
13
+
14
+ def self.options
15
+ [
16
+ ['[--hooks]', 'Only install local git hooks.'],
17
+ ['[--ci]', 'Perform a continuous-integration-aware install (such as on a CI build server or agent).']
18
+ ].concat(super)
19
+ end
20
+
21
+ def initialize(argv)
22
+ @hooks = argv.flag?("hooks")
23
+ @ci = argv.flag?("ci")
24
+ super
25
+ end
26
+
27
+ def validate!
28
+ super
29
+ unless validate_only_one_flag(@hooks, @ci)
30
+ help! "You can't provide more than one operation modifier (--hooks, --ci, etc.)"
31
+ end
32
+ end
33
+
34
+ def run
35
+ ensure_in_work_tree unless @ci
36
+ ensure_multirepo_tracked
37
+
38
+ if @hooks
39
+ Console.log_step("Installing hooks in main repo and all dependencies...")
40
+ install_hooks_step
41
+ else
42
+ Console.log_step("Installing dependencies...")
43
+ log_ci_info if @ci
44
+ full_install
45
+ end
46
+
47
+ Console.log_step("Done!")
48
+ end
49
+
50
+ def log_ci_info
51
+ Console.log_warning("Performing continuous-integration-aware install")
52
+ Console.log_info("Using git-multirepo #{MultiRepo::VERSION}")
53
+
54
+ main_repo = Repo.new(".")
55
+ main_repo_branch = main_repo.current_branch
56
+ meta_file = MetaFile.new(".").load
57
+
58
+ table = Terminal::Table.new do |t|
59
+ t.title = "Revision Info"
60
+ t.add_row ["Tracked Using", "git-multirepo #{meta_file.version}"]
61
+ t.add_separator
62
+ t.add_row ["Main Repo", commit_info(main_repo.head.commit_id, (main_repo_branch.name rescue nil))]
63
+ t.add_separator
64
+ LockFile.new(".").load_entries.each do |lock_entry|
65
+ branch_name = lock_entry.branch
66
+ t.add_row [lock_entry.name, commit_info(lock_entry.head, branch_name)]
67
+ end
68
+ end
69
+ puts table
70
+ end
71
+
72
+ def commit_info(commit_id, branch_name)
73
+ commit_id + (branch_name ? " (on branch #{branch_name})" : "")
74
+ end
75
+
76
+ def full_install
77
+ install_dependencies_step
78
+ install_hooks_step unless @ci
79
+ update_gitconfigs_step unless @ci
80
+ end
81
+
82
+ def install_dependencies_step
83
+ # Read config entries as-is on disk, without prior checkout
84
+ config_entries = ConfigFile.new(".").load_entries
85
+ Console.log_substep("Installing #{config_entries.count} dependencies...");
86
+
87
+ # Clone or fetch all configured dependencies to make sure nothing is missing locally
88
+ Performer.dependencies.each { |d| clone_or_fetch(d) }
89
+
90
+ # Checkout the appropriate branches as specified in the lock file
91
+ checkout_command = CheckoutCommand.new(CLAide::ARGV.new([]))
92
+ mode = @ci ? RevisionSelectionMode::AS_LOCK : RevisionSelectionMode::LATEST
93
+ checkout_command.dependencies_checkout_step(mode)
94
+ end
95
+
96
+ def install_hooks_step
97
+ perform_in_main_repo_and_dependencies("Installed git hooks") { |repo| install_hooks(repo) }
98
+ end
99
+
100
+ def update_gitconfigs_step
101
+ perform_in_main_repo_and_dependencies("Updated .git/config file") { |repo| update_gitconfig(repo) }
102
+ end
103
+
104
+ def perform_in_main_repo_and_dependencies(message_prefix, &operation)
105
+ operation.call(".")
106
+ Console.log_substep("#{message_prefix} in main repo")
107
+
108
+ multirepo_enabled_dependencies.each do |config_entry|
109
+ operation.call(config_entry.repo.path)
110
+ Console.log_substep("#{message_prefix} in multirepo-enabled dependency '#{config_entry.repo.path}'")
111
+ end
112
+ end
113
+
114
+ # Repo operations
115
+
116
+ def clone_or_fetch(dependency)
117
+ if dependency.config_entry.repo.exists?
118
+ check_repo_validity(dependency)
119
+
120
+ Console.log_substep("Working copy '#{dependency.config_entry.repo.path}' already exists, fetching...")
121
+ fetch_repo(dependency)
122
+ else
123
+ Console.log_substep("Cloning #{dependency.config_entry.url} into '#{dependency.config_entry.repo.path}'")
124
+ clone_repo(dependency)
125
+ end
126
+ end
127
+
128
+ def fetch_repo(dependency)
129
+ unless dependency.config_entry.repo.fetch
130
+ raise MultiRepoException, "Could not fetch from remote #{dependency.config_entry.repo.remote('origin').url}"
131
+ end
132
+ end
133
+
134
+ def clone_repo(dependency)
135
+ unless dependency.config_entry.repo.clone(dependency.config_entry.url, dependency.lock_entry.branch)
136
+ raise MultiRepoException, "Could not clone remote #{dependency.config_entry.url} with branch #{dependency.config_entry.branch}"
137
+ end
138
+ end
139
+
140
+ # Validation
141
+
142
+ def check_repo_validity(dependency)
143
+ unless dependency.config_entry.repo.remote("origin").url == dependency.config_entry.url
144
+ raise MultiRepoException, "'#{dependency.config_entry.path}' origin URL (#{dependency.config_entry.repo.remote('origin').url}) does not match entry (#{dependency.config_entry.url})!"
145
+ end
146
+ end
147
+ end
147
148
  end
@@ -1,226 +1,226 @@
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
- lock_file = LockFile.new(".")
78
-
79
- # Ensure the main repo is clean
80
- raise 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
- raise 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
- raise MultiRepoException, result.message
102
- when MergeValidationResult::PROCEED
103
- raise MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Proceed?")
104
- Console.log_warning(result.message) if result.message
105
- break
106
- when MergeValidationResult::MERGE_UPSTREAM
107
- Console.log_warning(result.message)
108
- raise MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Merge upstream instead of local branches?")
109
- # TODO: Modify operations!
110
- raise MultiRepoException, "Fallback behavior not implemented. Please merge manually."
111
- next
112
- end
113
-
114
- raise MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Proceed?")
115
- end
116
-
117
- Console.log_step("Performing merge...")
118
-
119
- # Merge dependencies and the main repo
120
- perform_merges(descriptors)
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
- Performer.perform_main_repo_checkout(main_repo, ref_name, "Checked out main repo '#{ref_name}' to inspect to-merge dependencies")
129
-
130
- # List dependencies for the ref we're trying to merge
131
- their_dependencies = Performer.dependencies
132
-
133
- # Checkout the initial revision ASAP
134
- Performer.perform_main_repo_checkout(main_repo, initial_revision, "Checked out initial main repo revision '#{initial_revision}'")
135
-
136
- # Auto-merge would be too complex to implement (due to lots of edge cases)
137
- # if the specified ref does not have the same dependencies. Better perform a manual merge.
138
- ensure_dependencies_match(our_dependencies, their_dependencies)
139
-
140
- # Create a merge descriptor for each would-be merge as well as the main repo.
141
- # This step MUST be performed in OUR revision for the merge descriptors to be correct!
142
- descriptors = build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
143
- descriptors.push(MergeDescriptor.new("Main Repo", main_repo, initial_revision, ref_name))
144
-
145
- return descriptors
146
- end
147
-
148
- def build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
149
- descriptors = []
150
- our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
151
- our_revision = our_dependency.config_entry.repo.current_revision
152
-
153
- their_revision = RevisionSelector.revision_for_mode(mode, ref_name, their_dependency.lock_entry)
154
- their_name = their_dependency.config_entry.name
155
- their_repo = their_dependency.config_entry.repo
156
-
157
- descriptor = MergeDescriptor.new(their_name, their_repo, our_revision, their_revision)
158
-
159
- descriptors.push(descriptor)
160
- end
161
- return descriptors
162
- end
163
-
164
- def ensure_dependencies_match(our_dependencies, their_dependencies)
165
- our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
166
- if their_dependency == nil || their_dependency.config_entry.id != our_dependency.config_entry.id
167
- raise MultiRepoException, "Dependencies differ, please merge manually"
168
- end
169
- end
170
-
171
- if their_dependencies.count > our_dependencies.count
172
- raise MultiRepoException, "There are more dependencies in the specified ref, please merge manually"
173
- end
174
- end
175
-
176
- def preview_merge(descriptors, mode, ref_name)
177
- Console.log_info("Merging would #{message_for_mode(mode, ref_name)}:")
178
-
179
- table = Terminal::Table.new do |t|
180
- descriptors.reverse.each_with_index do |descriptor, index|
181
- t.add_row [descriptor.name.bold, descriptor.merge_description, descriptor.upstream_description]
182
- t.add_separator unless index == descriptors.count - 1
183
- end
184
- end
185
- puts table
186
- end
187
-
188
- def ensure_merge_valid(descriptors)
189
- outcome = MergeValidationResult.new
190
- outcome.outcome = MergeValidationResult::PROCEED
191
-
192
- if descriptors.any? { |d| d.state == TheirState::LOCAL_NO_UPSTREAM }
193
- outcome.message = "Some branches are not remote-tracking! Please review the merge operations above."
194
- elsif descriptors.any? { |d| d.state == TheirState::LOCAL_UPSTREAM_DIVERGED }
195
- outcome.outcome = MergeValidationResult::ABORT
196
- outcome.message = "Some upstream branches have diverged. This warrants a manual merge!"
197
- elsif descriptors.any? { |d| d.state == TheirState::LOCAL_OUTDATED }
198
- outcome.outcome = MergeValidationResult::MERGE_UPSTREAM
199
- outcome.message = "Some local branches are outdated."
200
- end
201
-
202
- return outcome
203
- end
204
-
205
- def perform_merges(descriptors)
206
- success = true
207
- descriptors.each do |descriptor|
208
- Console.log_substep("#{descriptor.name} : Merging #{descriptor.their_revision} into #{descriptor.our_revision}...")
209
- GitRunner.run_in_working_dir(descriptor.repo.path, "merge #{descriptor.their_revision}", Runner::Verbosity::OUTPUT_ALWAYS)
210
- success &= GitRunner.last_command_succeeded
211
- end
212
- Console.log_warning("Some merge operations failed. Please review the above results.") unless success
213
- end
214
-
215
- def message_for_mode(mode, ref_name)
216
- case mode
217
- when RevisionSelectionMode::AS_LOCK
218
- "merge specific commits as stored in the lock file for main repo revision #{ref_name}"
219
- when RevisionSelectionMode::LATEST
220
- "merge each branch as stored in the lock file of main repo revision #{ref_name}"
221
- when RevisionSelectionMode::EXACT
222
- "merge #{ref_name} for each repository, ignoring the contents of the lock file"
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
+
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
+ lock_file = LockFile.new(".")
78
+
79
+ # Ensure the main repo is clean
80
+ raise 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
+ raise 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
+ raise MultiRepoException, result.message
102
+ when MergeValidationResult::PROCEED
103
+ raise MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Proceed?")
104
+ Console.log_warning(result.message) if result.message
105
+ break
106
+ when MergeValidationResult::MERGE_UPSTREAM
107
+ Console.log_warning(result.message)
108
+ raise MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Merge upstream instead of local branches?")
109
+ # TODO: Modify operations!
110
+ raise MultiRepoException, "Fallback behavior not implemented. Please merge manually."
111
+ next
112
+ end
113
+
114
+ raise MultiRepoException, "Merge aborted" unless Console.ask_yes_no("Proceed?")
115
+ end
116
+
117
+ Console.log_step("Performing merge...")
118
+
119
+ # Merge dependencies and the main repo
120
+ perform_merges(descriptors)
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
+ Performer.perform_main_repo_checkout(main_repo, ref_name, "Checked out main repo '#{ref_name}' to inspect to-merge dependencies")
129
+
130
+ # List dependencies for the ref we're trying to merge
131
+ their_dependencies = Performer.dependencies
132
+
133
+ # Checkout the initial revision ASAP
134
+ Performer.perform_main_repo_checkout(main_repo, initial_revision, "Checked out initial main repo revision '#{initial_revision}'")
135
+
136
+ # Auto-merge would be too complex to implement (due to lots of edge cases)
137
+ # if the specified ref does not have the same dependencies. Better perform a manual merge.
138
+ ensure_dependencies_match(our_dependencies, their_dependencies)
139
+
140
+ # Create a merge descriptor for each would-be merge as well as the main repo.
141
+ # This step MUST be performed in OUR revision for the merge descriptors to be correct!
142
+ descriptors = build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
143
+ descriptors.push(MergeDescriptor.new("Main Repo", main_repo, initial_revision, ref_name))
144
+
145
+ return descriptors
146
+ end
147
+
148
+ def build_dependency_merge_descriptors(our_dependencies, their_dependencies, ref_name, mode)
149
+ descriptors = []
150
+ our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
151
+ our_revision = our_dependency.config_entry.repo.current_revision
152
+
153
+ their_revision = RevisionSelector.revision_for_mode(mode, ref_name, their_dependency.lock_entry)
154
+ their_name = their_dependency.config_entry.name
155
+ their_repo = their_dependency.config_entry.repo
156
+
157
+ descriptor = MergeDescriptor.new(their_name, their_repo, our_revision, their_revision)
158
+
159
+ descriptors.push(descriptor)
160
+ end
161
+ return descriptors
162
+ end
163
+
164
+ def ensure_dependencies_match(our_dependencies, their_dependencies)
165
+ our_dependencies.zip(their_dependencies).each do |our_dependency, their_dependency|
166
+ if their_dependency == nil || their_dependency.config_entry.id != our_dependency.config_entry.id
167
+ raise MultiRepoException, "Dependencies differ, please merge manually"
168
+ end
169
+ end
170
+
171
+ if their_dependencies.count > our_dependencies.count
172
+ raise MultiRepoException, "There are more dependencies in the specified ref, please merge manually"
173
+ end
174
+ end
175
+
176
+ def preview_merge(descriptors, mode, ref_name)
177
+ Console.log_info("Merging would #{message_for_mode(mode, ref_name)}:")
178
+
179
+ table = Terminal::Table.new do |t|
180
+ descriptors.reverse.each_with_index do |descriptor, index|
181
+ t.add_row [descriptor.name.bold, descriptor.merge_description, descriptor.upstream_description]
182
+ t.add_separator unless index == descriptors.count - 1
183
+ end
184
+ end
185
+ puts table
186
+ end
187
+
188
+ def ensure_merge_valid(descriptors)
189
+ outcome = MergeValidationResult.new
190
+ outcome.outcome = MergeValidationResult::PROCEED
191
+
192
+ if descriptors.any? { |d| d.state == TheirState::LOCAL_NO_UPSTREAM }
193
+ outcome.message = "Some branches are not remote-tracking! Please review the merge operations above."
194
+ elsif descriptors.any? { |d| d.state == TheirState::LOCAL_UPSTREAM_DIVERGED }
195
+ outcome.outcome = MergeValidationResult::ABORT
196
+ outcome.message = "Some upstream branches have diverged. This warrants a manual merge!"
197
+ elsif descriptors.any? { |d| d.state == TheirState::LOCAL_OUTDATED }
198
+ outcome.outcome = MergeValidationResult::MERGE_UPSTREAM
199
+ outcome.message = "Some local branches are outdated."
200
+ end
201
+
202
+ return outcome
203
+ end
204
+
205
+ def perform_merges(descriptors)
206
+ success = true
207
+ descriptors.each do |descriptor|
208
+ Console.log_substep("#{descriptor.name} : Merging #{descriptor.their_revision} into #{descriptor.our_revision}...")
209
+ GitRunner.run_as_system(descriptor.repo.path, "merge #{descriptor.their_revision}")
210
+ success &= GitRunner.last_command_succeeded
211
+ end
212
+ Console.log_warning("Some merge operations failed. Please review the above results.") unless success
213
+ end
214
+
215
+ def message_for_mode(mode, ref_name)
216
+ case mode
217
+ when RevisionSelectionMode::AS_LOCK
218
+ "merge specific commits as stored in the lock file for main repo revision #{ref_name}"
219
+ when RevisionSelectionMode::LATEST
220
+ "merge each branch as stored in the lock file of main repo revision #{ref_name}"
221
+ when RevisionSelectionMode::EXACT
222
+ "merge #{ref_name} for each repository, ignoring the contents of the lock file"
223
+ end
224
+ end
225
+ end
226
226
  end