repository_merger 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1490218b0506f8561f3c7e6044783d6b583ceff8ff0fbb2f163fbcb3e793b323
4
+ data.tar.gz: 9766418d0125e6b70670bbf7e132bd07ab322b6871ccdda2c7c71b07d31d8b5e
5
+ SHA512:
6
+ metadata.gz: cbe57c8d914f2669b1dd39e8a70fabff6db161a544d354d56895a221dc641024fe1b3e19143822aa59c186fdd8dd583463afeb427f49f49a95bd1b96588d7490
7
+ data.tar.gz: 498183071861f94ded94f03c398391521a51fcf32e9f45437e8568be9c7dcf9f7df059761c36a7eb24e2473c19ba8d5de0f78c86b2ef66d86e03bd2a8fbaf895
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,30 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - 'dest/**/*'
6
+ - 'tmp/**/*'
7
+ - 'vendor/**/*'
8
+ NewCops: enable
9
+ SuggestExtensions: false
10
+
11
+ Layout/FirstArrayElementIndentation:
12
+ EnforcedStyle: consistent
13
+
14
+ Layout/FirstHashElementIndentation:
15
+ EnforcedStyle: consistent
16
+
17
+ Layout/HashAlignment:
18
+ EnforcedHashRocketStyle: table
19
+
20
+ Naming/HeredocDelimiterNaming:
21
+ Enabled: false
22
+
23
+ Style/Documentation:
24
+ Enabled: false
25
+
26
+ Style/EmptyElse:
27
+ Enabled: false
28
+
29
+ Style/IfUnlessModifier:
30
+ Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,91 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2021-10-03 08:53:27 UTC using RuboCop version 1.22.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 10
10
+ # Cop supports --auto-correct.
11
+ Layout/EmptyLineAfterGuardClause:
12
+ Exclude:
13
+ - 'lib/repository_merger/branch_local_commit_map.rb'
14
+ - 'lib/repository_merger/github_issue_reference.rb'
15
+ - 'lib/repository_merger/logger.rb'
16
+ - 'lib/repository_merger/mono_repository.rb'
17
+ - 'lib/repository_merger/repository.rb'
18
+ - 'lib/repository_merger/repository_commit_map.rb'
19
+ - 'spec/support/fixture_helper.rb'
20
+ - 'spec/support/git_helper.rb'
21
+
22
+ # Offense count: 8
23
+ # Cop supports --auto-correct.
24
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
25
+ # SupportedHashRocketStyles: key, separator, table
26
+ # SupportedColonStyles: key, separator, table
27
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
28
+ Layout/HashAlignment:
29
+ Exclude:
30
+ - 'spec/rspec/monorepo_spec.rb'
31
+ - 'spec/rspec/original_repos_spec.rb'
32
+
33
+ # Offense count: 1
34
+ # Cop supports --auto-correct.
35
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
36
+ # SupportedStyles: aligned, indented, indented_relative_to_receiver
37
+ Layout/MultilineMethodCallIndentation:
38
+ Exclude:
39
+ - 'spec/repository_merger/mono_repository_spec.rb'
40
+
41
+ # Offense count: 4
42
+ # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
43
+ Metrics/AbcSize:
44
+ Max: 42
45
+
46
+ # Offense count: 36
47
+ # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
48
+ # IgnoredMethods: refine
49
+ Metrics/BlockLength:
50
+ Max: 686
51
+
52
+ # Offense count: 4
53
+ # Configuration parameters: CountComments, CountAsOne.
54
+ Metrics/ClassLength:
55
+ Max: 442
56
+
57
+ # Offense count: 10
58
+ # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
59
+ Metrics/MethodLength:
60
+ Max: 40
61
+
62
+ # Offense count: 2
63
+ # Cop supports --auto-correct.
64
+ # Configuration parameters: EnforcedStyle, AllowInnerSlashes.
65
+ # SupportedStyles: slashes, percent_r, mixed
66
+ Style/RegexpLiteral:
67
+ Exclude:
68
+ - 'exe/merge_rspec_repos'
69
+ - 'lib/repository_merger/github_issue_reference.rb'
70
+
71
+ # Offense count: 1
72
+ # Cop supports --auto-correct.
73
+ # Configuration parameters: MinSize.
74
+ # SupportedStyles: percent, brackets
75
+ Style/SymbolArray:
76
+ EnforcedStyle: brackets
77
+
78
+ # Offense count: 5
79
+ # Cop supports --auto-correct.
80
+ # Configuration parameters: WordRegex.
81
+ # SupportedStyles: percent, brackets
82
+ Style/WordArray:
83
+ EnforcedStyle: percent
84
+ MinSize: 4
85
+
86
+ # Offense count: 14
87
+ # Cop supports --auto-correct.
88
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
89
+ # URISchemes: http, https
90
+ Layout/LineLength:
91
+ Max: 199
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem 'pry-byebug', '~> 3.9'
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rspec', '~> 3.10'
11
+ gem 'rubocop', '~> 1.22'
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,68 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ repository_merger (0.1.0)
5
+ ruby-progressbar (~> 1.11)
6
+ rugged (~> 1.2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.2)
12
+ byebug (11.1.3)
13
+ coderay (1.1.3)
14
+ diff-lcs (1.4.4)
15
+ method_source (1.0.0)
16
+ parallel (1.21.0)
17
+ parser (3.0.2.0)
18
+ ast (~> 2.4.1)
19
+ pry (0.13.1)
20
+ coderay (~> 1.1)
21
+ method_source (~> 1.0)
22
+ pry-byebug (3.9.0)
23
+ byebug (~> 11.0)
24
+ pry (~> 0.13.0)
25
+ rainbow (3.0.0)
26
+ rake (13.0.6)
27
+ regexp_parser (2.1.1)
28
+ rexml (3.2.5)
29
+ rspec (3.10.0)
30
+ rspec-core (~> 3.10.0)
31
+ rspec-expectations (~> 3.10.0)
32
+ rspec-mocks (~> 3.10.0)
33
+ rspec-core (3.10.1)
34
+ rspec-support (~> 3.10.0)
35
+ rspec-expectations (3.10.1)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.10.0)
38
+ rspec-mocks (3.10.2)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.10.0)
41
+ rspec-support (3.10.2)
42
+ rubocop (1.22.0)
43
+ parallel (~> 1.10)
44
+ parser (>= 3.0.0.0)
45
+ rainbow (>= 2.2.2, < 4.0)
46
+ regexp_parser (>= 1.8, < 3.0)
47
+ rexml
48
+ rubocop-ast (>= 1.12.0, < 2.0)
49
+ ruby-progressbar (~> 1.7)
50
+ unicode-display_width (>= 1.4.0, < 3.0)
51
+ rubocop-ast (1.12.0)
52
+ parser (>= 3.0.1.1)
53
+ ruby-progressbar (1.11.0)
54
+ rugged (1.2.0)
55
+ unicode-display_width (2.1.0)
56
+
57
+ PLATFORMS
58
+ ruby
59
+
60
+ DEPENDENCIES
61
+ pry-byebug (~> 3.9)
62
+ rake (~> 13.0)
63
+ repository_merger!
64
+ rspec (~> 3.10)
65
+ rubocop (~> 1.22)
66
+
67
+ BUNDLED WITH
68
+ 2.2.28
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Yuji Nakayama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ [![Test](https://github.com/yujinakayama/repository_merger/workflows/Test/badge.svg)](https://github.com/yujinakayama/repository_merger/actions?query=workflow%3ATest)
2
+
3
+ # RepositoryMerger
4
+
5
+ For https://github.com/rspec/rspec-core/issues/2509
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ desc 'Remove all generated files including monorepo'
6
+ task :clean do
7
+ require 'fileutils'
8
+
9
+ Dir.chdir(__dir__) do
10
+ FileUtils.rm_rf(['dest', 'tmp'])
11
+ end
12
+ end
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require_relative '../lib/repository_merger'
6
+ require_relative '../lib/repository_merger/github_issue_reference'
7
+ require 'pry'
8
+
9
+ def main
10
+ Signal.trap('INT') do
11
+ puts 'Aborting...'
12
+ exit(1)
13
+ end
14
+
15
+ Dir.chdir(dest_dir)
16
+
17
+ total_import_phase_count = tag_names_unreachable_from_target_branches.size + target_branch_names.size
18
+ current_import_phase_number = 1
19
+
20
+ tag_names_unreachable_from_target_branches.each do |tag_name|
21
+ tags = configuration.original_repos.map { |repo| repo.tag_for(tag_name) }.compact
22
+
23
+ repo_merger.merge_commit_history_of(
24
+ tags,
25
+ commit_message_conversion: method(:convert_commit_message),
26
+ progress_title: "[#{current_import_phase_number}/#{total_import_phase_count}: #{tag_name}]"
27
+ )
28
+
29
+ # We need to import the tags here before importing target branches
30
+ # so that the tags will reference right commits.
31
+ repo_merger.import_tags(tags, tag_name_conversion: method(:convert_tag_name))
32
+
33
+ current_import_phase_number += 1
34
+ end
35
+
36
+ target_branch_names.each do |branch_name|
37
+ repo_merger.merge_commit_history_of_branches_named(
38
+ branch_name,
39
+ commit_message_conversion: method(:convert_commit_message),
40
+ progress_title: "[#{current_import_phase_number}/#{total_import_phase_count}: #{branch_name}]"
41
+ )
42
+
43
+ current_import_phase_number += 1
44
+ end
45
+
46
+ repo_merger.import_all_tags(tag_name_conversion: method(:convert_tag_name))
47
+
48
+ Dir.chdir(configuration.monorepo_path) do
49
+ # Clear index and working tree since they're cluttered after the merge
50
+ `git reset --hard`
51
+
52
+ # Merged repos without GC tend to have large volume
53
+ puts 'Running `git gc`...'
54
+ system('git gc')
55
+ end
56
+ end
57
+
58
+ def repo_merger
59
+ @repo_merger ||= RepositoryMerger.new(configuration)
60
+ end
61
+
62
+ def configuration
63
+ @configuration ||= RepositoryMerger::Configuration.new(
64
+ original_repo_paths: fetch_original_repos_if_needed,
65
+ monorepo_path: create_monorepo_if_needed,
66
+ verbose_logging: ARGV.include?('-v')
67
+ )
68
+ end
69
+
70
+ def target_branch_names
71
+ @target_branch_names ||= begin
72
+ all_branch_names = configuration.original_repos.flat_map { |repo| repo.branches.map(&:name) }.uniq.sort
73
+ ['origin/main'] + all_branch_names.grep(/\Aorigin\/\d+-\d+-(maintenance|stable)\z/)
74
+ end
75
+ end
76
+
77
+ def tag_names_unreachable_from_target_branches
78
+ %w[
79
+ v2.0.0.beta.9
80
+ v3.5.0.beta2
81
+ ]
82
+ end
83
+
84
+ def convert_commit_message(original_commit)
85
+ message = RepositoryMerger::GitHubIssueReference.convert_repo_local_references_to_absolute_ones_in(
86
+ original_commit.message,
87
+ username: 'rspec',
88
+ repo_name: original_commit.repo.name
89
+ )
90
+
91
+ lines = message.split(/\r?\n/)
92
+
93
+ scope = original_commit.repo.name.sub(/\Arspec-/, '')
94
+ lines.first.prepend("[#{scope}] ")
95
+
96
+ if lines.size == 1
97
+ lines << ''
98
+ else
99
+ lines.concat([
100
+ '',
101
+ '---'
102
+ ])
103
+ end
104
+
105
+ original_commit_url = "https://github.com/rspec/#{original_commit.repo.name}/commit/#{original_commit.id}"
106
+ lines << "This commit was imported from #{original_commit_url}."
107
+
108
+ lines.join("\n")
109
+ end
110
+
111
+ def convert_tag_name(original_tag)
112
+ tag_name = original_tag.name
113
+ tag_name = "v#{tag_name}" if tag_name.match?(/\A\d+\.\d+\.\d+/)
114
+
115
+ scope = original_tag.repo.name.sub(/\Arspec-/, '')
116
+
117
+ "#{tag_name}-#{scope}"
118
+ end
119
+
120
+ def dest_dir
121
+ dest_dir = 'dest'
122
+ Dir.mkdir(dest_dir) unless Dir.exist?(dest_dir)
123
+ dest_dir
124
+ end
125
+
126
+ def fetch_original_repos_if_needed
127
+ repo_urls = %w[
128
+ https://github.com/yujinakayama/rspec.git
129
+ https://github.com/yujinakayama/rspec-core.git
130
+ https://github.com/yujinakayama/rspec-expectations.git
131
+ https://github.com/yujinakayama/rspec-mocks.git
132
+ https://github.com/yujinakayama/rspec-support.git
133
+ ]
134
+
135
+ original_repos_dir = 'original_repos'
136
+
137
+ Dir.mkdir(original_repos_dir) unless Dir.exist?(original_repos_dir)
138
+
139
+ Dir.chdir(original_repos_dir) do |current_directory|
140
+ repo_urls.map do |repo_url|
141
+ repo_name = File.basename(repo_url, '.git')
142
+
143
+ unless Dir.exist?(repo_name)
144
+ system("git clone #{repo_url}")
145
+ end
146
+
147
+ File.join(current_directory, repo_name)
148
+ end
149
+ end
150
+ end
151
+
152
+ def create_monorepo_if_needed
153
+ monorepo_dir = 'monorepo'
154
+
155
+ unless Dir.exist?(monorepo_dir)
156
+ Dir.mkdir(monorepo_dir)
157
+ Dir.chdir(monorepo_dir) do
158
+ system('git init')
159
+ end
160
+ end
161
+
162
+ monorepo_dir
163
+ end
164
+
165
+ main
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'reference'
4
+
5
+ require 'rugged'
6
+ require 'set'
7
+
8
+ class RepositoryMerger
9
+ Branch = Struct.new(:rugged_branch, :repo) do
10
+ include Reference
11
+
12
+ def ==(other)
13
+ repo == other.repo && canonical_name == other.canonical_name
14
+ end
15
+
16
+ alias_method :eql?, :==
17
+
18
+ def hash
19
+ repo.hash ^ name.hash
20
+ end
21
+
22
+ def name
23
+ rugged_branch.name
24
+ end
25
+
26
+ def canonical_name
27
+ rugged_branch.canonical_name
28
+ end
29
+
30
+ def local_name
31
+ if rugged_branch.remote_name
32
+ name.delete_prefix("#{rugged_branch.remote_name}/")
33
+ else
34
+ name
35
+ end
36
+ end
37
+
38
+ def target_commit
39
+ repo.commit_for(rugged_branch.target_id)
40
+ end
41
+
42
+ def revision_id
43
+ name
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'commit_map'
4
+
5
+ class RepositoryMerger
6
+ class BranchLocalCommitMap
7
+ include CommitMap
8
+
9
+ attr_reader :monorepo
10
+
11
+ def initialize(monorepo:)
12
+ @monorepo = monorepo
13
+ end
14
+
15
+ def map
16
+ @map ||= {}
17
+ end
18
+
19
+ def register(monorepo_commit:, original_commit:)
20
+ key = original_commit_key(original_commit)
21
+ raise if map.key?(key)
22
+ map[key] = monorepo_commit.id
23
+ end
24
+
25
+ def monorepo_commit_for(original_commit)
26
+ monorepo_commit_id = monorepo_commit_id_for(original_commit)
27
+ monorepo.commit_for(monorepo_commit_id)
28
+ end
29
+
30
+ def monorepo_commit_id_for(original_commit)
31
+ map[original_commit_key(original_commit)]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RepositoryMerger
4
+ Commit = Struct.new(:rugged_commit, :repo) do
5
+ def ==(other)
6
+ id == other.id
7
+ end
8
+
9
+ alias_method :eql?, :==
10
+
11
+ def hash
12
+ id.hash
13
+ end
14
+
15
+ def id
16
+ rugged_commit.oid
17
+ end
18
+
19
+ def abbreviated_id
20
+ id[0, 7]
21
+ end
22
+
23
+ def message
24
+ rugged_commit.message
25
+ end
26
+
27
+ def commit_time
28
+ rugged_commit.committer[:time]
29
+ end
30
+
31
+ def root?
32
+ parents.empty?
33
+ end
34
+
35
+ def parents
36
+ @parents ||= rugged_commit.parents.map do |parent_rugged_commit|
37
+ create_parent(parent_rugged_commit)
38
+ end
39
+ end
40
+
41
+ def files
42
+ @files ||= begin
43
+ blob_entries = rugged_commit.tree.walk(:postorder).select { |_, entry| entry[:type] == :blob }
44
+ blob_entries.map { |directory, entry| "#{directory}#{entry[:name]}" }
45
+ end
46
+ end
47
+
48
+ def checkout_contents
49
+ repo.rugged_repo.checkout_tree(id, strategy: :force)
50
+ end
51
+
52
+ def extract_contents_into(directory_path)
53
+ # We need to specify an empty tree as a baseline tree
54
+ # to prevent libgit2 from skipping checkout of some contents
55
+ # by comparing the commit tree with the HEAD tree.
56
+ empty_tree = Rugged::Tree.empty(repo.rugged_repo)
57
+
58
+ repo.rugged_repo.checkout_tree(
59
+ id,
60
+ baseline: empty_tree,
61
+ strategy: [:dont_update_index, :force, :remove_ignored, :remove_untracked],
62
+ target_directory: directory_path
63
+ )
64
+ end
65
+
66
+ def revision_id
67
+ id
68
+ end
69
+
70
+ def inspect
71
+ "#<#{self.class} id=#{abbreviated_id} repo=#{repo.name} message=#{message.each_line.first.chomp.inspect}>"
72
+ end
73
+
74
+ def pretty_print(pp)
75
+ pp.text(inspect)
76
+ end
77
+
78
+ private
79
+
80
+ def create_parent(parent_rugged_commit)
81
+ self.class.new(parent_rugged_commit, repo)
82
+ end
83
+ end
84
+ end