modulesync 2.1.0 → 2.3.0
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/.github/workflows/ci.yml +29 -9
- data/.github/workflows/release.yml +7 -7
- data/.gitignore +1 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +25 -17
- data/.simplecov +46 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +5 -3
- data/LICENSE +173 -12
- data/README.md +30 -0
- data/bin/msync +17 -1
- data/features/cli.feature +12 -6
- data/features/execute.feature +51 -0
- data/features/hook.feature +5 -8
- data/features/push.feature +46 -0
- data/features/reset.feature +57 -0
- data/features/step_definitions/git_steps.rb +29 -1
- data/features/support/env.rb +9 -0
- data/features/update/bump_version.feature +8 -12
- data/features/update/dot_sync.feature +52 -0
- data/features/update/pull_request.feature +180 -0
- data/features/update.feature +74 -103
- data/lib/modulesync/cli/thor.rb +12 -0
- data/lib/modulesync/cli.rb +122 -28
- data/lib/modulesync/git_service/base.rb +63 -0
- data/lib/modulesync/git_service/factory.rb +28 -0
- data/lib/modulesync/{pr → git_service}/github.rb +23 -21
- data/lib/modulesync/git_service/gitlab.rb +62 -0
- data/lib/modulesync/git_service.rb +96 -0
- data/lib/modulesync/hook.rb +11 -13
- data/lib/modulesync/renderer.rb +3 -6
- data/lib/modulesync/repository.rb +78 -28
- data/lib/modulesync/settings.rb +0 -1
- data/lib/modulesync/source_code.rb +28 -2
- data/lib/modulesync/util.rb +4 -4
- data/lib/modulesync.rb +104 -66
- data/modulesync.gemspec +9 -5
- data/spec/helpers/faker/puppet_module_remote_repo.rb +16 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/modulesync/git_service/factory_spec.rb +16 -0
- data/spec/unit/modulesync/git_service/github_spec.rb +81 -0
- data/spec/unit/modulesync/git_service/gitlab_spec.rb +90 -0
- data/spec/unit/modulesync/git_service_spec.rb +201 -0
- data/spec/unit/modulesync/source_code_spec.rb +22 -0
- data/spec/unit/modulesync_spec.rb +0 -12
- metadata +96 -14
- data/lib/modulesync/pr/gitlab.rb +0 -54
- data/spec/unit/modulesync/pr/github_spec.rb +0 -49
- data/spec/unit/modulesync/pr/gitlab_spec.rb +0 -81
@@ -0,0 +1,63 @@
|
|
1
|
+
module ModuleSync
|
2
|
+
module GitService
|
3
|
+
# Generic class for git services
|
4
|
+
class Base
|
5
|
+
def open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) # rubocop:disable Metrics/ParameterLists
|
6
|
+
unless source_branch != target_branch
|
7
|
+
raise ModuleSync::Error,
|
8
|
+
"Unable to open a pull request with the same source and target branch: '#{source_branch}'"
|
9
|
+
end
|
10
|
+
|
11
|
+
_open_pull_request(
|
12
|
+
repo_path: repo_path,
|
13
|
+
namespace: namespace,
|
14
|
+
title: title,
|
15
|
+
message: message,
|
16
|
+
source_branch: source_branch,
|
17
|
+
target_branch: target_branch,
|
18
|
+
labels: labels,
|
19
|
+
noop: noop,
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method attempts to guess the git service endpoint based on remote
|
24
|
+
def self.guess_endpoint_from(remote:)
|
25
|
+
hostname = extract_hostname(remote)
|
26
|
+
return nil if hostname.nil?
|
27
|
+
|
28
|
+
"https://#{hostname}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method extracts hostname from URL like:
|
32
|
+
#
|
33
|
+
# - ssh://[user@]host.xz[:port]/path/to/repo.git/
|
34
|
+
# - git://host.xz[:port]/path/to/repo.git/
|
35
|
+
# - [user@]host.xz:path/to/repo.git/
|
36
|
+
# - http[s]://host.xz[:port]/path/to/repo.git/
|
37
|
+
# - ftp[s]://host.xz[:port]/path/to/repo.git/
|
38
|
+
#
|
39
|
+
# Returns nil if
|
40
|
+
# - /path/to/repo.git/
|
41
|
+
# - file:///path/to/repo.git/
|
42
|
+
# - any invalid URL
|
43
|
+
def self.extract_hostname(url)
|
44
|
+
return nil if url.start_with?('/') || url.start_with?('file://') # local path (e.g. file:///path/to/repo)
|
45
|
+
|
46
|
+
unless url.start_with?(%r{[a-z]+://}) # SSH notation does not contain protocol (e.g. user@server:path/to/repo/)
|
47
|
+
pattern = /^(?<user>.*@)?(?<hostname>[\w|.]*):(?<repo>.*)$/ # SSH path (e.g. user@server:repo)
|
48
|
+
return url.match(pattern)[:hostname] if url.match?(pattern)
|
49
|
+
end
|
50
|
+
|
51
|
+
URI.parse(url).host
|
52
|
+
rescue URI::InvalidURIError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def _open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) # rubocop:disable Metrics/ParameterLists
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ModuleSync
|
2
|
+
module GitService
|
3
|
+
# Git service's factory
|
4
|
+
module Factory
|
5
|
+
def self.instantiate(type:, endpoint:, token:)
|
6
|
+
raise MissingCredentialsError, <<~MESSAGE if token.nil?
|
7
|
+
A token is required to use services from #{type}:
|
8
|
+
Please set environment variable: "#{type.upcase}_TOKEN" or set the token entry in module options.
|
9
|
+
MESSAGE
|
10
|
+
|
11
|
+
klass(type: type).new token, endpoint
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.klass(type:)
|
15
|
+
case type
|
16
|
+
when :github
|
17
|
+
require 'modulesync/git_service/github'
|
18
|
+
ModuleSync::GitService::GitHub
|
19
|
+
when :gitlab
|
20
|
+
require 'modulesync/git_service/gitlab'
|
21
|
+
ModuleSync::GitService::GitLab
|
22
|
+
else
|
23
|
+
raise NotImplementedError, "Unknown git service: '#{type}'"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,28 +1,30 @@
|
|
1
|
+
require 'modulesync/git_service'
|
2
|
+
require 'modulesync/git_service/base'
|
1
3
|
require 'octokit'
|
2
|
-
require 'modulesync/util'
|
3
4
|
|
4
5
|
module ModuleSync
|
5
|
-
module
|
6
|
+
module GitService
|
6
7
|
# GitHub creates and manages pull requests on github.com or GitHub
|
7
8
|
# Enterprise installations.
|
8
|
-
class GitHub
|
9
|
+
class GitHub < Base
|
9
10
|
def initialize(token, endpoint)
|
11
|
+
super()
|
12
|
+
|
10
13
|
Octokit.configure do |c|
|
11
14
|
c.api_endpoint = endpoint
|
12
15
|
end
|
13
16
|
@api = Octokit::Client.new(:access_token => token)
|
14
17
|
end
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
head = "#{namespace}:#{
|
20
|
-
target_branch = options[:pr_target_branch] || 'master'
|
19
|
+
private
|
20
|
+
|
21
|
+
def _open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) # rubocop:disable Metrics/ParameterLists
|
22
|
+
head = "#{namespace}:#{source_branch}"
|
21
23
|
|
22
|
-
if
|
24
|
+
if noop
|
23
25
|
$stdout.puts \
|
24
|
-
"Using no-op. Would submit PR '#{
|
25
|
-
"- merges #{
|
26
|
+
"Using no-op. Would submit PR '#{title}' to '#{repo_path}' " \
|
27
|
+
"- merges '#{source_branch}' into '#{target_branch}'"
|
26
28
|
return
|
27
29
|
end
|
28
30
|
|
@@ -32,25 +34,25 @@ module ModuleSync
|
|
32
34
|
:head => head)
|
33
35
|
unless pull_requests.empty?
|
34
36
|
# Skip creating the PR if it exists already.
|
35
|
-
$stdout.puts "Skipped! #{pull_requests.length} PRs found for branch #{
|
37
|
+
$stdout.puts "Skipped! #{pull_requests.length} PRs found for branch '#{source_branch}'"
|
36
38
|
return
|
37
39
|
end
|
38
40
|
|
39
|
-
pr_labels = ModuleSync::Util.parse_list(options[:pr_labels])
|
40
41
|
pr = @api.create_pull_request(repo_path,
|
41
42
|
target_branch,
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
source_branch,
|
44
|
+
title,
|
45
|
+
message)
|
45
46
|
$stdout.puts \
|
46
|
-
"Submitted PR '#{
|
47
|
-
"- merges #{
|
47
|
+
"Submitted PR '#{title}' to '#{repo_path}' " \
|
48
|
+
"- merges #{source_branch} into #{target_branch}"
|
48
49
|
|
49
50
|
# We only assign labels to the PR if we've discovered a list > 1. The labels MUST
|
50
51
|
# already exist. We DO NOT create missing labels.
|
51
|
-
return if
|
52
|
-
|
53
|
-
|
52
|
+
return if labels.empty?
|
53
|
+
|
54
|
+
$stdout.puts "Attaching the following labels to PR #{pr['number']}: #{labels.join(', ')}"
|
55
|
+
@api.add_labels_to_an_issue(repo_path, pr['number'], labels)
|
54
56
|
end
|
55
57
|
end
|
56
58
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'gitlab'
|
2
|
+
require 'modulesync/git_service'
|
3
|
+
require 'modulesync/git_service/base'
|
4
|
+
|
5
|
+
module ModuleSync
|
6
|
+
module GitService
|
7
|
+
# GitLab creates and manages merge requests on gitlab.com or private GitLab
|
8
|
+
# installations.
|
9
|
+
class GitLab < Base
|
10
|
+
def initialize(token, endpoint)
|
11
|
+
super()
|
12
|
+
|
13
|
+
@api = Gitlab::Client.new(
|
14
|
+
:endpoint => endpoint,
|
15
|
+
:private_token => token,
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.guess_endpoint_from(remote:)
|
20
|
+
endpoint = super
|
21
|
+
return nil if endpoint.nil?
|
22
|
+
|
23
|
+
endpoint += '/api/v4'
|
24
|
+
endpoint
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def _open_pull_request(repo_path:, namespace:, title:, message:, source_branch:, target_branch:, labels:, noop:) # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument
|
30
|
+
if noop
|
31
|
+
$stdout.puts \
|
32
|
+
"Using no-op. Would submit MR '#{title}' to '#{repo_path}' " \
|
33
|
+
"- merges #{source_branch} into #{target_branch}"
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
merge_requests = @api.merge_requests(repo_path,
|
38
|
+
:state => 'opened',
|
39
|
+
:source_branch => source_branch,
|
40
|
+
:target_branch => target_branch)
|
41
|
+
unless merge_requests.empty?
|
42
|
+
# Skip creating the MR if it exists already.
|
43
|
+
$stdout.puts "Skipped! #{merge_requests.length} MRs found for branch '#{source_branch}'"
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
mr = @api.create_merge_request(repo_path,
|
48
|
+
title,
|
49
|
+
:source_branch => source_branch,
|
50
|
+
:target_branch => target_branch,
|
51
|
+
:labels => labels)
|
52
|
+
$stdout.puts \
|
53
|
+
"Submitted MR '#{title}' to '#{repo_path}' " \
|
54
|
+
"- merges '#{source_branch}' into '#{target_branch}'"
|
55
|
+
|
56
|
+
return if labels.empty?
|
57
|
+
|
58
|
+
$stdout.puts "Attached the following labels to MR #{mr.iid}: #{labels.join(', ')}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module ModuleSync
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
# Namespace for Git service classes (ie. GitHub, GitLab)
|
5
|
+
module GitService
|
6
|
+
class MissingCredentialsError < Error; end
|
7
|
+
|
8
|
+
class UnguessableTypeError < Error; end
|
9
|
+
|
10
|
+
def self.configuration_for(sourcecode:)
|
11
|
+
type = type_for(sourcecode: sourcecode)
|
12
|
+
|
13
|
+
{
|
14
|
+
type: type,
|
15
|
+
endpoint: endpoint_for(sourcecode: sourcecode, type: type),
|
16
|
+
token: token_for(sourcecode: sourcecode, type: type),
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# This method attempts to guess git service's type (ie. gitlab or github)
|
21
|
+
# It process in this order
|
22
|
+
# 1. use module specific configuration entry (ie. a specific entry named `gitlab` or `github`)
|
23
|
+
# 2. guess using remote url (ie. looking for `github` or `gitlab` string)
|
24
|
+
# 3. use environment variables (ie. check if GITHUB_TOKEN or GITLAB_TOKEN is set)
|
25
|
+
# 4. fail
|
26
|
+
def self.type_for(sourcecode:)
|
27
|
+
return :github unless sourcecode.options[:github].nil?
|
28
|
+
return :gitlab unless sourcecode.options[:gitlab].nil?
|
29
|
+
return :github if sourcecode.repository_remote.include? 'github'
|
30
|
+
return :gitlab if sourcecode.repository_remote.include? 'gitlab'
|
31
|
+
|
32
|
+
if ENV['GITLAB_TOKEN'].nil? && ENV['GITHUB_TOKEN'].nil?
|
33
|
+
raise UnguessableTypeError, <<~MESSAGE
|
34
|
+
Unable to guess Git service type without GITLAB_TOKEN or GITHUB_TOKEN sets.
|
35
|
+
MESSAGE
|
36
|
+
end
|
37
|
+
|
38
|
+
unless ENV['GITLAB_TOKEN'].nil? || ENV['GITHUB_TOKEN'].nil?
|
39
|
+
raise UnguessableTypeError, <<~MESSAGE
|
40
|
+
Unable to guess Git service type with both GITLAB_TOKEN and GITHUB_TOKEN sets.
|
41
|
+
|
42
|
+
Please set the wanted one in configuration (ie. add `gitlab:` or `github:` key)
|
43
|
+
MESSAGE
|
44
|
+
end
|
45
|
+
|
46
|
+
return :github unless ENV['GITHUB_TOKEN'].nil?
|
47
|
+
return :gitlab unless ENV['GITLAB_TOKEN'].nil?
|
48
|
+
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
# This method attempts to find git service's endpoint based on sourcecode and type
|
53
|
+
# It process in this order
|
54
|
+
# 1. use module specific configuration (ie. `base_url`)
|
55
|
+
# 2. use environment variable dependending on type (e.g. GITLAB_BASE_URL)
|
56
|
+
# 3. guess using the git remote url
|
57
|
+
# 4. fail
|
58
|
+
def self.endpoint_for(sourcecode:, type:)
|
59
|
+
endpoint = sourcecode.options.dig(type, :base_url)
|
60
|
+
|
61
|
+
endpoint ||= case type
|
62
|
+
when :github
|
63
|
+
ENV['GITHUB_BASE_URL']
|
64
|
+
when :gitlab
|
65
|
+
ENV['GITLAB_BASE_URL']
|
66
|
+
end
|
67
|
+
|
68
|
+
endpoint ||= GitService::Factory.klass(type: type).guess_endpoint_from(remote: sourcecode.repository_remote)
|
69
|
+
|
70
|
+
raise NotImplementedError, <<~MESSAGE if endpoint.nil?
|
71
|
+
Unable to guess endpoint for remote: '#{sourcecode.repository_remote}'
|
72
|
+
Please provide `base_url` option in configuration file
|
73
|
+
MESSAGE
|
74
|
+
|
75
|
+
endpoint
|
76
|
+
end
|
77
|
+
|
78
|
+
# This method attempts to find the token associated to provided sourcecode and type
|
79
|
+
# It process in this order:
|
80
|
+
# 1. use module specific configuration (ie. `token`)
|
81
|
+
# 2. use environment variable depending on type (e.g. GITLAB_TOKEN)
|
82
|
+
# 3. fail
|
83
|
+
def self.token_for(sourcecode:, type:)
|
84
|
+
token = sourcecode.options.dig(type, :token)
|
85
|
+
|
86
|
+
token ||= case type
|
87
|
+
when :github
|
88
|
+
ENV['GITHUB_TOKEN']
|
89
|
+
when :gitlab
|
90
|
+
ENV['GITLAB_TOKEN']
|
91
|
+
end
|
92
|
+
|
93
|
+
token
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/modulesync/hook.rb
CHANGED
@@ -6,20 +6,20 @@ module ModuleSync
|
|
6
6
|
|
7
7
|
def initialize(hook_file, options = [])
|
8
8
|
@hook_file = hook_file
|
9
|
-
@namespace = options[
|
10
|
-
@branch = options[
|
11
|
-
@args = options[
|
9
|
+
@namespace = options[:namespace]
|
10
|
+
@branch = options[:branch]
|
11
|
+
@args = options[:hook_args]
|
12
12
|
end
|
13
13
|
|
14
14
|
def content(arguments)
|
15
|
-
|
16
|
-
#!/usr/bin/env bash
|
15
|
+
<<~CONTENT
|
16
|
+
#!/usr/bin/env bash
|
17
17
|
|
18
|
-
current_branch=\`git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,'\`
|
19
|
-
git_dir=\`git rev-parse --show-toplevel\`
|
20
|
-
message=\`git log -1 --format=%B\`
|
21
|
-
msync -m "\$message" #{arguments}
|
22
|
-
CONTENT
|
18
|
+
current_branch=\`git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,'\`
|
19
|
+
git_dir=\`git rev-parse --show-toplevel\`
|
20
|
+
message=\`git log -1 --format=%B\`
|
21
|
+
msync -m "\$message" #{arguments}
|
22
|
+
CONTENT
|
23
23
|
end
|
24
24
|
|
25
25
|
def activate
|
@@ -28,9 +28,7 @@ CONTENT
|
|
28
28
|
hook_args << "-b #{branch}" if branch
|
29
29
|
hook_args << args if args
|
30
30
|
|
31
|
-
File.
|
32
|
-
file.write(content(hook_args.join(' ')))
|
33
|
-
end
|
31
|
+
File.write(hook_file, content(hook_args.join(' ')))
|
34
32
|
end
|
35
33
|
|
36
34
|
def deactivate
|
data/lib/modulesync/renderer.rb
CHANGED
@@ -12,7 +12,7 @@ module ModuleSync
|
|
12
12
|
|
13
13
|
def self.build(target_name)
|
14
14
|
template_file = if !File.exist?("#{target_name}.erb") && File.exist?(target_name)
|
15
|
-
|
15
|
+
$stderr.puts "Warning: using '#{target_name}' as template without '.erb' suffix"
|
16
16
|
target_name
|
17
17
|
else
|
18
18
|
"#{target_name}.erb"
|
@@ -32,11 +32,8 @@ module ModuleSync
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def self.sync(template, target_name)
|
35
|
-
|
36
|
-
|
37
|
-
File.open(target_name, 'w') do |file|
|
38
|
-
file.write(template)
|
39
|
-
end
|
35
|
+
FileUtils.mkdir_p(File.dirname(target_name))
|
36
|
+
File.write(target_name, template)
|
40
37
|
end
|
41
38
|
end
|
42
39
|
end
|
@@ -33,10 +33,11 @@ module ModuleSync
|
|
33
33
|
def default_branch
|
34
34
|
symbolic_ref = repo.branches.find { |b| b.full =~ %r{remotes/origin/HEAD} }
|
35
35
|
return unless symbolic_ref
|
36
|
+
|
36
37
|
%r{remotes/origin/HEAD\s+->\s+origin/(?<branch>.+?)$}.match(symbolic_ref.full)[:branch]
|
37
38
|
end
|
38
39
|
|
39
|
-
def
|
40
|
+
def switch(branch:)
|
40
41
|
unless branch
|
41
42
|
branch = default_branch
|
42
43
|
puts "Using repository's default branch: #{branch}"
|
@@ -51,30 +52,61 @@ module ModuleSync
|
|
51
52
|
repo.checkout("origin/#{branch}")
|
52
53
|
repo.branch(branch).checkout
|
53
54
|
else
|
54
|
-
|
55
|
-
|
55
|
+
base_branch = default_branch
|
56
|
+
unless base_branch
|
57
|
+
puts "Couldn't detect default branch. Falling back to assuming 'master'"
|
58
|
+
base_branch = 'master'
|
59
|
+
end
|
60
|
+
puts "Creating new branch #{branch} from #{base_branch}"
|
61
|
+
repo.checkout("origin/#{base_branch}")
|
56
62
|
repo.branch(branch).checkout
|
57
63
|
end
|
58
64
|
end
|
59
65
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
66
|
+
def cloned?
|
67
|
+
Dir.exist? File.join(@directory, '.git')
|
68
|
+
end
|
69
|
+
|
70
|
+
def clone
|
71
|
+
puts "Cloning from '#{@remote}'"
|
72
|
+
@git = Git.clone(@remote, @directory)
|
73
|
+
end
|
74
|
+
|
75
|
+
def prepare_workspace(branch:, operate_offline:)
|
76
|
+
if cloned?
|
77
|
+
puts "Overriding any local changes to repository in '#{@directory}'"
|
78
|
+
git.fetch 'origin', prune: true unless operate_offline
|
79
|
+
git.reset_hard
|
80
|
+
switch(branch: branch)
|
81
|
+
git.pull('origin', branch) if !operate_offline && remote_branch_exists?(branch)
|
68
82
|
else
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
83
|
+
raise ModuleSync::Error, 'Unable to clone in offline mode.' if operate_offline
|
84
|
+
|
85
|
+
clone
|
86
|
+
switch(branch: branch)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def default_reset_branch(branch)
|
91
|
+
remote_branch_exists?(branch) ? branch : default_branch
|
92
|
+
end
|
93
|
+
|
94
|
+
def reset_workspace(branch:, operate_offline:, source_branch: nil)
|
95
|
+
raise if branch.nil?
|
96
|
+
|
97
|
+
if cloned?
|
98
|
+
source_branch ||= "origin/#{default_reset_branch branch}"
|
99
|
+
puts "Hard-resetting any local changes to repository in '#{@directory}' from branch '#{source_branch}'"
|
100
|
+
switch(branch: branch)
|
101
|
+
git.fetch 'origin', prune: true unless operate_offline
|
102
|
+
|
103
|
+
git.reset_hard source_branch
|
104
|
+
git.clean(d: true, force: true)
|
105
|
+
else
|
106
|
+
raise ModuleSync::Error, 'Unable to clone in offline mode.' if operate_offline
|
107
|
+
|
108
|
+
clone
|
109
|
+
switch(branch: branch)
|
78
110
|
end
|
79
111
|
end
|
80
112
|
|
@@ -98,7 +130,7 @@ module ModuleSync
|
|
98
130
|
files.each do |file|
|
99
131
|
if repo.status.deleted.include?(file)
|
100
132
|
repo.remove(file)
|
101
|
-
elsif File.exist?(
|
133
|
+
elsif File.exist? File.join(@directory, file)
|
102
134
|
repo.add(file)
|
103
135
|
end
|
104
136
|
end
|
@@ -115,9 +147,11 @@ module ModuleSync
|
|
115
147
|
if options[:remote_branch]
|
116
148
|
if remote_branch_differ?(branch, options[:remote_branch])
|
117
149
|
repo.push('origin', "#{branch}:#{options[:remote_branch]}", opts_push)
|
150
|
+
puts "Changes have been pushed to: '#{branch}:#{options[:remote_branch]}'"
|
118
151
|
end
|
119
152
|
else
|
120
153
|
repo.push('origin', branch, opts_push)
|
154
|
+
puts "Changes have been pushed to: '#{branch}'"
|
121
155
|
end
|
122
156
|
rescue Git::GitExecuteError => e
|
123
157
|
raise unless e.message.match?(/working (directory|tree) clean/)
|
@@ -129,11 +163,21 @@ module ModuleSync
|
|
129
163
|
true
|
130
164
|
end
|
131
165
|
|
166
|
+
def push(branch:, remote_branch:, remote_name: 'origin')
|
167
|
+
raise ModuleSync::Error, 'Repository must be locally available before trying to push' unless cloned?
|
168
|
+
|
169
|
+
remote_url = git.remote(remote_name).url
|
170
|
+
remote_branch ||= branch
|
171
|
+
puts "Push branch '#{branch}' to '#{remote_url}' (#{remote_name}/#{remote_branch})"
|
172
|
+
|
173
|
+
git.push(remote_name, "#{branch}:#{remote_branch}", force: true)
|
174
|
+
end
|
175
|
+
|
132
176
|
# Needed because of a bug in the git gem that lists ignored files as
|
133
177
|
# untracked under some circumstances
|
134
178
|
# https://github.com/schacon/ruby-git/issues/130
|
135
179
|
def untracked_unignored_files
|
136
|
-
ignore_path =
|
180
|
+
ignore_path = File.join @directory, '.gitignore'
|
137
181
|
ignored = File.exist?(ignore_path) ? File.read(ignore_path).split : []
|
138
182
|
repo.status.untracked.keep_if { |f, _| ignored.none? { |i| File.fnmatch(i, f) } }
|
139
183
|
end
|
@@ -141,18 +185,24 @@ module ModuleSync
|
|
141
185
|
def show_changes(options)
|
142
186
|
checkout_branch(options[:branch])
|
143
187
|
|
144
|
-
puts 'Files changed:'
|
188
|
+
$stdout.puts 'Files changed:'
|
145
189
|
repo.diff('HEAD', '--').each do |diff|
|
146
|
-
puts diff.patch
|
190
|
+
$stdout.puts diff.patch
|
147
191
|
end
|
148
192
|
|
149
|
-
puts 'Files added:'
|
193
|
+
$stdout.puts 'Files added:'
|
150
194
|
untracked_unignored_files.each_key do |file|
|
151
|
-
puts file
|
195
|
+
$stdout.puts file
|
152
196
|
end
|
153
197
|
|
154
|
-
puts "\n\n"
|
155
|
-
puts '--------------------------------'
|
198
|
+
$stdout.puts "\n\n"
|
199
|
+
$stdout.puts '--------------------------------'
|
200
|
+
|
201
|
+
git.diff('HEAD', '--').any? || untracked_unignored_files.any?
|
202
|
+
end
|
203
|
+
|
204
|
+
def puts(*args)
|
205
|
+
$stdout.puts(*args) if ModuleSync.options[:verbose]
|
156
206
|
end
|
157
207
|
end
|
158
208
|
end
|
data/lib/modulesync/settings.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require 'modulesync'
|
2
|
+
require 'modulesync/git_service'
|
3
|
+
require 'modulesync/git_service/factory'
|
2
4
|
require 'modulesync/repository'
|
3
5
|
require 'modulesync/util'
|
4
6
|
|
5
7
|
module ModuleSync
|
6
8
|
# Provide methods to retrieve source code attributes
|
7
9
|
class SourceCode
|
8
|
-
attr_reader :given_name
|
9
|
-
attr_reader :options
|
10
|
+
attr_reader :given_name, :options
|
10
11
|
|
11
12
|
def initialize(given_name, options)
|
12
13
|
@options = Util.symbolize_keys(options || {})
|
@@ -47,6 +48,31 @@ module ModuleSync
|
|
47
48
|
File.join(working_directory, *parts)
|
48
49
|
end
|
49
50
|
|
51
|
+
def git_service
|
52
|
+
return nil if git_service_configuration.nil?
|
53
|
+
|
54
|
+
@git_service ||= GitService::Factory.instantiate(**git_service_configuration)
|
55
|
+
end
|
56
|
+
|
57
|
+
def git_service_configuration
|
58
|
+
@git_service_configuration ||= GitService.configuration_for(sourcecode: self)
|
59
|
+
rescue GitService::UnguessableTypeError
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def open_pull_request
|
64
|
+
git_service.open_pull_request(
|
65
|
+
repo_path: repository_path,
|
66
|
+
namespace: repository_namespace,
|
67
|
+
title: ModuleSync.options[:pr_title],
|
68
|
+
message: ModuleSync.options[:message],
|
69
|
+
source_branch: ModuleSync.options[:remote_branch] || ModuleSync.options[:branch] || repository.default_branch,
|
70
|
+
target_branch: ModuleSync.options[:pr_target_branch] || repository.default_branch,
|
71
|
+
labels: ModuleSync::Util.parse_list(ModuleSync.options[:pr_labels]),
|
72
|
+
noop: ModuleSync.options[:noop],
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
50
76
|
private
|
51
77
|
|
52
78
|
def _repository_remote
|
data/lib/modulesync/util.rb
CHANGED
@@ -3,9 +3,8 @@ require 'yaml'
|
|
3
3
|
module ModuleSync
|
4
4
|
module Util
|
5
5
|
def self.symbolize_keys(hash)
|
6
|
-
hash.
|
6
|
+
hash.each_with_object({}) do |(k, v), memo|
|
7
7
|
memo[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
|
8
|
-
memo
|
9
8
|
end
|
10
9
|
end
|
11
10
|
|
@@ -19,9 +18,10 @@ module ModuleSync
|
|
19
18
|
end
|
20
19
|
|
21
20
|
def self.parse_list(option_value)
|
22
|
-
|
21
|
+
case option_value
|
22
|
+
when String
|
23
23
|
option_value.split(',')
|
24
|
-
|
24
|
+
when Array
|
25
25
|
option_value
|
26
26
|
else
|
27
27
|
[]
|