git_reflow 0.8.10 → 0.9.4

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/multi-ruby-tests.yml +33 -0
  3. data/.rubocop.yml +2 -0
  4. data/.ruby-version +1 -1
  5. data/Appraisals +1 -6
  6. data/CHANGELOG.md +466 -348
  7. data/Gemfile.lock +100 -70
  8. data/LICENSE +20 -20
  9. data/README.md +36 -12
  10. data/Rakefile +15 -8
  11. data/Workflow +3 -0
  12. data/bin/console +7 -7
  13. data/bin/setup +6 -6
  14. data/exe/git-reflow +14 -30
  15. data/git_reflow.gemspec +25 -24
  16. data/lib/git_reflow.rb +3 -14
  17. data/lib/git_reflow/config.rb +52 -17
  18. data/lib/git_reflow/git_helpers.rb +69 -22
  19. data/lib/git_reflow/git_server/base.rb +68 -68
  20. data/lib/git_reflow/git_server/git_hub.rb +53 -40
  21. data/lib/git_reflow/git_server/git_hub/pull_request.rb +25 -17
  22. data/lib/git_reflow/git_server/pull_request.rb +19 -3
  23. data/lib/git_reflow/merge_error.rb +9 -9
  24. data/lib/git_reflow/rspec.rb +1 -0
  25. data/lib/git_reflow/rspec/command_line_helpers.rb +23 -6
  26. data/lib/git_reflow/rspec/stub_helpers.rb +13 -13
  27. data/lib/git_reflow/rspec/workflow_helpers.rb +18 -0
  28. data/lib/git_reflow/sandbox.rb +16 -6
  29. data/lib/git_reflow/version.rb +1 -1
  30. data/lib/git_reflow/workflow.rb +305 -10
  31. data/lib/git_reflow/workflows/FlatMergeWorkflow +38 -0
  32. data/lib/git_reflow/workflows/core.rb +208 -79
  33. data/spec/fixtures/authentication_failure.json +3 -0
  34. data/spec/fixtures/awesome_workflow.rb +2 -6
  35. data/spec/fixtures/git/git_config +7 -7
  36. data/spec/fixtures/issues/comment.json.erb +27 -27
  37. data/spec/fixtures/issues/comments.json +29 -29
  38. data/spec/fixtures/issues/comments.json.erb +15 -15
  39. data/spec/fixtures/pull_requests/comment.json.erb +45 -45
  40. data/spec/fixtures/pull_requests/comments.json +47 -47
  41. data/spec/fixtures/pull_requests/comments.json.erb +15 -15
  42. data/spec/fixtures/pull_requests/commits.json +29 -29
  43. data/spec/fixtures/pull_requests/external_pull_request.json +145 -145
  44. data/spec/fixtures/pull_requests/pull_request.json +142 -142
  45. data/spec/fixtures/pull_requests/pull_request.json.erb +142 -142
  46. data/spec/fixtures/pull_requests/pull_request_branch_nonexistent_error.json +32 -0
  47. data/spec/fixtures/pull_requests/pull_request_exists_error.json +32 -32
  48. data/spec/fixtures/pull_requests/pull_requests.json +136 -136
  49. data/spec/fixtures/repositories/commit.json +53 -53
  50. data/spec/fixtures/repositories/commit.json.erb +53 -53
  51. data/spec/fixtures/repositories/commits.json.erb +13 -13
  52. data/spec/fixtures/repositories/statuses.json +31 -31
  53. data/spec/fixtures/users/user.json +32 -0
  54. data/spec/lib/git_reflow/git_helpers_spec.rb +115 -12
  55. data/spec/lib/git_reflow/git_server/git_hub/pull_request_spec.rb +6 -6
  56. data/spec/lib/git_reflow/git_server/git_hub_spec.rb +77 -3
  57. data/spec/lib/git_reflow/git_server/pull_request_spec.rb +41 -7
  58. data/spec/lib/git_reflow/workflow_spec.rb +259 -14
  59. data/spec/lib/git_reflow/workflows/core_spec.rb +224 -65
  60. data/spec/lib/git_reflow/workflows/flat_merge_spec.rb +17 -6
  61. data/spec/lib/git_reflow_spec.rb +2 -25
  62. data/spec/spec_helper.rb +3 -0
  63. data/spec/support/github_helpers.rb +1 -1
  64. data/spec/support/mock_pull_request.rb +17 -17
  65. data/spec/support/web_mocks.rb +39 -39
  66. metadata +52 -53
  67. data/circle.yml +0 -26
  68. data/lib/git_reflow/commands/deliver.rb +0 -10
  69. data/lib/git_reflow/commands/refresh.rb +0 -20
  70. data/lib/git_reflow/commands/review.rb +0 -13
  71. data/lib/git_reflow/commands/setup.rb +0 -11
  72. data/lib/git_reflow/commands/stage.rb +0 -9
  73. data/lib/git_reflow/commands/start.rb +0 -18
  74. data/lib/git_reflow/commands/status.rb +0 -7
  75. data/lib/git_reflow/workflows/flat_merge.rb +0 -10
  76. data/spec/fixtures/workflow_with_super.rb +0 -8
@@ -84,56 +84,69 @@ module GitReflow
84
84
  end
85
85
 
86
86
  def authenticate(options = {silent: false})
87
+ if !options[:user].to_s.empty?
88
+ self.class.user = options[:user]
89
+ elsif self.class.user.empty?
90
+ self.class.user = ask("Please enter your GitHub username: ")
91
+ end
92
+
87
93
  if connection and self.class.oauth_token.length > 0
88
- unless options[:silent]
89
- GitReflow.say "Your GitHub account was already setup with: "
90
- GitReflow.say "\tUser Name: #{self.class.user}"
91
- GitReflow.say "\tEndpoint: #{self.class.api_endpoint}"
92
- end
93
- else
94
94
  begin
95
- gh_user = options[:user] || ask("Please enter your GitHub username: ")
96
- gh_password = options[:password] || ask("Please enter your GitHub password (we do NOT store this): ") { |q| q.echo = false }
97
-
98
- @connection = ::Github.new do |config|
99
- config.basic_auth = "#{gh_user}:#{gh_password}"
100
- config.endpoint = GitServer::GitHub.api_endpoint
101
- config.site = GitServer::GitHub.site_url
102
- config.adapter = :net_http
95
+ connection.users.get
96
+ unless options[:silent]
97
+ GitReflow.say "Your GitHub account was already setup with: "
98
+ GitReflow.say "\tUser Name: #{self.class.user}"
99
+ GitReflow.say "\tEndpoint: #{self.class.api_endpoint}"
103
100
  end
101
+ return connection
102
+ rescue ::Github::Error::Unauthorized => e
103
+ GitReflow.logger.debug "[GitHub Error] Current oauth-token is invalid or expired..."
104
+ end
105
+ end
104
106
 
105
- @connection.connection_options = {headers: {"X-GitHub-OTP" => options[:two_factor_auth_code]}} if options[:two_factor_auth_code]
107
+ begin
108
+ gh_password = options[:password] || ask("Please enter your GitHub password (we do NOT store this): ") { |q| q.echo = false }
106
109
 
107
- previous_authorizations = @connection.oauth.all.select {|auth| auth.note == "git-reflow (#{run('hostname', loud: false).strip})" }
108
- if previous_authorizations.any?
109
- authorization = previous_authorizations.last
110
- GitReflow.say "You have previously setup git-reflow on this machine, but we can no longer find the stored token.", :error
111
- GitReflow.say "Please visit https://github.com/settings/tokens and delete the token for: git-reflow (#{run('hostname', loud: false).strip})", :notice
112
- raise "Setup could not be completed."
113
- else
114
- authorization = @connection.oauth.create scopes: ['repo'], note: "git-reflow (#{run('hostname', loud: false).strip})"
115
- end
110
+ @connection = ::Github.new do |config|
111
+ config.basic_auth = "#{self.class.user}:#{gh_password}"
112
+ config.endpoint = GitServer::GitHub.api_endpoint
113
+ config.site = GitServer::GitHub.site_url
114
+ config.adapter = :net_http
115
+ end
116
116
 
117
- self.class.oauth_token = authorization.token
117
+ @connection.connection_options = {headers: {"X-GitHub-OTP" => options[:two_factor_auth_code]}} if options[:two_factor_auth_code]
118
118
 
119
- rescue ::Github::Error::Unauthorized => e
120
- if e.inspect.to_s.include?('two-factor')
121
- begin
122
- # dummy request to trigger a 2FA SMS since a HTTP GET won't do it
123
- @connection.oauth.create scopes: ['repo'], note: "thank Github for not making this straightforward"
124
- rescue ::Github::Error::Unauthorized
125
- ensure
126
- two_factor_code = ask("Please enter your two-factor authentication code: ")
127
- self.authenticate options.merge({user: gh_user, password: gh_password, two_factor_auth_code: two_factor_code})
128
- end
129
- else
130
- GitReflow.say "Github Authentication Error: #{e.inspect}", :error
119
+ previous_authorizations = @connection.oauth.all.select {|auth| auth.note == "git-reflow (#{run('hostname', loud: false).strip})" }
120
+ if previous_authorizations.any?
121
+ authorization = previous_authorizations.last
122
+ GitReflow.say "You have previously setup git-reflow on this machine, but we can no longer find the stored token.", :error
123
+ GitReflow.say "Please visit https://github.com/settings/tokens and delete the token for: git-reflow (#{run('hostname', loud: false).strip})", :notice
124
+ raise "Setup could not be completed."
125
+ else
126
+ authorization = @connection.oauth.create scopes: ['repo'], note: "git-reflow (#{run('hostname', loud: false).strip})"
127
+ end
128
+
129
+ self.class.oauth_token = authorization.token
130
+
131
+ rescue ::Github::Error::Unauthorized => e
132
+ if e.inspect.to_s.include?('two-factor')
133
+ begin
134
+ # dummy request to trigger a 2FA SMS since a HTTP GET won't do it
135
+ @connection.oauth.create scopes: ['repo'], note: "thank Github for not making this straightforward"
136
+ rescue ::Github::Error::Unauthorized
137
+ ensure
138
+ two_factor_code = ask("Please enter your two-factor authentication code: ")
139
+ self.authenticate options.merge({user: self.class.user, password: gh_password, two_factor_auth_code: two_factor_code})
131
140
  end
132
- rescue StandardError => e
133
- raise "We were unable to authenticate with Github."
134
141
  else
135
- GitReflow.say "Your GitHub account was successfully setup!", :success
142
+ GitReflow.say "Github Authentication Error: #{e.inspect}", :error
143
+ raise "Setup could not be completed."
136
144
  end
145
+ rescue StandardError => e
146
+ raise "We were unable to authenticate with Github."
147
+ else
148
+ GitReflow.say "Your GitHub account was successfully setup!", :success
149
+
137
150
  end
138
151
 
139
152
  @connection
@@ -38,7 +38,7 @@ module GitReflow
38
38
 
39
39
  def commit_author
40
40
  begin
41
- username, branch = base.label.split(':')
41
+ username, _ = base.label.split(':')
42
42
  first_commit = GitReflow.git_server.connection.pull_requests.commits(username, GitReflow.git_server.class.remote_repo_name, number.to_s).first
43
43
  "#{first_commit.commit.author.name} <#{first_commit.commit.author.email}>".strip
44
44
  rescue Github::Error::NotFound
@@ -47,7 +47,7 @@ module GitReflow
47
47
  end
48
48
 
49
49
  def reviewers
50
- (comment_authors + pull_request_reviews.map(&:user).map(&:login)).uniq
50
+ (comment_authors + pull_request_reviews.map(&:user).map(&:login)).uniq - [user.login]
51
51
  end
52
52
 
53
53
  def approvals
@@ -65,7 +65,7 @@ module GitReflow
65
65
  comments = GitReflow.git_server.connection.issues.comments.all GitReflow.remote_user, GitReflow.remote_repo_name, number: self.number
66
66
  review_comments = GitReflow.git_server.connection.pull_requests.comments.all GitReflow.remote_user, GitReflow.remote_repo_name, number: self.number
67
67
 
68
- review_comments.to_a + comments.to_a
68
+ (review_comments.to_a + comments.to_a).select { |c| c.user.login != user.login }
69
69
  end
70
70
 
71
71
  def last_comment
@@ -80,7 +80,10 @@ module GitReflow
80
80
  if self.class.minimum_approvals.to_i == 0
81
81
  super
82
82
  else
83
- approvals.size >= self.class.minimum_approvals.to_i and !last_comment.match(self.class.approval_regex).nil?
83
+ approvals.size >= self.class.minimum_approvals.to_i and (
84
+ last_comment.empty? ||
85
+ !last_comment.match(self.class.approval_regex).nil?
86
+ )
84
87
  end
85
88
  end
86
89
 
@@ -93,20 +96,22 @@ module GitReflow
93
96
  if deliver?
94
97
  GitReflow.say "Merging pull request ##{self.number}: '#{self.title}', from '#{self.feature_branch_name}' into '#{self.base_branch_name}'", :notice
95
98
 
99
+ merge_method = options[:merge_method] || GitReflow::Config.get("reflow.merge-method")
100
+ merge_method = "squash" if "#{merge_method}".length < 1
101
+ merge_message_file = GitReflow.merge_message_path(merge_method: merge_method)
102
+
96
103
  unless options[:title] || options[:message]
97
104
  # prompts user for commit_title and commit_message
98
- squash_merge_message_file = "#{GitReflow.git_root_dir}/.git/SQUASH_MSG"
99
-
100
- File.open(squash_merge_message_file, 'w') do |file|
105
+ File.open(merge_message_file, 'w') do |file|
101
106
  file.write("#{self.title}\n#{self.commit_message_for_merge}\n")
102
107
  end
103
108
 
104
- GitReflow.run("#{GitReflow.git_editor_command} #{squash_merge_message_file}", with_system: true)
105
- merge_message = File.read(squash_merge_message_file).split(/[\r\n]|\r\n/).map(&:strip)
109
+ GitReflow.run("#{GitReflow.git_editor_command} #{merge_message_file}", with_system: true)
110
+ merge_message = File.read(merge_message_file).split(/[\r\n]|\r\n/).map(&:strip)
106
111
 
107
112
  title = merge_message.shift
108
113
 
109
- File.delete(squash_merge_message_file)
114
+ File.delete(merge_message_file)
110
115
 
111
116
  unless merge_message.empty?
112
117
  merge_message.shift if merge_message.first.empty?
@@ -124,9 +129,6 @@ module GitReflow
124
129
 
125
130
  options[:body] = "#{options[:message]}\n" if options[:body].nil? and "#{options[:message]}".length > 0
126
131
 
127
- merge_method = options[:merge_method] || GitReflow::Config.get("reflow.merge-method")
128
- merge_method = "squash" if "#{merge_method}".length < 1
129
-
130
132
  merge_response = GitReflow::GitServer::GitHub.connection.pull_requests.merge(
131
133
  "#{GitReflow.git_server.class.remote_user}",
132
134
  "#{GitReflow.git_server.class.remote_repo_name}",
@@ -145,13 +147,19 @@ module GitReflow
145
147
  GitReflow.run_command_with_label "git pull origin #{self.base_branch_name}"
146
148
  GitReflow.say "Pull request ##{self.number} successfully merged.", :success
147
149
 
148
- if cleanup_feature_branch?
149
- GitReflow.run_command_with_label "git push origin :#{self.feature_branch_name}"
150
+ if cleanup_remote_feature_branch?
151
+ GitReflow.run_command_with_label "git push origin :#{self.feature_branch_name}", blocking: false
152
+ else
153
+ GitReflow.say "Skipped. Remote feature branch #{self.feature_branch_name} left in tact."
154
+ end
155
+
156
+ if cleanup_local_feature_branch?
150
157
  GitReflow.run_command_with_label "git branch -D #{self.feature_branch_name}"
151
- GitReflow.say "Nice job buddy."
152
158
  else
153
- cleanup_failure_message
159
+ GitReflow.say "Skipped. Local feature branch #{self.feature_branch_name} left in tact."
154
160
  end
161
+
162
+ GitReflow.say "Nice job buddy."
155
163
  else
156
164
  GitReflow.say merge_response.to_s, :deliver_halted
157
165
  GitReflow.say "There were problems commiting your feature... please check the errors above and try again.", :error
@@ -158,6 +158,8 @@ module GitReflow
158
158
  end
159
159
 
160
160
  def commit_message_for_merge
161
+ return GitReflow.merge_commit_template unless GitReflow.merge_commit_template.nil?
162
+
161
163
  message = ""
162
164
 
163
165
  if "#{self.description}".length > 0
@@ -176,7 +178,21 @@ module GitReflow
176
178
  end
177
179
 
178
180
  def cleanup_feature_branch?
179
- GitReflow::Config.get('reflow.always-cleanup') == "true" || (ask "Would you like to push this branch to your remote repo and cleanup your feature branch? ") =~ /^y/i
181
+ cleanup_local_feature_branch? || cleanup_remote_feature_branch?
182
+ end
183
+
184
+ def cleanup_local_feature_branch?
185
+ # backwards compat
186
+ always_cleanup_local = GitReflow::Config.get('reflow.always-cleanup').to_s
187
+ always_cleanup_local = GitReflow::Config.get('reflow.always-cleanup-local') if always_cleanup_local.empty?
188
+ always_cleanup_local == "true" || (ask "Would you like to cleanup your local feature branch? ") =~ /^y/i
189
+ end
190
+
191
+ def cleanup_remote_feature_branch?
192
+ # backwards compat
193
+ always_cleanup_remote = GitReflow::Config.get('reflow.always-cleanup').to_s
194
+ always_cleanup_remote = GitReflow::Config.get('reflow.always-cleanup-remote') if always_cleanup_remote.empty?
195
+ always_cleanup_remote == "true" || (ask "Would you like to cleanup your remote feature branch? ") =~ /^y/i
180
196
  end
181
197
 
182
198
  def deliver?
@@ -204,14 +220,14 @@ module GitReflow
204
220
  GitReflow.run_command_with_label "git checkout #{self.base_branch_name}"
205
221
  GitReflow.run_command_with_label "git pull origin #{self.base_branch_name}"
206
222
 
207
- case merge_method
223
+ case merge_method.to_s
208
224
  when /squash/i
209
225
  GitReflow.run_command_with_label "git merge --squash #{self.feature_branch_name}"
210
226
  else
211
227
  GitReflow.run_command_with_label "git merge #{self.feature_branch_name}"
212
228
  end
213
229
 
214
- GitReflow.append_to_squashed_commit_message(message) if message.length > 0
230
+ GitReflow.append_to_merge_commit_message(message) if message.length > 0
215
231
 
216
232
  if GitReflow.run_command_with_label 'git commit', with_system: true
217
233
  GitReflow.say "Pull request ##{self.number} successfully merged.", :success
@@ -1,9 +1,9 @@
1
- module GitReflow
2
- module GitServer
3
- class MergeError < StandardError
4
- def initialize(msg="Merge failed")
5
- super(msg)
6
- end
7
- end
8
- end
9
- end
1
+ module GitReflow
2
+ module GitServer
3
+ class MergeError < StandardError
4
+ def initialize(msg="Merge failed")
5
+ super(msg)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,2 +1,3 @@
1
1
  require_relative 'rspec/command_line_helpers'
2
2
  require_relative 'rspec/stub_helpers'
3
+ require_relative 'rspec/workflow_helpers'
@@ -2,11 +2,12 @@ require "highline"
2
2
 
3
3
  module GitReflow
4
4
  module RSpec
5
+ # @nodoc
5
6
  module CommandLineHelpers
6
-
7
7
  def stub_command_line
8
8
  $commands_ran = []
9
9
  $stubbed_commands = {}
10
+ $stubbed_runners = Set.new
10
11
  $output = []
11
12
  $says = []
12
13
 
@@ -33,6 +34,7 @@ module GitReflow
33
34
  end
34
35
 
35
36
  def stub_run_for(module_to_stub)
37
+ $stubbed_runners << module_to_stub
36
38
  allow(module_to_stub).to receive(:run) do |command, options|
37
39
  options = { loud: true, blocking: true }.merge(options || {})
38
40
  $commands_ran << Hashie::Mash.new(command: command, options: options)
@@ -52,16 +54,31 @@ module GitReflow
52
54
  $says = []
53
55
  end
54
56
 
55
- def stub_command(command, return_value)
57
+ def stub_command(command:, return_value: "", options: {})
56
58
  $stubbed_commands[command] = return_value
57
- allow(GitReflow::Sandbox).to receive(:run).with(command).and_return(return_value)
59
+ $stubbed_runners.each do |runner|
60
+ allow(runner).to receive(:run).with(command, options) do |command, options|
61
+ options = { loud: true, blocking: true }.merge(options || {})
62
+ $commands_ran << Hashie::Mash.new(command: command, options: options)
63
+ $stubbed_commands[command] = return_value
64
+ raise GitReflow::Sandbox::CommandError.new(return_value, "\"#{command}\" failed to run.") if options[:raise]
65
+ end
66
+ end
67
+ end
68
+
69
+ def stub_command_line_inputs_for(module_to_stub, inputs)
70
+ allow(module_to_stub).to receive(:ask) do |terminal, question|
71
+ return_value = inputs[question]
72
+ question = ""
73
+ return_value
74
+ end
58
75
  end
59
76
 
60
77
  def stub_command_line_inputs(inputs)
61
78
  allow_any_instance_of(HighLine).to receive(:ask) do |terminal, question|
62
- return_value = inputs[question]
63
- question = ""
64
- return_value
79
+ return_value = inputs[question]
80
+ question = ""
81
+ return_value
65
82
  end
66
83
  end
67
84
 
@@ -1,13 +1,13 @@
1
- module GitReflow
2
- module RSpec
3
- module StubHelpers
4
-
5
- def stub_with_fallback(obj, method)
6
- original_method = obj.method(method)
7
- allow(obj).to receive(method).with(anything()) { |*args| original_method.call(*args) }
8
- return allow(obj).to receive(method)
9
- end
10
-
11
- end
12
- end
13
- end
1
+ module GitReflow
2
+ module RSpec
3
+ module StubHelpers
4
+
5
+ def stub_with_fallback(obj, method)
6
+ original_method = obj.method(method)
7
+ allow(obj).to receive(method).with(anything()) { |*args| original_method.call(*args) }
8
+ return allow(obj).to receive(method)
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module GitReflow
2
+ module RSpec
3
+ # @nodoc
4
+ module WorkflowHelpers
5
+ def use_workflow(path)
6
+ allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return(
7
+ GitReflow::Workflows::Core.load_raw_workflow(File.read(path))
8
+ )
9
+ end
10
+
11
+ def suppress_loading_of_external_workflows
12
+ allow(GitReflow::Workflows::Core).to receive(:load__workflow).with("#{GitReflow.git_root_dir}/Workflow").and_return(false)
13
+ return if GitReflow::Config.get('reflow.workflow').to_s.empty?
14
+ allow(GitReflow::Workflows::Core).to receive(:load_workflow).with(GitReflow::Config.get('reflow.workflow')).and_return(false)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,6 +4,7 @@ module GitReflow
4
4
 
5
5
  COLOR_FOR_LABEL = {
6
6
  notice: :yellow,
7
+ info: :yellow,
7
8
  error: :red,
8
9
  deliver_halted: :red,
9
10
  review_halted: :red,
@@ -11,8 +12,16 @@ module GitReflow
11
12
  plain: :white
12
13
  }
13
14
 
15
+ class CommandError < StandardError;
16
+ attr_reader :output
17
+ def initialize(output, *args)
18
+ @output = output
19
+ super(*args)
20
+ end
21
+ end
22
+
14
23
  def run(command, options = {})
15
- options = { loud: true, blocking: true }.merge(options)
24
+ options = { loud: true, blocking: true, raise: false }.merge(options)
16
25
 
17
26
  GitReflow.logger.debug "Running... #{command}"
18
27
 
@@ -21,12 +30,13 @@ module GitReflow
21
30
  else
22
31
  output = %x{#{command}}
23
32
 
24
- if options[:blocking] == true && !$?.success?
25
- abort "\"#{command}\" failed to run."
26
- else
27
- puts output if options[:loud] == true
28
- output
33
+ if !$?.success?
34
+ raise CommandError.new(output, "\"#{command}\" failed to run.") if options[:raise] == true
35
+ abort "\"#{command}\" failed to run." if options[:blocking] == true
29
36
  end
37
+
38
+ puts output if options[:loud] == true
39
+ output
30
40
  end
31
41
  end
32
42
 
@@ -1,3 +1,3 @@
1
1
  module GitReflow
2
- VERSION = "0.8.10"
2
+ VERSION = "0.9.4"
3
3
  end
@@ -1,5 +1,6 @@
1
1
  require 'git_reflow/sandbox'
2
2
  require 'git_reflow/git_helpers'
3
+ require 'bundler/inline'
3
4
 
4
5
  module GitReflow
5
6
  module Workflow
@@ -9,21 +10,129 @@ module GitReflow
9
10
 
10
11
  # @nodoc
11
12
  def self.current
12
- workflow_file = GitReflow::Config.get('reflow.workflow')
13
- if workflow_file.length > 0 and File.exists?(workflow_file)
14
- GitReflow.logger.debug "Using workflow: #{workflow_file}"
15
- eval(File.read(workflow_file))
16
- else
17
- GitReflow.logger.debug "Using core workflow..."
18
- GitReflow::Workflows::Core
13
+ return @current unless @current.nil?
14
+ # First look for a "Workflow" file in the current directory, then check
15
+ # for a global Workflow file stored in git-reflow git config.
16
+ loaded_local_workflow = GitReflow::Workflows::Core.load_workflow "#{GitReflow.git_root_dir}/Workflow"
17
+ loaded_global_workflow = false
18
+
19
+ unless loaded_local_workflow
20
+ loaded_global_workflow = GitReflow::Workflows::Core.load_workflow GitReflow::Config.get('reflow.workflow')
19
21
  end
22
+
23
+ @current = GitReflow::Workflows::Core
24
+ end
25
+
26
+ # @nodoc
27
+ # This is primarily a helper method for tests. Due to the nature of how the
28
+ # tests load many different workflows, this helps start fresh and isolate
29
+ # the scenario at hand.
30
+ def self.reset!
31
+ GitReflow.logger.debug "Resetting GitReflow workflow..."
32
+ current.commands = {}
33
+ current.callbacks = { before: {}, after: {}}
34
+ @current = nil
35
+ # We'll need to reload the core class again in order to clear previously
36
+ # eval'd content in the context of the class
37
+ load File.expand_path('../workflows/core.rb', __FILE__)
20
38
  end
21
39
 
22
40
  module ClassMethods
23
41
  include GitReflow::Sandbox
24
42
  include GitReflow::GitHelpers
25
43
 
26
- # Creates a singleton method on the inlcuded class
44
+ def commands
45
+ @commands ||= {}
46
+ end
47
+
48
+ def commands=(command_hash)
49
+ @commands = command_hash
50
+ end
51
+
52
+ def command_docs
53
+ @command_docs ||= {}
54
+ end
55
+
56
+ def command_docs=(command_doc_hash)
57
+ @command_docs = command_doc_hash
58
+ end
59
+
60
+ def callbacks
61
+ @callbacks ||= {
62
+ before: {},
63
+ after: {}
64
+ }
65
+ end
66
+
67
+ def callbacks=(callback_hash)
68
+ @callbacks = callback_hash
69
+ end
70
+
71
+ # Proxy our Config class so that it's available in workflow files
72
+ def git_config
73
+ GitReflow::Config
74
+ end
75
+
76
+ def git_server
77
+ GitReflow.git_server
78
+ end
79
+
80
+ def logger
81
+ GitReflow.logger
82
+ end
83
+
84
+ # Checks for an installed gem, and if none is installed use bundler's
85
+ # inline gemfile to install it.
86
+ #
87
+ # @param name [String] the name of the gem to require as a dependency
88
+ def use_gem(name, *args)
89
+ run("gem list -ie #{name}", loud: false, raise: true)
90
+ logger.info "Using installed gem '#{name}' with options: #{args.inspect}"
91
+ rescue ::GitReflow::Sandbox::CommandError => e
92
+ abort e.message unless e.output =~ /\Afalse/
93
+ logger.info "Installing gem '#{name}' with options: #{args.inspect}"
94
+ say "Installing gem '#{name}'...", :notice
95
+ gemfile do
96
+ source "https://rubygems.org"
97
+ gem name, *args
98
+ end
99
+ end
100
+
101
+ # Use bundler's inline gemfile to install dependencies.
102
+ # See: https://bundler.io/v1.16/guides/bundler_in_a_single_file_ruby_script.html
103
+ #
104
+ # @yield A block to be executed in the context of Bundler's `gemfile` DSL
105
+ def use_gemfile(&block)
106
+ logger.info "Using a custom gemfile"
107
+ gemfile(true, &block)
108
+ end
109
+
110
+ # Loads a pre-defined workflow (FlatMergeWorkflow) from within another
111
+ # Workflow file
112
+ #
113
+ # @param name [String] the name of the Workflow file to use as a basis
114
+ def use(workflow_name)
115
+ if workflows.key?(workflow_name)
116
+ GitReflow.logger.debug "Using Workflow: #{workflow_name}"
117
+ GitReflow::Workflows::Core.load_workflow(workflows[workflow_name])
118
+ else
119
+ GitReflow.logger.error "Tried to use non-existent Workflow: #{workflow_name}"
120
+ end
121
+ end
122
+
123
+ # Keeps track of available workflows when using `.use(workflow_name)`
124
+ # Workflow file
125
+ #
126
+ # @return [Hash, nil] A hash with [workflow_name, workflow_path] as key/value pairs
127
+ def workflows
128
+ return @workflows if @workflows
129
+ workflow_paths = Dir["#{File.dirname(__FILE__)}/workflows/*Workflow"]
130
+ @workflows = {}
131
+ workflow_paths.each { |p| @workflows[File.basename(p)] = p }
132
+ @workflows
133
+ end
134
+
135
+ # Creates a singleton method on the included class
27
136
  #
28
137
  # This method will take any number of keyword parameters. If @defaults keyword is provided, and the given
29
138
  # key(s) in the defaults are not provided as keyword parameters, then it will use the value given in the
@@ -34,11 +143,24 @@ module GitReflow
34
143
  #
35
144
  # @yield [a:, b:, c:, ...] Invokes the block with an arbitrary number of keyword arguments
36
145
  def command(name, **params, &block)
37
- defaults = params[:defaults] || {}
146
+ params[:flags] ||= {}
147
+ params[:switches] ||= {}
148
+ params[:arguments] ||= {}
149
+ defaults ||= params[:arguments].merge(params[:flags]).merge(params[:switches])
150
+
151
+ # Ensure flags and switches use kebab-case
152
+ kebab_case_keys!(params[:flags])
153
+ kebab_case_keys!(params[:switches])
154
+
155
+ # Register the command with the workflow so that we can properly handle
156
+ # option parsing from the command line
157
+ self.commands[name] = params
158
+ self.command_docs[name] = params
159
+
38
160
  self.define_singleton_method(name) do |**args|
39
161
  args_with_defaults = {}
40
162
  args.each do |name, value|
41
- if "#{value}".length <= 0
163
+ if "#{value}".length <= 0 && !defaults[name].nil?
42
164
  args_with_defaults[name] = defaults[name]
43
165
  else
44
166
  args_with_defaults[name] = value
@@ -51,9 +173,182 @@ module GitReflow
51
173
  end
52
174
  end
53
175
 
176
+ GitReflow.logger.debug "callbacks: #{callbacks.inspect}"
177
+ Array(callbacks[:before][name]).each do |block|
178
+ GitReflow.logger.debug "(before) callback running for `#{name}` command..."
179
+ argument_overrides = block.call(**args_with_defaults) || {}
180
+ args_with_defaults.merge!(argument_overrides) if argument_overrides.is_a?(Hash)
181
+ end
182
+
183
+ GitReflow.logger.info "Running command `#{name}` with args: #{args_with_defaults.inspect}..."
54
184
  block.call(**args_with_defaults)
185
+
186
+ Array(callbacks[:after][name]).each do |block|
187
+ GitReflow.logger.debug "(after) callback running for `#{name}` command..."
188
+ block.call(**args_with_defaults)
189
+ end
55
190
  end
56
191
  end
192
+
193
+ # Stores a Proc to be called once the command successfully finishes
194
+ #
195
+ # Procs declared with `before` are executed sequentially in the order they are defined in a custom Workflow
196
+ # file.
197
+ #
198
+ # @param name [Symbol] the name of the method to create
199
+ #
200
+ # @yield A block to be executed before the given command. These blocks
201
+ # are executed in the context of `GitReflow::Workflows::Core`
202
+ def before(name, &block)
203
+ name = name.to_sym
204
+ if commands[name].nil?
205
+ GitReflow.logger.error "Attempted to register (before) callback for non-existing command: #{name}"
206
+ else
207
+ GitReflow.logger.debug "(before) callback registered for: #{name}"
208
+ callbacks[:before][name] ||= []
209
+ callbacks[:before][name] << block
210
+ end
211
+ end
212
+
213
+ # Stores a Proc to be called once the command successfully finishes
214
+ #
215
+ # Procs declared with `after` are executed sequentially in the order they are defined in a custom Workflow
216
+ # file.
217
+ #
218
+ # @param name [Symbol] the name of the method to create
219
+ #
220
+ # @yield A block to be executed after the given command. These blocks
221
+ # are executed in the context of `GitReflow::Workflows::Core`
222
+ def after(name, &block)
223
+ name = name.to_sym
224
+ if commands[name].nil?
225
+ GitReflow.logger.error "Attempted to register (after) callback for non-existing command: #{name}"
226
+ else
227
+ GitReflow.logger.debug "(after) callback registered for: #{name}"
228
+ callbacks[:after][name] ||= []
229
+ callbacks[:after][name] << block
230
+ end
231
+ end
232
+
233
+ # Creates a singleton method on the included class
234
+ #
235
+ # This method updates the help text associated with the provided command.
236
+ #
237
+ # @param name [Symbol] the name of the command to add/update help text for
238
+ # @param defaults [Hash] keyword arguments to provide fallbacks for
239
+ def command_help(name, summary:, arguments: {}, flags: {}, switches: {}, description: "")
240
+ command_docs[name] = {
241
+ summary: summary,
242
+ description: description,
243
+ arguments: arguments,
244
+ flags: kebab_case_keys!(flags),
245
+ switches: kebab_case_keys!(switches)
246
+ }
247
+ end
248
+
249
+ # Outputs documentation for the provided command
250
+ #
251
+ # @param name [Symbol] the name of the command to output help text for
252
+ def documentation_for_command(name)
253
+ name = name.to_sym
254
+ docs = command_docs[name]
255
+ if !docs.nil?
256
+ GitReflow.say "USAGE"
257
+ GitReflow.say " git-reflow #{name} [command options] #{docs[:arguments].keys.map {|arg| "[#{arg}]" }.join(' ')}"
258
+ if docs[:arguments].any?
259
+ GitReflow.say "ARGUMENTS"
260
+ docs[:arguments].each do |arg_name, arg_desc|
261
+ default_text = commands[name][:arguments][arg_name].nil? ? "" : "(default: #{commands[name][:arguments][arg_name]}) "
262
+ GitReflow.say " #{arg_name} – #{default_text}#{arg_desc}"
263
+ end
264
+ end
265
+ if docs[:flags].any? || docs[:switches].any?
266
+ cmd = commands[name.to_sym]
267
+ GitReflow.say "COMMAND OPTIONS"
268
+ docs[:flags].each do |flag_name, flag_desc|
269
+ flag_names = ["-#{flag_name.to_s[0]}", "--#{flag_name}"]
270
+ flag_default = cmd[:flags][flag_name]
271
+
272
+ GitReflow.say " #{flag_names} – #{!flag_default.nil? ? "(default: #{flag_default}) " : ""}#{flag_desc}"
273
+ end
274
+ docs[:switches].each do |switch_name, switch_desc|
275
+ switch_names = [switch_name.to_s[0], "-#{switch_name}"].map {|s| "-#{s}" }.join(', ')
276
+ switch_default = cmd[:switches][switch_name]
277
+
278
+ GitReflow.say " #{switch_names} – #{!switch_default.nil? ? "(default: #{switch_default}) " : ""}#{switch_desc}"
279
+ end
280
+ end
281
+ else
282
+ help
283
+ end
284
+ end
285
+
286
+ # Outputs documentation for git-reflow
287
+ def help
288
+ GitReflow.say "NAME"
289
+ GitReflow.say " git-reflow – Git Reflow manages your git workflow."
290
+ GitReflow.say "VERSION"
291
+ GitReflow.say " #{GitReflow::VERSION}"
292
+ GitReflow.say "USAGE"
293
+ GitReflow.say " git-reflow command [command options] [arguments...]"
294
+ GitReflow.say "COMMANDS"
295
+ command_docs.each do |command_name, command_doc|
296
+ GitReflow.say " #{command_name}\t– #{command_doc[:summary]}"
297
+ end
298
+ end
299
+
300
+ # Parses ARGV for the provided git-reflow command name
301
+ #
302
+ # @param name [Symbol, String] the name of the git-reflow command to parse from ARGV
303
+ def parse_command_options!(name)
304
+ name = name.to_sym
305
+ options = {}
306
+ docs = command_docs[name]
307
+ OptionParser.new do |opts|
308
+ opts.banner = "USAGE:\n git-reflow #{name} [command options] #{docs[:arguments].keys.map {|arg| "[#{arg}]" }.join(' ')}"
309
+ opts.separator ""
310
+ opts.separator "COMMAND OPTIONS:" if docs[:flags].any? || docs[:switches].any?
311
+
312
+ self.commands[name][:flags].each do |flag_name, flag_default|
313
+ opts.on("-#{flag_name[0]}", "--#{flag_name} #{flag_name.upcase}", command_docs[name][:flags][flag_name]) do |f|
314
+ options[kebab_to_underscore(flag_name)] = f || flag_default
315
+ end
316
+ end
317
+
318
+ self.commands[name][:switches].each do |switch_name, switch_default|
319
+ opts.on("-#{switch_name[0]}", "--[no-]#{switch_name}", command_docs[name][:switches][switch_name]) do |s|
320
+ options[kebab_to_underscore(switch_name)] = s || switch_default
321
+ end
322
+ end
323
+ end.parse!
324
+
325
+ # Add arguments to optiosn to pass to defined commands
326
+ commands[name][:arguments].each do |arg_name, arg_default|
327
+ options[arg_name] = ARGV.shift || arg_default
328
+ end
329
+ options
330
+ rescue OptionParser::InvalidOption
331
+ documentation_for_command(name)
332
+ exit 1
333
+ end
334
+
335
+ private
336
+
337
+ def kebab_case_keys!(hsh)
338
+ hsh.keys.each do |key_to_update|
339
+ hsh[underscore_to_kebab(key_to_update)] = hsh.delete(key_to_update) if key_to_update =~ /_/
340
+ end
341
+
342
+ hsh
343
+ end
344
+
345
+ def kebab_to_underscore(sym_or_string)
346
+ sym_or_string.to_s.gsub('-', '_').to_sym
347
+ end
348
+
349
+ def underscore_to_kebab(sym_or_string)
350
+ sym_or_string.to_s.gsub('_', '-').to_sym
351
+ end
57
352
  end
58
353
  end
59
354
  end