modulesync 2.1.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
  []