multi_repo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +16 -0
- data/.github/workflows/ci.yaml +32 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_cc.yml +4 -0
- data/.rubocop_local.yml +0 -0
- data/.whitesource +3 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/Rakefile +6 -0
- data/bin/console +8 -0
- data/exe/multi_repo +29 -0
- data/lib/multi_repo/cli.rb +92 -0
- data/lib/multi_repo/helpers/git_mirror.rb +198 -0
- data/lib/multi_repo/helpers/license.rb +106 -0
- data/lib/multi_repo/helpers/pull_request_blaster_outer.rb +129 -0
- data/lib/multi_repo/helpers/readme_badges.rb +84 -0
- data/lib/multi_repo/helpers/rename_labels.rb +26 -0
- data/lib/multi_repo/helpers/update_branch_protection.rb +24 -0
- data/lib/multi_repo/helpers/update_labels.rb +34 -0
- data/lib/multi_repo/helpers/update_milestone.rb +33 -0
- data/lib/multi_repo/helpers/update_repo_settings.rb +23 -0
- data/lib/multi_repo/labels.rb +31 -0
- data/lib/multi_repo/repo.rb +56 -0
- data/lib/multi_repo/repo_set.rb +27 -0
- data/lib/multi_repo/service/artifactory.rb +122 -0
- data/lib/multi_repo/service/code_climate.rb +119 -0
- data/lib/multi_repo/service/docker.rb +178 -0
- data/lib/multi_repo/service/git/minigit_capturing_patch.rb +12 -0
- data/lib/multi_repo/service/git.rb +90 -0
- data/lib/multi_repo/service/github.rb +238 -0
- data/lib/multi_repo/service/rubygems_stub.rb +103 -0
- data/lib/multi_repo/service/travis.rb +68 -0
- data/lib/multi_repo/version.rb +3 -0
- data/lib/multi_repo.rb +44 -0
- data/multi_repo.gemspec +44 -0
- data/repos/.gitkeep +0 -0
- data/scripts/delete_labels +23 -0
- data/scripts/destroy_branch +23 -0
- data/scripts/destroy_remote +26 -0
- data/scripts/destroy_tag +31 -0
- data/scripts/each_repo +23 -0
- data/scripts/fetch_repos +18 -0
- data/scripts/git_mirror +9 -0
- data/scripts/github_rate_limit +10 -0
- data/scripts/hacktoberfest +138 -0
- data/scripts/make_alumni +50 -0
- data/scripts/new_rubygems_stub +17 -0
- data/scripts/pull_request_blaster_outer +24 -0
- data/scripts/pull_request_labeler +59 -0
- data/scripts/pull_request_merger +63 -0
- data/scripts/reenable_repo_workflows +33 -0
- data/scripts/rename_labels +22 -0
- data/scripts/restart_travis_builds +31 -0
- data/scripts/show_commit_history +86 -0
- data/scripts/show_org_members +19 -0
- data/scripts/show_org_repos +13 -0
- data/scripts/show_org_stats +82 -0
- data/scripts/show_project_cards +35 -0
- data/scripts/show_repo_set +13 -0
- data/scripts/show_tag +33 -0
- data/scripts/show_travis_status +63 -0
- data/scripts/update_branch_protection +22 -0
- data/scripts/update_labels +16 -0
- data/scripts/update_milestone +21 -0
- data/scripts/update_repo_settings +15 -0
- 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
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_cc.yml
ADDED
data/.rubocop_local.yml
ADDED
File without changes
|
data/.whitesource
ADDED
data/Gemfile
ADDED
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
data/bin/console
ADDED
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
|