git-multirepo 1.0.0.beta42 → 1.0.0.beta43
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.
- checksums.yaml +4 -4
- data/.gitattributes +2 -2
- data/.gitbugtraq +3 -3
- data/.gitignore +38 -38
- data/.multirepo.meta +2 -2
- data/.rspec +2 -2
- data/Gemfile +4 -4
- data/Gemfile.lock +42 -42
- data/LICENSE +22 -22
- data/README.md +143 -143
- data/Rakefile +2 -2
- data/bin/multi +10 -10
- data/docs/bug-repros/91565510-repro.sh +20 -20
- data/git-multirepo.gemspec +31 -31
- data/lib/commands.rb +13 -13
- data/lib/git-multirepo.rb +2 -2
- data/lib/info.rb +4 -4
- data/lib/multirepo/commands/add-command.rb +50 -50
- data/lib/multirepo/commands/branch-command.rb +81 -81
- data/lib/multirepo/commands/checkout-command.rb +119 -119
- data/lib/multirepo/commands/clone-command.rb +67 -67
- data/lib/multirepo/commands/command.rb +89 -89
- data/lib/multirepo/commands/do-command.rb +100 -100
- data/lib/multirepo/commands/graph-command.rb +42 -42
- data/lib/multirepo/commands/init-command.rb +119 -119
- data/lib/multirepo/commands/install-command.rb +106 -106
- data/lib/multirepo/commands/merge-command.rb +225 -225
- data/lib/multirepo/commands/open-command.rb +55 -55
- data/lib/multirepo/commands/remove-command.rb +47 -47
- data/lib/multirepo/commands/uninit-command.rb +17 -17
- data/lib/multirepo/commands/update-command.rb +55 -55
- data/lib/multirepo/config.rb +15 -15
- data/lib/multirepo/files/config-entry.rb +38 -38
- data/lib/multirepo/files/config-file.rb +45 -45
- data/lib/multirepo/files/lock-entry.rb +28 -28
- data/lib/multirepo/files/lock-file.rb +55 -55
- data/lib/multirepo/files/meta-file.rb +40 -40
- data/lib/multirepo/files/tracking-file.rb +8 -8
- data/lib/multirepo/files/tracking-files.rb +46 -46
- data/lib/multirepo/git/branch.rb +31 -31
- data/lib/multirepo/git/change.rb +10 -10
- data/lib/multirepo/git/commit.rb +6 -6
- data/lib/multirepo/git/git-runner.rb +46 -46
- data/lib/multirepo/git/git.rb +9 -9
- data/lib/multirepo/git/ref.rb +37 -37
- data/lib/multirepo/git/remote.rb +16 -16
- data/lib/multirepo/git/repo.rb +122 -122
- data/lib/multirepo/hooks/post-commit-hook.rb +22 -22
- data/lib/multirepo/hooks/pre-commit-hook.rb +34 -34
- data/lib/multirepo/logic/dependency.rb +5 -5
- data/lib/multirepo/logic/merge-descriptor.rb +94 -94
- data/lib/multirepo/logic/node.rb +71 -71
- data/lib/multirepo/logic/performer.rb +56 -56
- data/lib/multirepo/logic/revision-selector.rb +34 -34
- data/lib/multirepo/multirepo-exception.rb +5 -5
- data/lib/multirepo/utility/console.rb +51 -51
- data/lib/multirepo/utility/runner.rb +34 -34
- data/lib/multirepo/utility/utils.rb +94 -94
- data/resources/.gitconfig +2 -2
- data/resources/post-commit +5 -5
- data/resources/pre-commit +5 -5
- data/spec/integration/init_spec.rb +18 -18
- data/spec/spec_helper.rb +89 -89
- metadata +2 -2
@@ -1,107 +1,107 @@
|
|
1
|
-
require "multirepo/utility/console"
|
2
|
-
require "multirepo/utility/utils"
|
3
|
-
require "multirepo/git/repo"
|
4
|
-
require "multirepo/logic/performer"
|
5
|
-
require "multirepo/commands/checkout-command"
|
6
|
-
|
7
|
-
module MultiRepo
|
8
|
-
class InstallCommand < Command
|
9
|
-
self.command = "install"
|
10
|
-
self.summary = "Clones and checks out dependencies as defined in the version-controlled multirepo metadata files and installs git-multirepo's local git hooks."
|
11
|
-
|
12
|
-
def self.options
|
13
|
-
[['[--hooks]', 'Only install local git hooks.']].concat(super)
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(argv)
|
17
|
-
@hooks = argv.flag?("hooks")
|
18
|
-
super
|
19
|
-
end
|
20
|
-
|
21
|
-
def run
|
22
|
-
ensure_in_work_tree
|
23
|
-
ensure_multirepo_tracked
|
24
|
-
|
25
|
-
if @hooks
|
26
|
-
Console.log_step("Installing hooks in main repo and all dependencies...")
|
27
|
-
install_hooks_step
|
28
|
-
else
|
29
|
-
Console.log_step("Cloning dependencies and installing hooks...")
|
30
|
-
full_install
|
31
|
-
end
|
32
|
-
|
33
|
-
Console.log_step("Done!")
|
34
|
-
end
|
35
|
-
|
36
|
-
def full_install
|
37
|
-
install_dependencies_step
|
38
|
-
install_hooks_step
|
39
|
-
update_gitconfigs_step
|
40
|
-
end
|
41
|
-
|
42
|
-
def install_dependencies_step
|
43
|
-
# Read config entries as-is on disk, without prior checkout
|
44
|
-
config_entries = ConfigFile.new(".").load_entries
|
45
|
-
Console.log_substep("Installing #{config_entries.count} dependencies...");
|
46
|
-
|
47
|
-
# Clone or fetch all configured dependencies to make sure nothing is missing locally
|
48
|
-
Performer.dependencies.each { |d| clone_or_fetch(d) }
|
49
|
-
|
50
|
-
# Checkout the appropriate branches as specified in the lock file
|
51
|
-
checkout_command = CheckoutCommand.new(CLAide::ARGV.new([]))
|
52
|
-
checkout_command.dependencies_checkout_step(RevisionSelectionMode::LATEST)
|
53
|
-
end
|
54
|
-
|
55
|
-
def install_hooks_step
|
56
|
-
perform_in_main_repo_and_dependencies("Installed git hooks") { |repo| install_hooks(repo) }
|
57
|
-
end
|
58
|
-
|
59
|
-
def update_gitconfigs_step
|
60
|
-
perform_in_main_repo_and_dependencies("Updated .git/config file") { |repo| update_gitconfig(repo) }
|
61
|
-
end
|
62
|
-
|
63
|
-
def perform_in_main_repo_and_dependencies(message_prefix, &operation)
|
64
|
-
operation.call(".")
|
65
|
-
Console.log_substep("#{message_prefix} in main repo")
|
66
|
-
|
67
|
-
multirepo_enabled_dependencies.each do |config_entry|
|
68
|
-
operation.call(config_entry.repo.path)
|
69
|
-
Console.log_substep("#{message_prefix} in multirepo-enabled dependency '#{config_entry.repo.path}'")
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# Repo operations
|
74
|
-
|
75
|
-
def clone_or_fetch(dependency)
|
76
|
-
if dependency.config_entry.repo.exists?
|
77
|
-
check_repo_validity(dependency)
|
78
|
-
|
79
|
-
Console.log_substep("Working copy '#{dependency.config_entry.repo.path}' already exists, fetching instead...")
|
80
|
-
fetch_repo(dependency)
|
81
|
-
else
|
82
|
-
Console.log_substep("Cloning #{dependency.config_entry.url} into '#{dependency.config_entry.repo.path}'")
|
83
|
-
clone_repo(dependency)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def fetch_repo(dependency)
|
88
|
-
unless dependency.config_entry.repo.fetch
|
89
|
-
raise MultiRepoException, "Could not fetch from remote #{dependency.config_entry.repo.remote('origin').url}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def clone_repo(dependency)
|
94
|
-
unless dependency.config_entry.repo.clone(dependency.config_entry.url, dependency.lock_entry.branch)
|
95
|
-
raise MultiRepoException, "Could not clone remote #{dependency.config_entry.url} with branch #{dependency.config_entry.branch}"
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# Validation
|
100
|
-
|
101
|
-
def check_repo_validity(dependency)
|
102
|
-
unless dependency.config_entry.repo.remote("origin").url == dependency.config_entry.url
|
103
|
-
raise MultiRepoException, "'#{dependency.config_entry.path}' origin URL (#{dependency.config_entry.repo.remote('origin').url}) does not match entry (#{dependency.config_entry.url})!"
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
1
|
+
require "multirepo/utility/console"
|
2
|
+
require "multirepo/utility/utils"
|
3
|
+
require "multirepo/git/repo"
|
4
|
+
require "multirepo/logic/performer"
|
5
|
+
require "multirepo/commands/checkout-command"
|
6
|
+
|
7
|
+
module MultiRepo
|
8
|
+
class InstallCommand < Command
|
9
|
+
self.command = "install"
|
10
|
+
self.summary = "Clones and checks out dependencies as defined in the version-controlled multirepo metadata files and installs git-multirepo's local git hooks."
|
11
|
+
|
12
|
+
def self.options
|
13
|
+
[['[--hooks]', 'Only install local git hooks.']].concat(super)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(argv)
|
17
|
+
@hooks = argv.flag?("hooks")
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
ensure_in_work_tree
|
23
|
+
ensure_multirepo_tracked
|
24
|
+
|
25
|
+
if @hooks
|
26
|
+
Console.log_step("Installing hooks in main repo and all dependencies...")
|
27
|
+
install_hooks_step
|
28
|
+
else
|
29
|
+
Console.log_step("Cloning dependencies and installing hooks...")
|
30
|
+
full_install
|
31
|
+
end
|
32
|
+
|
33
|
+
Console.log_step("Done!")
|
34
|
+
end
|
35
|
+
|
36
|
+
def full_install
|
37
|
+
install_dependencies_step
|
38
|
+
install_hooks_step
|
39
|
+
update_gitconfigs_step
|
40
|
+
end
|
41
|
+
|
42
|
+
def install_dependencies_step
|
43
|
+
# Read config entries as-is on disk, without prior checkout
|
44
|
+
config_entries = ConfigFile.new(".").load_entries
|
45
|
+
Console.log_substep("Installing #{config_entries.count} dependencies...");
|
46
|
+
|
47
|
+
# Clone or fetch all configured dependencies to make sure nothing is missing locally
|
48
|
+
Performer.dependencies.each { |d| clone_or_fetch(d) }
|
49
|
+
|
50
|
+
# Checkout the appropriate branches as specified in the lock file
|
51
|
+
checkout_command = CheckoutCommand.new(CLAide::ARGV.new([]))
|
52
|
+
checkout_command.dependencies_checkout_step(RevisionSelectionMode::LATEST)
|
53
|
+
end
|
54
|
+
|
55
|
+
def install_hooks_step
|
56
|
+
perform_in_main_repo_and_dependencies("Installed git hooks") { |repo| install_hooks(repo) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_gitconfigs_step
|
60
|
+
perform_in_main_repo_and_dependencies("Updated .git/config file") { |repo| update_gitconfig(repo) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def perform_in_main_repo_and_dependencies(message_prefix, &operation)
|
64
|
+
operation.call(".")
|
65
|
+
Console.log_substep("#{message_prefix} in main repo")
|
66
|
+
|
67
|
+
multirepo_enabled_dependencies.each do |config_entry|
|
68
|
+
operation.call(config_entry.repo.path)
|
69
|
+
Console.log_substep("#{message_prefix} in multirepo-enabled dependency '#{config_entry.repo.path}'")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Repo operations
|
74
|
+
|
75
|
+
def clone_or_fetch(dependency)
|
76
|
+
if dependency.config_entry.repo.exists?
|
77
|
+
check_repo_validity(dependency)
|
78
|
+
|
79
|
+
Console.log_substep("Working copy '#{dependency.config_entry.repo.path}' already exists, fetching instead...")
|
80
|
+
fetch_repo(dependency)
|
81
|
+
else
|
82
|
+
Console.log_substep("Cloning #{dependency.config_entry.url} into '#{dependency.config_entry.repo.path}'")
|
83
|
+
clone_repo(dependency)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fetch_repo(dependency)
|
88
|
+
unless dependency.config_entry.repo.fetch
|
89
|
+
raise MultiRepoException, "Could not fetch from remote #{dependency.config_entry.repo.remote('origin').url}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def clone_repo(dependency)
|
94
|
+
unless dependency.config_entry.repo.clone(dependency.config_entry.url, dependency.lock_entry.branch)
|
95
|
+
raise MultiRepoException, "Could not clone remote #{dependency.config_entry.url} with branch #{dependency.config_entry.branch}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Validation
|
100
|
+
|
101
|
+
def check_repo_validity(dependency)
|
102
|
+
unless dependency.config_entry.repo.remote("origin").url == dependency.config_entry.url
|
103
|
+
raise MultiRepoException, "'#{dependency.config_entry.path}' origin URL (#{dependency.config_entry.repo.remote('origin').url}) does not match entry (#{dependency.config_entry.url})!"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
107
|
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_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
|
226
226
|
end
|