git_reflow 0.8.10 → 0.9.4

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