git_reflow 0.8.10 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.ruby-version +1 -1
  4. data/Appraisals +1 -6
  5. data/CHANGELOG.md +426 -348
  6. data/Gemfile.lock +15 -19
  7. data/LICENSE +20 -20
  8. data/README.md +27 -7
  9. data/Rakefile +8 -8
  10. data/bin/console +7 -7
  11. data/bin/setup +6 -6
  12. data/circle.yml +5 -5
  13. data/exe/git-reflow +9 -30
  14. data/git_reflow.gemspec +1 -2
  15. data/lib/git_reflow/config.rb +22 -13
  16. data/lib/git_reflow/git_helpers.rb +69 -22
  17. data/lib/git_reflow/git_server/base.rb +68 -68
  18. data/lib/git_reflow/git_server/git_hub/pull_request.rb +15 -13
  19. data/lib/git_reflow/git_server/pull_request.rb +4 -2
  20. data/lib/git_reflow/merge_error.rb +9 -9
  21. data/lib/git_reflow/rspec/command_line_helpers.rb +9 -1
  22. data/lib/git_reflow/rspec/stub_helpers.rb +13 -13
  23. data/lib/git_reflow/rspec/workflow_helpers.rb +18 -0
  24. data/lib/git_reflow/rspec.rb +1 -0
  25. data/lib/git_reflow/sandbox.rb +1 -0
  26. data/lib/git_reflow/version.rb +1 -1
  27. data/lib/git_reflow/workflow.rb +277 -9
  28. data/lib/git_reflow/workflows/FlatMergeWorkflow +38 -0
  29. data/lib/git_reflow/workflows/core.rb +208 -79
  30. data/lib/git_reflow.rb +3 -14
  31. data/spec/fixtures/awesome_workflow.rb +2 -6
  32. data/spec/fixtures/git/git_config +7 -7
  33. data/spec/fixtures/issues/comment.json.erb +27 -27
  34. data/spec/fixtures/issues/comments.json +29 -29
  35. data/spec/fixtures/issues/comments.json.erb +15 -15
  36. data/spec/fixtures/pull_requests/comment.json.erb +45 -45
  37. data/spec/fixtures/pull_requests/comments.json +47 -47
  38. data/spec/fixtures/pull_requests/comments.json.erb +15 -15
  39. data/spec/fixtures/pull_requests/commits.json +29 -29
  40. data/spec/fixtures/pull_requests/external_pull_request.json +145 -145
  41. data/spec/fixtures/pull_requests/pull_request.json +142 -142
  42. data/spec/fixtures/pull_requests/pull_request.json.erb +142 -142
  43. data/spec/fixtures/pull_requests/pull_request_branch_nonexistent_error.json +32 -0
  44. data/spec/fixtures/pull_requests/pull_request_exists_error.json +32 -32
  45. data/spec/fixtures/pull_requests/pull_requests.json +136 -136
  46. data/spec/fixtures/repositories/commit.json +53 -53
  47. data/spec/fixtures/repositories/commit.json.erb +53 -53
  48. data/spec/fixtures/repositories/commits.json.erb +13 -13
  49. data/spec/fixtures/repositories/statuses.json +31 -31
  50. data/spec/lib/git_reflow/git_helpers_spec.rb +115 -12
  51. data/spec/lib/git_reflow/git_server/git_hub/pull_request_spec.rb +6 -6
  52. data/spec/lib/git_reflow/git_server/pull_request_spec.rb +9 -3
  53. data/spec/lib/git_reflow/workflow_spec.rb +190 -11
  54. data/spec/lib/git_reflow/workflows/core_spec.rb +224 -65
  55. data/spec/lib/git_reflow/workflows/flat_merge_spec.rb +17 -6
  56. data/spec/lib/git_reflow_spec.rb +2 -25
  57. data/spec/spec_helper.rb +3 -0
  58. data/spec/support/github_helpers.rb +1 -1
  59. data/spec/support/mock_pull_request.rb +17 -17
  60. data/spec/support/web_mocks.rb +39 -39
  61. metadata +9 -28
  62. data/lib/git_reflow/commands/deliver.rb +0 -10
  63. data/lib/git_reflow/commands/refresh.rb +0 -20
  64. data/lib/git_reflow/commands/review.rb +0 -13
  65. data/lib/git_reflow/commands/setup.rb +0 -11
  66. data/lib/git_reflow/commands/stage.rb +0 -9
  67. data/lib/git_reflow/commands/start.rb +0 -18
  68. data/lib/git_reflow/commands/status.rb +0 -7
  69. data/lib/git_reflow/workflows/flat_merge.rb +0 -10
  70. data/spec/fixtures/workflow_with_super.rb +0 -8
@@ -1,68 +1,68 @@
1
- require 'git_reflow/config'
2
-
3
- module GitReflow
4
- class GitServer::Base
5
- extend GitHelpers
6
-
7
- @@connection = nil
8
-
9
- def initialize(options)
10
- site_url = self.class.site_url
11
- api_endpoint = self.class.api_endpoint
12
-
13
- self.class.site_url = site_url
14
- self.class.api_endpoint = api_endpoint
15
-
16
- authenticate
17
- end
18
-
19
- def self.connection
20
- raise "#{self.class.to_s}.connection method must be implemented"
21
- end
22
-
23
- def self.user
24
- raise "#{self.class.to_s}.user method must be implemented"
25
- end
26
-
27
- def self.api_endpoint
28
- raise "#{self.class.to_s}.api_endpoint method must be implemented"
29
- end
30
-
31
- def self.api_endpoint=(api_endpoint, options = {local: false})
32
- raise "#{self.class.to_s}.api_endpoint= method must be implemented"
33
- end
34
-
35
- def self.site_url
36
- raise "#{self.class.to_s}.site_url method must be implemented"
37
- end
38
-
39
- def self.site_url=(site_url, options = {local: false})
40
- raise "#{self.class.to_s}.site_url= method must be implemented"
41
- end
42
-
43
- def self.project_only?
44
- GitReflow::Config.get("reflow.local-projects", all: true).include? "#{remote_user}/#{remote_repo_name}"
45
- end
46
-
47
- def connection
48
- @connection ||= self.class.connection
49
- end
50
-
51
- def authenticate
52
- raise "#{self.class.to_s}#authenticate method must be implemented"
53
- end
54
-
55
- def find_open_pull_request(options)
56
- raise "#{self.class.to_s}#find_open_pull_request(options) method must be implemented"
57
- end
58
-
59
- def get_build_status sha
60
- raise "#{self.class.to_s}#get_build_status(sha) method must be implemented"
61
- end
62
-
63
- def colorized_build_description status
64
- raise "#{self.class.to_s}#colorized_build_description(status) method must be implemented"
65
- end
66
-
67
- end
68
- end
1
+ require 'git_reflow/config'
2
+
3
+ module GitReflow
4
+ class GitServer::Base
5
+ extend GitHelpers
6
+
7
+ @@connection = nil
8
+
9
+ def initialize(options)
10
+ site_url = self.class.site_url
11
+ api_endpoint = self.class.api_endpoint
12
+
13
+ self.class.site_url = site_url
14
+ self.class.api_endpoint = api_endpoint
15
+
16
+ authenticate
17
+ end
18
+
19
+ def self.connection
20
+ raise "#{self.class.to_s}.connection method must be implemented"
21
+ end
22
+
23
+ def self.user
24
+ raise "#{self.class.to_s}.user method must be implemented"
25
+ end
26
+
27
+ def self.api_endpoint
28
+ raise "#{self.class.to_s}.api_endpoint method must be implemented"
29
+ end
30
+
31
+ def self.api_endpoint=(api_endpoint, options = {local: false})
32
+ raise "#{self.class.to_s}.api_endpoint= method must be implemented"
33
+ end
34
+
35
+ def self.site_url
36
+ raise "#{self.class.to_s}.site_url method must be implemented"
37
+ end
38
+
39
+ def self.site_url=(site_url, options = {local: false})
40
+ raise "#{self.class.to_s}.site_url= method must be implemented"
41
+ end
42
+
43
+ def self.project_only?
44
+ GitReflow::Config.get("reflow.local-projects", all: true).include? "#{remote_user}/#{remote_repo_name}"
45
+ end
46
+
47
+ def connection
48
+ @connection ||= self.class.connection
49
+ end
50
+
51
+ def authenticate
52
+ raise "#{self.class.to_s}#authenticate method must be implemented"
53
+ end
54
+
55
+ def find_open_pull_request(options)
56
+ raise "#{self.class.to_s}#find_open_pull_request(options) method must be implemented"
57
+ end
58
+
59
+ def get_build_status sha
60
+ raise "#{self.class.to_s}#get_build_status(sha) method must be implemented"
61
+ end
62
+
63
+ def colorized_build_description status
64
+ raise "#{self.class.to_s}#colorized_build_description(status) method must be implemented"
65
+ end
66
+
67
+ end
68
+ end
@@ -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}",
@@ -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
@@ -204,14 +206,14 @@ module GitReflow
204
206
  GitReflow.run_command_with_label "git checkout #{self.base_branch_name}"
205
207
  GitReflow.run_command_with_label "git pull origin #{self.base_branch_name}"
206
208
 
207
- case merge_method
209
+ case merge_method.to_s
208
210
  when /squash/i
209
211
  GitReflow.run_command_with_label "git merge --squash #{self.feature_branch_name}"
210
212
  else
211
213
  GitReflow.run_command_with_label "git merge #{self.feature_branch_name}"
212
214
  end
213
215
 
214
- GitReflow.append_to_squashed_commit_message(message) if message.length > 0
216
+ GitReflow.append_to_merge_commit_message(message) if message.length > 0
215
217
 
216
218
  if GitReflow.run_command_with_label 'git commit', with_system: true
217
219
  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
@@ -2,8 +2,8 @@ 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 = {}
@@ -57,6 +57,14 @@ module GitReflow
57
57
  allow(GitReflow::Sandbox).to receive(:run).with(command).and_return(return_value)
58
58
  end
59
59
 
60
+ def stub_command_line_inputs_for(module_to_stub, inputs)
61
+ allow(module_to_stub).to receive(:ask) do |terminal, question|
62
+ return_value = inputs[question]
63
+ question = ""
64
+ return_value
65
+ end
66
+ end
67
+
60
68
  def stub_command_line_inputs(inputs)
61
69
  allow_any_instance_of(HighLine).to receive(:ask) do |terminal, question|
62
70
  return_value = inputs[question]
@@ -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
@@ -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'
@@ -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,
@@ -1,3 +1,3 @@
1
1
  module GitReflow
2
- VERSION = "0.8.10"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -9,20 +9,102 @@ module GitReflow
9
9
 
10
10
  # @nodoc
11
11
  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
12
+ return @current unless @current.nil?
13
+ # First look for a "Workflow" file in the current directory, then check
14
+ # for a global Workflow file stored in git-reflow git config.
15
+ loaded_local_workflow = GitReflow::Workflows::Core.load_workflow "#{GitReflow.git_root_dir}/Workflow"
16
+ loaded_global_workflow = false
17
+
18
+ unless loaded_local_workflow
19
+ loaded_global_workflow = GitReflow::Workflows::Core.load_workflow GitReflow::Config.get('reflow.workflow')
19
20
  end
21
+
22
+ @current = GitReflow::Workflows::Core
23
+ end
24
+
25
+ # @nodoc
26
+ # This is primarily a helper method for tests. Due to the nature of how the
27
+ # tests load many different workflows, this helps start fresh and isolate
28
+ # the scenario at hand.
29
+ def self.reset!
30
+ GitReflow.logger.debug "Resetting GitReflow workflow..."
31
+ current.commands = {}
32
+ current.callbacks = { before: {}, after: {}}
33
+ @current = nil
34
+ # We'll need to reload the core class again in order to clear previously
35
+ # eval'd content in the context of the class
36
+ load File.expand_path('../workflows/core.rb', __FILE__)
20
37
  end
21
38
 
22
39
  module ClassMethods
23
40
  include GitReflow::Sandbox
24
41
  include GitReflow::GitHelpers
25
42
 
43
+ def commands
44
+ @commands ||= {}
45
+ end
46
+
47
+ def commands=(command_hash)
48
+ @commands = command_hash
49
+ end
50
+
51
+ def command_docs
52
+ @command_docs ||= {}
53
+ end
54
+
55
+ def command_docs=(command_doc_hash)
56
+ @command_docs = command_doc_hash
57
+ end
58
+
59
+ def callbacks
60
+ @callbacks ||= {
61
+ before: {},
62
+ after: {}
63
+ }
64
+ end
65
+
66
+ def callbacks=(callback_hash)
67
+ @callbacks = callback_hash
68
+ end
69
+
70
+ # Proxy our Config class so that it's available in workflow files
71
+ def git_config
72
+ GitReflow::Config
73
+ end
74
+
75
+ def git_server
76
+ GitReflow.git_server
77
+ end
78
+
79
+ def logger
80
+ GitReflow.logger
81
+ end
82
+
83
+ # Loads a pre-defined workflow (FlatMergeWorkflow) from within another
84
+ # Workflow file
85
+ #
86
+ # @param name [String] the name of the Workflow file to use as a basis
87
+ def use(workflow_name)
88
+ if workflows.key?(workflow_name)
89
+ GitReflow.logger.debug "Using Workflow: #{workflow_name}"
90
+ GitReflow::Workflows::Core.load_workflow(workflows[workflow_name])
91
+ else
92
+ GitReflow.logger.error "Tried to use non-existent Workflow: #{workflow_name}"
93
+ end
94
+ end
95
+
96
+ # Keeps track of available workflows when using `.use(workflow_name)`
97
+ # Workflow file
98
+ #
99
+ # @return [Hash, nil] A hash with [workflow_name, workflow_path] as key/value pairs
100
+ def workflows
101
+ return @workflows if @workflows
102
+ workflow_paths = Dir["#{File.dirname(__FILE__)}/workflows/*Workflow"]
103
+ @workflows = {}
104
+ workflow_paths.each { |p| @workflows[File.basename(p)] = p }
105
+ @workflows
106
+ end
107
+
26
108
  # Creates a singleton method on the inlcuded class
27
109
  #
28
110
  # This method will take any number of keyword parameters. If @defaults keyword is provided, and the given
@@ -34,11 +116,24 @@ module GitReflow
34
116
  #
35
117
  # @yield [a:, b:, c:, ...] Invokes the block with an arbitrary number of keyword arguments
36
118
  def command(name, **params, &block)
37
- defaults = params[:defaults] || {}
119
+ params[:flags] ||= {}
120
+ params[:switches] ||= {}
121
+ params[:arguments] ||= {}
122
+ defaults ||= params[:arguments].merge(params[:flags]).merge(params[:switches])
123
+
124
+ # Ensure flags and switches use kebab-case
125
+ kebab_case_keys!(params[:flags])
126
+ kebab_case_keys!(params[:switches])
127
+
128
+ # Register the command with the workflow so that we can properly handle
129
+ # option parsing from the command line
130
+ self.commands[name] = params
131
+ self.command_docs[name] = params
132
+
38
133
  self.define_singleton_method(name) do |**args|
39
134
  args_with_defaults = {}
40
135
  args.each do |name, value|
41
- if "#{value}".length <= 0
136
+ if "#{value}".length <= 0 && !defaults[name].nil?
42
137
  args_with_defaults[name] = defaults[name]
43
138
  else
44
139
  args_with_defaults[name] = value
@@ -51,9 +146,182 @@ module GitReflow
51
146
  end
52
147
  end
53
148
 
149
+ GitReflow.logger.debug "callbacks: #{callbacks.inspect}"
150
+ Array(callbacks[:before][name]).each do |block|
151
+ GitReflow.logger.debug "(before) callback running for `#{name}` command..."
152
+ argument_overrides = block.call(**args_with_defaults) || {}
153
+ args_with_defaults.merge!(argument_overrides) if argument_overrides.is_a?(Hash)
154
+ end
155
+
156
+ GitReflow.logger.debug "Running command `#{name}` with args: #{args_with_defaults.inspect}..."
54
157
  block.call(**args_with_defaults)
158
+
159
+ Array(callbacks[:after][name]).each do |block|
160
+ GitReflow.logger.debug "(after) callback running for `#{name}` command..."
161
+ block.call(**args_with_defaults)
162
+ end
55
163
  end
56
164
  end
165
+
166
+ # Stores a Proc to be called once the command successfully finishes
167
+ #
168
+ # Procs declared with `before` are executed sequentially in the order they are defined in a custom Workflow
169
+ # file.
170
+ #
171
+ # @param name [Symbol] the name of the method to create
172
+ #
173
+ # @yield A block to be executed before the given command. These blocks
174
+ # are executed in the context of `GitReflow::Workflows::Core`
175
+ def before(name, &block)
176
+ name = name.to_sym
177
+ if commands[name].nil?
178
+ GitReflow.logger.error "Attempted to register (before) callback for non-existing command: #{name}"
179
+ else
180
+ GitReflow.logger.debug "(before) callback registered for: #{name}"
181
+ callbacks[:before][name] ||= []
182
+ callbacks[:before][name] << block
183
+ end
184
+ end
185
+
186
+ # Stores a Proc to be called once the command successfully finishes
187
+ #
188
+ # Procs declared with `after` are executed sequentially in the order they are defined in a custom Workflow
189
+ # file.
190
+ #
191
+ # @param name [Symbol] the name of the method to create
192
+ #
193
+ # @yield A block to be executed after the given command. These blocks
194
+ # are executed in the context of `GitReflow::Workflows::Core`
195
+ def after(name, &block)
196
+ name = name.to_sym
197
+ if commands[name].nil?
198
+ GitReflow.logger.error "Attempted to register (after) callback for non-existing command: #{name}"
199
+ else
200
+ GitReflow.logger.debug "(after) callback registered for: #{name}"
201
+ callbacks[:after][name] ||= []
202
+ callbacks[:after][name] << block
203
+ end
204
+ end
205
+
206
+ # Creates a singleton method on the inlcuded class
207
+ #
208
+ # This method updates the help text associated with the provided command.
209
+ #
210
+ # @param name [Symbol] the name of the command to add/update help text for
211
+ # @param defaults [Hash] keyword arguments to provide fallbacks for
212
+ def command_help(name, summary:, arguments: {}, flags: {}, switches: {}, description: "")
213
+ command_docs[name] = {
214
+ summary: summary,
215
+ description: description,
216
+ arguments: arguments,
217
+ flags: kebab_case_keys!(flags),
218
+ switches: kebab_case_keys!(switches)
219
+ }
220
+ end
221
+
222
+ # Outputs documentation for the provided command
223
+ #
224
+ # @param name [Symbol] the name of the command to output help text for
225
+ def documentation_for_command(name)
226
+ name = name.to_sym
227
+ docs = command_docs[name]
228
+ if !docs.nil?
229
+ GitReflow.say "USAGE"
230
+ GitReflow.say " git-reflow #{name} [command options] #{docs[:arguments].keys.map {|arg| "[#{arg}]" }.join(' ')}"
231
+ if docs[:arguments].any?
232
+ GitReflow.say "ARGUMENTS"
233
+ docs[:arguments].each do |arg_name, arg_desc|
234
+ default_text = commands[name][:arguments][arg_name].nil? ? "" : "(default: #{commands[name][:arguments][arg_name]}) "
235
+ GitReflow.say " #{arg_name} – #{default_text}#{arg_desc}"
236
+ end
237
+ end
238
+ if docs[:flags].any? || docs[:switches].any?
239
+ cmd = commands[name.to_sym]
240
+ GitReflow.say "COMMAND OPTIONS"
241
+ docs[:flags].each do |flag_name, flag_desc|
242
+ flag_names = ["-#{flag_name.to_s[0]}", "--#{flag_name}"]
243
+ flag_default = cmd[:flags][flag_name]
244
+
245
+ GitReflow.say " #{flag_names} – #{!flag_default.nil? ? "(default: #{flag_default}) " : ""}#{flag_desc}"
246
+ end
247
+ docs[:switches].each do |switch_name, switch_desc|
248
+ switch_names = [switch_name.to_s[0], "-#{switch_name}"].map {|s| "-#{s}" }.join(', ')
249
+ switch_default = cmd[:switches][switch_name]
250
+
251
+ GitReflow.say " #{switch_names} – #{!switch_default.nil? ? "(default: #{switch_default}) " : ""}#{switch_desc}"
252
+ end
253
+ end
254
+ else
255
+ help
256
+ end
257
+ end
258
+
259
+ # Outputs documentation for git-reflow
260
+ def help
261
+ GitReflow.say "NAME"
262
+ GitReflow.say " git-reflow – Git Reflow manages your git workflow."
263
+ GitReflow.say "VERSION"
264
+ GitReflow.say " #{GitReflow::VERSION}"
265
+ GitReflow.say "USAGE"
266
+ GitReflow.say " git-reflow command [command options] [arguments...]"
267
+ GitReflow.say "COMMANDS"
268
+ command_docs.each do |command_name, command_doc|
269
+ GitReflow.say " #{command_name}\t– #{command_doc[:summary]}"
270
+ end
271
+ end
272
+
273
+ # Parses ARGV for the provided git-reflow command name
274
+ #
275
+ # @param name [Symbol, String] the name of the git-reflow command to parse from ARGV
276
+ def parse_command_options!(name)
277
+ name = name.to_sym
278
+ options = {}
279
+ docs = command_docs[name]
280
+ OptionParser.new do |opts|
281
+ opts.banner = "USAGE:\n git-reflow #{name} [command options] #{docs[:arguments].keys.map {|arg| "[#{arg}]" }.join(' ')}"
282
+ opts.separator ""
283
+ opts.separator "COMMAND OPTIONS:" if docs[:flags].any? || docs[:switches].any?
284
+
285
+ self.commands[name][:flags].each do |flag_name, flag_default|
286
+ opts.on("-#{flag_name[0]}", "--#{flag_name} #{flag_name.upcase}", command_docs[name][:flags][flag_name]) do |f|
287
+ options[kebab_to_underscore(flag_name)] = f || flag_default
288
+ end
289
+ end
290
+
291
+ self.commands[name][:switches].each do |switch_name, switch_default|
292
+ opts.on("-#{switch_name[0]}", "--[no-]#{switch_name}", command_docs[name][:switches][switch_name]) do |s|
293
+ options[kebab_to_underscore(switch_name)] = s || switch_default
294
+ end
295
+ end
296
+ end.parse!
297
+
298
+ # Add arguments to optiosn to pass to defined commands
299
+ commands[name][:arguments].each do |arg_name, arg_default|
300
+ options[arg_name] = ARGV.shift || arg_default
301
+ end
302
+ options
303
+ rescue OptionParser::InvalidOption
304
+ documentation_for_command(name)
305
+ exit 1
306
+ end
307
+
308
+ private
309
+
310
+ def kebab_case_keys!(hsh)
311
+ hsh.keys.each do |key_to_update|
312
+ hsh[underscore_to_kebab(key_to_update)] = hsh.delete(key_to_update) if key_to_update =~ /_/
313
+ end
314
+
315
+ hsh
316
+ end
317
+
318
+ def kebab_to_underscore(sym_or_string)
319
+ sym_or_string.to_s.gsub('-', '_').to_sym
320
+ end
321
+
322
+ def underscore_to_kebab(sym_or_string)
323
+ sym_or_string.to_s.gsub('_', '-').to_sym
324
+ end
57
325
  end
58
326
  end
59
327
  end