git_reflow 0.8.10 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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