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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +29 -9
  3. data/.github/workflows/release.yml +7 -7
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +14 -8
  6. data/.rubocop_todo.yml +25 -17
  7. data/.simplecov +46 -0
  8. data/CHANGELOG.md +58 -0
  9. data/Gemfile +5 -3
  10. data/LICENSE +173 -12
  11. data/README.md +30 -0
  12. data/bin/msync +17 -1
  13. data/features/cli.feature +12 -6
  14. data/features/execute.feature +51 -0
  15. data/features/hook.feature +5 -8
  16. data/features/push.feature +46 -0
  17. data/features/reset.feature +57 -0
  18. data/features/step_definitions/git_steps.rb +29 -1
  19. data/features/support/env.rb +9 -0
  20. data/features/update/bump_version.feature +8 -12
  21. data/features/update/dot_sync.feature +52 -0
  22. data/features/update/pull_request.feature +180 -0
  23. data/features/update.feature +74 -103
  24. data/lib/modulesync/cli/thor.rb +12 -0
  25. data/lib/modulesync/cli.rb +122 -28
  26. data/lib/modulesync/git_service/base.rb +63 -0
  27. data/lib/modulesync/git_service/factory.rb +28 -0
  28. data/lib/modulesync/{pr → git_service}/github.rb +23 -21
  29. data/lib/modulesync/git_service/gitlab.rb +62 -0
  30. data/lib/modulesync/git_service.rb +96 -0
  31. data/lib/modulesync/hook.rb +11 -13
  32. data/lib/modulesync/renderer.rb +3 -6
  33. data/lib/modulesync/repository.rb +78 -28
  34. data/lib/modulesync/settings.rb +0 -1
  35. data/lib/modulesync/source_code.rb +28 -2
  36. data/lib/modulesync/util.rb +4 -4
  37. data/lib/modulesync.rb +104 -66
  38. data/modulesync.gemspec +9 -5
  39. data/spec/helpers/faker/puppet_module_remote_repo.rb +16 -1
  40. data/spec/spec_helper.rb +2 -0
  41. data/spec/unit/modulesync/git_service/factory_spec.rb +16 -0
  42. data/spec/unit/modulesync/git_service/github_spec.rb +81 -0
  43. data/spec/unit/modulesync/git_service/gitlab_spec.rb +90 -0
  44. data/spec/unit/modulesync/git_service_spec.rb +201 -0
  45. data/spec/unit/modulesync/source_code_spec.rb +22 -0
  46. data/spec/unit/modulesync_spec.rb +0 -12
  47. metadata +96 -14
  48. data/lib/modulesync/pr/gitlab.rb +0 -54
  49. data/spec/unit/modulesync/pr/github_spec.rb +0 -49
  50. 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 PR
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
- def manage(namespace, module_name, options)
17
- repo_path = File.join(namespace, module_name)
18
- branch = options[:remote_branch] || options[:branch]
19
- head = "#{namespace}:#{branch}"
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 options[:noop]
24
+ if noop
23
25
  $stdout.puts \
24
- "Using no-op. Would submit PR '#{options[:pr_title]}' to #{repo_path} " \
25
- "- merges #{branch} into #{target_branch}"
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 #{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
- branch,
43
- options[:pr_title],
44
- options[:message])
43
+ source_branch,
44
+ title,
45
+ message)
45
46
  $stdout.puts \
46
- "Submitted PR '#{options[:pr_title]}' to #{repo_path} " \
47
- "- merges #{branch} into #{target_branch}"
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 pr_labels.empty?
52
- $stdout.puts "Attaching the following labels to PR #{pr['number']}: #{pr_labels.join(', ')}"
53
- @api.add_labels_to_an_issue(repo_path, pr['number'], pr_labels)
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
@@ -6,20 +6,20 @@ module ModuleSync
6
6
 
7
7
  def initialize(hook_file, options = [])
8
8
  @hook_file = hook_file
9
- @namespace = options['namespace']
10
- @branch = options['branch']
11
- @args = options['hook_args']
9
+ @namespace = options[:namespace]
10
+ @branch = options[:branch]
11
+ @args = options[:hook_args]
12
12
  end
13
13
 
14
14
  def content(arguments)
15
- <<-CONTENT
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.open(hook_file, 'w') do |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
@@ -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
- STDERR.puts "Warning: using '#{target_name}' as template without '.erb' suffix"
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
- path = target_name.rpartition('/').first
36
- FileUtils.mkdir_p(path) unless path.empty?
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 switch_branch(branch)
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
- repo.checkout('origin/master')
55
- puts "Creating new branch #{branch}"
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 prepare_workspace(branch)
61
- # Repo needs to be cloned in the cwd
62
- if !Dir.exist?("#{@directory}/.git")
63
- puts 'Cloning repository fresh'
64
- puts "Cloning from '#{@remote}'"
65
- @git = Git.clone(@remote, @directory)
66
- switch_branch(branch)
67
- # Repo already cloned, check out master and override local changes
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
- # Some versions of git can't properly handle managing a repo from outside the repo directory
70
- Dir.chdir(@directory) do
71
- puts "Overriding any local changes to repository in '#{@directory}'"
72
- @git = Git.open('.')
73
- repo.fetch
74
- repo.reset_hard
75
- switch_branch(branch)
76
- git.pull('origin', branch) if remote_branch_exists?(branch)
77
- end
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?("#{@directory}/#{file}")
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 = "#{@directory}/.gitignore"
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
@@ -1,4 +1,3 @@
1
-
2
1
  module ModuleSync
3
2
  # Encapsulate a configs for a module, providing easy access to its parts
4
3
  # All configs MUST be keyed by the relative target filename
@@ -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
@@ -3,9 +3,8 @@ require 'yaml'
3
3
  module ModuleSync
4
4
  module Util
5
5
  def self.symbolize_keys(hash)
6
- hash.inject({}) do |memo, (k, v)|
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
- if option_value.is_a? String
21
+ case option_value
22
+ when String
23
23
  option_value.split(',')
24
- elsif option_value.is_a? Array
24
+ when Array
25
25
  option_value
26
26
  else
27
27
  []