multi_repo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +16 -0
  3. data/.github/workflows/ci.yaml +32 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +4 -0
  8. data/.rubocop_local.yml +0 -0
  9. data/.whitesource +3 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +90 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +8 -0
  15. data/exe/multi_repo +29 -0
  16. data/lib/multi_repo/cli.rb +92 -0
  17. data/lib/multi_repo/helpers/git_mirror.rb +198 -0
  18. data/lib/multi_repo/helpers/license.rb +106 -0
  19. data/lib/multi_repo/helpers/pull_request_blaster_outer.rb +129 -0
  20. data/lib/multi_repo/helpers/readme_badges.rb +84 -0
  21. data/lib/multi_repo/helpers/rename_labels.rb +26 -0
  22. data/lib/multi_repo/helpers/update_branch_protection.rb +24 -0
  23. data/lib/multi_repo/helpers/update_labels.rb +34 -0
  24. data/lib/multi_repo/helpers/update_milestone.rb +33 -0
  25. data/lib/multi_repo/helpers/update_repo_settings.rb +23 -0
  26. data/lib/multi_repo/labels.rb +31 -0
  27. data/lib/multi_repo/repo.rb +56 -0
  28. data/lib/multi_repo/repo_set.rb +27 -0
  29. data/lib/multi_repo/service/artifactory.rb +122 -0
  30. data/lib/multi_repo/service/code_climate.rb +119 -0
  31. data/lib/multi_repo/service/docker.rb +178 -0
  32. data/lib/multi_repo/service/git/minigit_capturing_patch.rb +12 -0
  33. data/lib/multi_repo/service/git.rb +90 -0
  34. data/lib/multi_repo/service/github.rb +238 -0
  35. data/lib/multi_repo/service/rubygems_stub.rb +103 -0
  36. data/lib/multi_repo/service/travis.rb +68 -0
  37. data/lib/multi_repo/version.rb +3 -0
  38. data/lib/multi_repo.rb +44 -0
  39. data/multi_repo.gemspec +44 -0
  40. data/repos/.gitkeep +0 -0
  41. data/scripts/delete_labels +23 -0
  42. data/scripts/destroy_branch +23 -0
  43. data/scripts/destroy_remote +26 -0
  44. data/scripts/destroy_tag +31 -0
  45. data/scripts/each_repo +23 -0
  46. data/scripts/fetch_repos +18 -0
  47. data/scripts/git_mirror +9 -0
  48. data/scripts/github_rate_limit +10 -0
  49. data/scripts/hacktoberfest +138 -0
  50. data/scripts/make_alumni +50 -0
  51. data/scripts/new_rubygems_stub +17 -0
  52. data/scripts/pull_request_blaster_outer +24 -0
  53. data/scripts/pull_request_labeler +59 -0
  54. data/scripts/pull_request_merger +63 -0
  55. data/scripts/reenable_repo_workflows +33 -0
  56. data/scripts/rename_labels +22 -0
  57. data/scripts/restart_travis_builds +31 -0
  58. data/scripts/show_commit_history +86 -0
  59. data/scripts/show_org_members +19 -0
  60. data/scripts/show_org_repos +13 -0
  61. data/scripts/show_org_stats +82 -0
  62. data/scripts/show_project_cards +35 -0
  63. data/scripts/show_repo_set +13 -0
  64. data/scripts/show_tag +33 -0
  65. data/scripts/show_travis_status +63 -0
  66. data/scripts/update_branch_protection +22 -0
  67. data/scripts/update_labels +16 -0
  68. data/scripts/update_milestone +21 -0
  69. data/scripts/update_repo_settings +15 -0
  70. metadata +366 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7e5efacc85571dad2a80c845667ec2fa40e253fd13547da6df08725d1ee18daa
4
+ data.tar.gz: 9764b15530f6e62de724fa57152b6a6b3aa1af5c7bc2620a30c52de8b2314692
5
+ SHA512:
6
+ metadata.gz: 45b956c9f7d52529f5c278eca077c0ce3acdbd573d80a73eac286832b8d7f99adc1efc10cee545a32decfdddb679585fac97529217104b5bb2069ee55e31dd01
7
+ data.tar.gz: 66f89c406e312e39aac877141be238633535b35f2dcdf22802539c5de2433f70d46fa5e0c00ce596dd235248800a571a2a608a9b66fc03d8cb520e3232c20227
data/.codeclimate.yml ADDED
@@ -0,0 +1,16 @@
1
+ prepare:
2
+ fetch:
3
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
4
+ path: ".rubocop_base.yml"
5
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_cc_base.yml
6
+ path: ".rubocop_cc_base.yml"
7
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/base.yml
8
+ path: styles/base.yml
9
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
10
+ path: styles/cc_base.yml
11
+ plugins:
12
+ rubocop:
13
+ enabled: true
14
+ config: ".rubocop_cc.yml"
15
+ channel: rubocop-0-82
16
+ version: '2'
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ schedule:
7
+ - cron: '0 0 * * 0'
8
+
9
+ jobs:
10
+ ci:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version:
15
+ - '2.7'
16
+ - '3.0'
17
+ env:
18
+ CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
19
+ steps:
20
+ - uses: actions/checkout@v2
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby-version }}
25
+ bundler-cache: true
26
+ timeout-minutes: 30
27
+ - name: Run tests
28
+ run: bundle exec rake
29
+ - name: Report code coverage
30
+ if: ${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '3.0' }}
31
+ continue-on-error: true
32
+ uses: paambaati/codeclimate-action@v3.0.0
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /.bundle/
2
+ /config/
3
+ /mirrors/
4
+ /repos/
5
+ /spec/root/
6
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --order random
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_gem:
2
+ manageiq-style: ".rubocop_base.yml"
3
+ inherit_from:
4
+ - ".rubocop_local.yml"
data/.rubocop_cc.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_from:
2
+ - ".rubocop_base.yml"
3
+ - ".rubocop_cc_base.yml"
4
+ - ".rubocop_local.yml"
File without changes
data/.whitesource ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "settingsInheritedFrom": "ManageIQ/whitesource-config@master"
3
+ }
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ plugin 'bundler-inject'
4
+ require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil
5
+
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 ManageIQ Authors.
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,90 @@
1
+ # MultiRepo
2
+
3
+ MultiRepo is a tool for managing multiple git repositories.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/multi_repo.svg)](http://badge.fury.io/rb/multi_repo)
6
+ [![CI](https://github.com/ManageIQ/multi_repo/actions/workflows/ci.yaml/badge.svg)](https://github.com/ManageIQ/multi_repo/actions/workflows/ci.yaml)
7
+ [![Code Climate](http://img.shields.io/codeclimate/github/ManageIQ/multi_repo.svg)](https://codeclimate.com/github/ManageIQ/multi_repo)
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ gem install multi_repo
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ ## Usage
18
+
19
+ Typical usage will be from single scripts. In order to keep each script manageable, it can be preferable to use bundler/inline to define the gems needed by that script. To do this, add the following to the top of the script:
20
+
21
+ ```ruby
22
+ #/usr/bin/env ruby
23
+
24
+ require "bundler/inline"
25
+ gemfile do
26
+ source "https://rubygems.org"
27
+ gem "multi_repo", require: "multi_repo/cli"
28
+ end
29
+ ```
30
+
31
+ Then, you would set up options for your script. A `MultiRepo::CLI` helper is provided to make this easier. It has the [optimist](https://github.com/ManageIQ/optimist) gem already prepared and comes with a `.common_options` helper method to set up options for `--repo-set`, `--repo`, and `--dry-run`. For example,
32
+
33
+ ```ruby
34
+ opts = Optimist.options do
35
+ opt :some_opt, "An option your script needs", :type => :string, :required => true
36
+
37
+ MultiRepo::CLI.common_options(self)
38
+ end
39
+ ```
40
+
41
+ would produce the following help output:
42
+
43
+ ```
44
+ Options:
45
+ -o, --some-opt=<s> An option your script needs
46
+
47
+ Common Options:
48
+ -s, --repo-set=<s> The repo set to work with (default: master)
49
+ -r, --repo=<s+> Individual repo(s) to work with; Overrides --repo-set
50
+ -d, --dry-run Execute without making changes
51
+ -h, --help Show this message
52
+ ```
53
+
54
+ After you have set up the options, you can write your script. `MultiRepo::Service` classes are provided to help interface with common third-party services, such as GitHub. `MultiRepo::Helper` classes are provided to do relatively common operations, such as renaming labels. The `MultiRepo::CLI` class also has helpers for looping over the repo set. For example, to loop over each repo in the repo set and show the file contents one can do:
55
+
56
+ ```ruby
57
+ MultiRepo::CLI.each_repo(**opts) do |repo|
58
+ system("ls")
59
+ end
60
+ ```
61
+
62
+ ## GitHub interactions
63
+
64
+ Certain commands interact with GitHub and expect a GitHub API Token set in the
65
+ ENV variable GITHUB_API_TOKEN.
66
+
67
+ If you don't already have a token, or want to create one specific to these
68
+ purposes
69
+ - Go to https://github.com/settings/tokens
70
+ - Choose "Generate New Token"
71
+ - Give the token a description
72
+ - At a mimimum, choose "repo" for the permissions.
73
+ - Click "Generate Token"
74
+ - Copy the token given to you, and keep it in a safe location, as once you leave
75
+ the page, the token is no longer accessible
76
+
77
+ Then, in order to use it, export the ENV variable permanently, or pass it to the
78
+ program as part of the call.
79
+
80
+ ```sh
81
+ GITHUB_API_TOKEN=<token> bin/update_labels
82
+ ```
83
+
84
+ ## Contributing
85
+
86
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ManageIQ/multi_repo.
87
+
88
+ ## License
89
+
90
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :test => :spec
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path("../lib", __dir__)
4
+
5
+ require "bundler/setup"
6
+ require "multi_repo/cli"
7
+ require "irb"
8
+ IRB.start
data/exe/multi_repo ADDED
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)/scripts"
4
+
5
+ usage() {
6
+ echo "Usage: multi_repo <script> [args]"
7
+ echo " script Script to run"
8
+ echo " args Arguments to pass to the script"
9
+ echo " -h, --help Show this help message"
10
+ echo
11
+ echo "Available scripts:"
12
+ for f in $(ls -1 "$SCRIPT_DIR" | sort); do
13
+ echo " $f"
14
+ done
15
+ }
16
+
17
+ if [ -z "$1" -o "$1" = "--help" -o "$1" = "-h" ]; then
18
+ usage
19
+ exit
20
+ fi
21
+
22
+ if [ ! -f "$SCRIPT_DIR/$1" ]; then
23
+ echo "ERROR: script '$1' not found"
24
+ echo
25
+ usage
26
+ exit 1
27
+ fi
28
+
29
+ exec "$SCRIPT_DIR/$1" "${@:2}"
@@ -0,0 +1,92 @@
1
+ require "multi_repo"
2
+ require "optimist"
3
+ require "colorize"
4
+
5
+ module MultiRepo
6
+ module CLI
7
+ def self.each_repo(**kwargs)
8
+ raise "no block given" unless block_given?
9
+
10
+ repos_for(**kwargs).each do |repo|
11
+ puts header(repo.name)
12
+ yield repo
13
+ puts
14
+ end
15
+ end
16
+
17
+ def self.repos_for(repo: nil, repo_set: nil, dry_run: false, **_)
18
+ Optimist.die("options --repo or --repo_set must be specified") unless repo || repo_set
19
+
20
+ if repo_set
21
+ repos = MultiRepo::RepoSet[repo_set]&.deep_dup
22
+ Optimist.die(:repo_set, "#{repo_set.inspect} was not found in the config") if repos.nil?
23
+
24
+ if repo
25
+ repo_names = Set.new(Array(repo))
26
+ repos.select! { |r| repo_names.include?(r.name) }
27
+ end
28
+
29
+ repos.each { |r| r.dry_run = dry_run }
30
+
31
+ repos
32
+ else
33
+ Array(repo).map { |n| MultiRepo::Repo.new(n, dry_run: dry_run) }
34
+ end
35
+ end
36
+
37
+ def self.repo_for(repo_name, repo_set: nil, dry_run: false)
38
+ Optimist.die(:repo, "must be specified") if repo_name.nil?
39
+
40
+ repos_for(repo: repo_name, repo_set: repo_set, dry_run: dry_run).first
41
+ end
42
+
43
+ def self.common_options(optimist, only: %i[repo repo_set dry_run], except: nil, repo_set_default: "master")
44
+ optimist.banner("")
45
+ optimist.banner("Common Options:")
46
+
47
+ subset = Array(only).map(&:to_sym) - Array(except).map(&:to_sym)
48
+
49
+ if subset.include?(:repo_set)
50
+ optimist.opt :repo_set, "The repo set to work with", :type => :string, :default => repo_set_default, :short => "s"
51
+ end
52
+ if subset.include?(:repo)
53
+ msg = "Individual repo(s) to work with"
54
+ if subset.include?(:repo_set)
55
+ sub_opts = {}
56
+ msg << "; Overrides --repo-set"
57
+ else
58
+ sub_opts = {:required => true}
59
+ end
60
+ optimist.opt :repo, msg, sub_opts.merge(:type => :strings)
61
+ end
62
+ if subset.include?(:dry_run)
63
+ optimist.opt :dry_run, "Execute without making changes", :default => false
64
+ end
65
+ end
66
+
67
+ #
68
+ # Logging helpers
69
+ #
70
+
71
+ HEADER_SIZE = 80
72
+
73
+ def self.header(title, char = "=")
74
+ title = " #{title} "
75
+ start = (HEADER_SIZE / 2) - (title.length / 2)
76
+ separator(char).tap { |h| h[start, title.length] = title }
77
+ end
78
+
79
+ def self.separator(char = "*")
80
+ char * HEADER_SIZE
81
+ end
82
+
83
+ def self.progress_bar(total = 100)
84
+ require "progressbar"
85
+ ProgressBar.create(
86
+ :format => "%j%% |%B| %E",
87
+ :length => HEADER_SIZE,
88
+ :total => total
89
+ )
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,198 @@
1
+ module MultiRepo::Helpers
2
+ class GitMirror
3
+ def initialize
4
+ require "colorize"
5
+ require "config"
6
+
7
+ @errors_occurred = false
8
+ end
9
+
10
+ def settings
11
+ @settings ||= Config.load_files(MultiRepo.config_dir.join("settings.yml").to_s, MultiRepo.config_dir.join("settings.local.yml").to_s)
12
+ end
13
+
14
+ def mirror_all
15
+ settings.git_mirror.repos_to_mirror.keys.each { |repo| mirror(repo) }
16
+ !@errors_occurred
17
+ end
18
+
19
+ def mirror(repo)
20
+ repo = repo.to_s
21
+ options = default_repo_options.dup.merge!(settings.git_mirror.repos_to_mirror[repo].to_h)
22
+ with_repo(repo, options) do
23
+ send("mirror_#{options.remote_source}_repo", repo)
24
+ end
25
+ !@errors_occurred
26
+ end
27
+
28
+ private
29
+
30
+ def default_repo_options
31
+ Config::Options.new(:remote_source => :upstream)
32
+ end
33
+
34
+ def backup_remote_defined?
35
+ !!settings.git_mirror.remotes.backup
36
+ end
37
+
38
+ def mirror_branches_for(repo)
39
+ settings.git_mirror.branch_mirror_defaults.to_h.merge(settings.git_mirror.branch_mirror_overrides[repo].to_h || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v }
40
+ end
41
+
42
+ def mirror_branches(repo, source_remote, dest_remote)
43
+ mirror_branches_for(repo).each do |source_name, dest_name|
44
+ sync_branch(source_remote, source_name, dest_remote, dest_name)
45
+ end
46
+ end
47
+
48
+ def mirror_upstream_repo(repo)
49
+ mirror_remote_refs(repo, "upstream", "downstream")
50
+ mirror_branches(repo, "upstream", "downstream")
51
+ mirror_remote_refs(repo, "downstream", "backup") if backup_remote_defined?
52
+ end
53
+
54
+ def mirror_downstream_repo(repo)
55
+ mirror_branches(repo, "downstream", "downstream")
56
+ mirror_remote_refs(repo, "downstream", "backup") if backup_remote_defined?
57
+ end
58
+
59
+ def dry_run?
60
+ return @dry_run if defined?(@dry_run)
61
+ @dry_run = ARGV.include?("--dry-run")
62
+ end
63
+
64
+ def downstream_repo_name(repo, options)
65
+ options.downstream_repo_name || repo.sub(/^manageiq/, settings.git_mirror.productization_name)
66
+ end
67
+
68
+ def system(*args)
69
+ puts "+ #{"dry_run: " if dry_run?}#{args.join(" ")}"
70
+ return true if dry_run?
71
+
72
+ args << {} unless args.last.is_a?(Hash)
73
+ args.last[[:out, :err]] = ["/tmp/mirror_helper_out", "w"]
74
+
75
+ super.tap do |result|
76
+ unless result
77
+ @errors_occurred = true
78
+ STDERR.puts "!!! An error has occurred:\n#{File.read("/tmp/mirror_helper_out")}".bold.red
79
+ end
80
+ end
81
+ end
82
+
83
+ def with_repo(repo, options)
84
+ repo_name = downstream_repo_name(repo, options)
85
+ puts "\n==== Mirroring #{repo_name} ====".bold.cyan
86
+
87
+ working_dir = settings.git_mirror.working_directory
88
+ FileUtils.mkdir_p(working_dir)
89
+
90
+ path = "#{working_dir}/#{repo_name}"
91
+ clone_repo(repo, repo_name, path, options.remote_source) unless File.directory?(path)
92
+
93
+ Dir.chdir(path) do
94
+ puts "\n==== Fetching for #{repo_name} ====".bold.green
95
+ # Enforce an order for remote fetching to ensure that moved
96
+ # tags prefer what is on upstream
97
+ system("git fetch backup --prune --tags") if backup_remote_defined? && remote_exists?("backup")
98
+ system("git fetch downstream --prune --tags")
99
+ system("git fetch upstream --prune --tags") if [:red_hat_cloudforms, :upstream].include?(options.remote_source)
100
+
101
+ yield
102
+ end
103
+
104
+ puts
105
+ end
106
+
107
+ def clone_repo(upstream_repo, downstream_repo, path, remote_source)
108
+ upstream_remote = settings.git_mirror.remotes[remote_source]
109
+ raise "remote '#{remote_source}'' not found in settings" if upstream_remote.nil?
110
+
111
+ system("git clone #{upstream_remote}/#{upstream_repo}.git #{path} -o upstream")
112
+ Dir.chdir(path) do
113
+ unless remote_exists?("downstream")
114
+ downstream_remote = settings.git_mirror.remotes.downstream
115
+ raise "remote 'downstream' not found in settings" if downstream_remote.nil?
116
+
117
+ system("git remote add downstream #{downstream_remote}/#{downstream_repo}.git")
118
+ end
119
+ if backup_remote_defined? && !remote_exists?("backup")
120
+ backup_remote = settings.git_mirror.remotes.backup
121
+ system("git remote add backup #{backup_remote}/#{downstream_repo}.git")
122
+ end
123
+ end
124
+ end
125
+
126
+ def remote_refs(repo, remote)
127
+ return unless remote_exists?(remote)
128
+
129
+ `git ls-remote #{remote} | grep "heads"`.split("\n").collect do |line|
130
+ branch = line.split("/").last
131
+ next if remote == "upstream" && !upstream_branch?(repo, branch)
132
+ "#{remote}/#{branch}:refs/heads/#{branch}"
133
+ end.compact.join(" ")
134
+ end
135
+
136
+ def remote_exists?(remote)
137
+ `git ls-remote #{remote} --exit-code 2>/dev/null`
138
+ $? == 0
139
+ end
140
+
141
+ def upstream_branch?(repo, branch)
142
+ (mirror_branches_for(repo).keys.collect(&:to_s) + ["master"]).include?(branch)
143
+ end
144
+
145
+ def remote_branch?(branch)
146
+ !`git branch -r | grep "\\b#{branch}\\b"`.strip.empty?
147
+ end
148
+
149
+ def sync_branch(source_remote, source_name, dest_remote, dest_name)
150
+ return unless dest_remote && dest_name
151
+
152
+ source_fq_name = "#{source_remote}/#{source_name}"
153
+ dest_fq_name = "#{dest_remote}/#{dest_name}"
154
+
155
+ puts "\n==== Syncing #{source_name} to #{dest_name} ====".bold.green
156
+ unless remote_branch?(source_fq_name)
157
+ puts "! Skipping sync of #{source_name} to #{dest_name} since #{source_fq_name} branch does not exist".yellow
158
+ return
159
+ end
160
+
161
+ start_point = remote_branch?(dest_fq_name) ? dest_fq_name : source_fq_name
162
+ system("git rebase --abort || true") # `git rebase --abort` will exit non-zero if there's nothing to abort
163
+ system("git reset --hard")
164
+
165
+ success =
166
+ system("git checkout -B #{dest_name} #{start_point}") &&
167
+ system("git pull --rebase=merges #{source_remote} #{source_name}") &&
168
+ system("git push -f #{dest_remote} #{dest_name}")
169
+
170
+ if backup_remote_defined?
171
+ if success && remote_exists?("backup")
172
+ success = system("git push -f backup #{dest_name}")
173
+ else
174
+ puts "! Skipping sync of #{source_name} to backup/#{dest_name} since backup remote does not exist".yellow
175
+ end
176
+ end
177
+
178
+ success
179
+ end
180
+
181
+ def mirror_remote_refs(repo, source_remote, dest_remote)
182
+ puts "\n==== Mirroring #{source_remote} to #{dest_remote} ====".bold.green
183
+ unless remote_exists?(dest_remote)
184
+ puts "! Skipping mirror of #{source_remote} to #{dest_remote} since #{dest_remote} does not exist".yellow
185
+ return
186
+ end
187
+
188
+ refs = remote_refs(repo, source_remote)
189
+ if refs.to_s.strip.empty?
190
+ puts "! Skipping mirror of #{source_remote} to #{dest_remote} since there are no refs to mirror".yellow
191
+ return
192
+ end
193
+
194
+ system("git push #{dest_remote} #{refs}") &&
195
+ system("git push -f #{dest_remote} --tags")
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,106 @@
1
+ module MultiRepo::Helpers
2
+ class License
3
+ attr_reader :repo, :dry_run
4
+
5
+ def initialize(repo, dry_run: false, **)
6
+ @repo = repo
7
+ @dry_run = dry_run
8
+ reload
9
+ end
10
+
11
+ def save!
12
+ save_license!
13
+ save_readme_license!
14
+ reload unless dry_run
15
+ true
16
+ end
17
+
18
+ def content
19
+ @license.text
20
+ end
21
+
22
+ def license
23
+ @license.key
24
+ end
25
+
26
+ def license=(value)
27
+ @license = Licensee::License.new(value)
28
+ end
29
+
30
+ private
31
+
32
+ def reload
33
+ require 'licensee'
34
+ @license = Licensee.license(repo.path.to_s)
35
+ end
36
+
37
+ def save_license!
38
+ repo.rm_file("LICENSE.md", dry_run: dry_run)
39
+ repo.rm_file("LICENSE", dry_run: dry_run)
40
+ repo.write_file("LICENSE.txt", content, dry_run: dry_run)
41
+ end
42
+
43
+ def save_readme_license!
44
+ readme_file = repo.detect_readme_file
45
+ lines = readme_file ? File.read(repo.path.join(readme_file)).lines : []
46
+ readme_file ||= "README.md"
47
+
48
+ apply_readme_license!(lines)
49
+ repo.write_file(readme_file, lines.join, dry_run: dry_run)
50
+ end
51
+
52
+ def extract_readme_license(lines)
53
+ section = lines.each.with_index.drop_while do |l, _i|
54
+ !l.downcase.include?("## license")
55
+ end.take_while.with_index do |(l, _i), i2|
56
+ i2 == 0 || !l.start_with?("## ")
57
+ end
58
+
59
+ section.each { |l, _i| l =~ /(mit|apache)/i && break }
60
+ type =
61
+ case $1.presence&.downcase
62
+ when "mit" then "mit"
63
+ when "apache" then "apache-2.0"
64
+ end
65
+
66
+ return type, section.map(&:last)
67
+ end
68
+
69
+ def license_details
70
+ case license
71
+ when "mit"
72
+ {
73
+ :name => "MIT License",
74
+ :url => "https://opensource.org/licenses/MIT"
75
+ }
76
+ when "apache-2.0"
77
+ {
78
+ :name => "Apache License 2.0",
79
+ :url => "http://www.apache.org/licenses/LICENSE-2.0"
80
+ }
81
+ end
82
+ end
83
+
84
+ def apply_readme_license!(lines)
85
+ readme_license, readme_license_indexes = extract_readme_license(lines)
86
+ return if readme_license == license
87
+
88
+ details = license_details
89
+ return unless details
90
+
91
+ lines.reject!.with_index { |_l, i| readme_license_indexes.include?(i) }
92
+
93
+ start_index = readme_license_indexes[0] || lines.size
94
+ new_lines = <<~EOF.lines
95
+ ## License
96
+
97
+ This project is available as open source under the terms of the [#{details[:name]}](#{details[:url]}).
98
+
99
+ EOF
100
+
101
+ new_lines.reverse_each do |l|
102
+ lines.insert(start_index, l)
103
+ end
104
+ end
105
+ end
106
+ end