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.
- checksums.yaml +4 -4
- data/.github/workflows/multi-ruby-tests.yml +33 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/Appraisals +1 -6
- data/CHANGELOG.md +466 -348
- data/Gemfile.lock +100 -70
- data/LICENSE +20 -20
- data/README.md +36 -12
- data/Rakefile +15 -8
- data/Workflow +3 -0
- data/bin/console +7 -7
- data/bin/setup +6 -6
- data/exe/git-reflow +14 -30
- data/git_reflow.gemspec +25 -24
- data/lib/git_reflow.rb +3 -14
- data/lib/git_reflow/config.rb +52 -17
- data/lib/git_reflow/git_helpers.rb +69 -22
- data/lib/git_reflow/git_server/base.rb +68 -68
- data/lib/git_reflow/git_server/git_hub.rb +53 -40
- data/lib/git_reflow/git_server/git_hub/pull_request.rb +25 -17
- data/lib/git_reflow/git_server/pull_request.rb +19 -3
- data/lib/git_reflow/merge_error.rb +9 -9
- data/lib/git_reflow/rspec.rb +1 -0
- data/lib/git_reflow/rspec/command_line_helpers.rb +23 -6
- data/lib/git_reflow/rspec/stub_helpers.rb +13 -13
- data/lib/git_reflow/rspec/workflow_helpers.rb +18 -0
- data/lib/git_reflow/sandbox.rb +16 -6
- data/lib/git_reflow/version.rb +1 -1
- data/lib/git_reflow/workflow.rb +305 -10
- data/lib/git_reflow/workflows/FlatMergeWorkflow +38 -0
- data/lib/git_reflow/workflows/core.rb +208 -79
- data/spec/fixtures/authentication_failure.json +3 -0
- data/spec/fixtures/awesome_workflow.rb +2 -6
- data/spec/fixtures/git/git_config +7 -7
- data/spec/fixtures/issues/comment.json.erb +27 -27
- data/spec/fixtures/issues/comments.json +29 -29
- data/spec/fixtures/issues/comments.json.erb +15 -15
- data/spec/fixtures/pull_requests/comment.json.erb +45 -45
- data/spec/fixtures/pull_requests/comments.json +47 -47
- data/spec/fixtures/pull_requests/comments.json.erb +15 -15
- data/spec/fixtures/pull_requests/commits.json +29 -29
- data/spec/fixtures/pull_requests/external_pull_request.json +145 -145
- data/spec/fixtures/pull_requests/pull_request.json +142 -142
- data/spec/fixtures/pull_requests/pull_request.json.erb +142 -142
- data/spec/fixtures/pull_requests/pull_request_branch_nonexistent_error.json +32 -0
- data/spec/fixtures/pull_requests/pull_request_exists_error.json +32 -32
- data/spec/fixtures/pull_requests/pull_requests.json +136 -136
- data/spec/fixtures/repositories/commit.json +53 -53
- data/spec/fixtures/repositories/commit.json.erb +53 -53
- data/spec/fixtures/repositories/commits.json.erb +13 -13
- data/spec/fixtures/repositories/statuses.json +31 -31
- data/spec/fixtures/users/user.json +32 -0
- data/spec/lib/git_reflow/git_helpers_spec.rb +115 -12
- data/spec/lib/git_reflow/git_server/git_hub/pull_request_spec.rb +6 -6
- data/spec/lib/git_reflow/git_server/git_hub_spec.rb +77 -3
- data/spec/lib/git_reflow/git_server/pull_request_spec.rb +41 -7
- data/spec/lib/git_reflow/workflow_spec.rb +259 -14
- data/spec/lib/git_reflow/workflows/core_spec.rb +224 -65
- data/spec/lib/git_reflow/workflows/flat_merge_spec.rb +17 -6
- data/spec/lib/git_reflow_spec.rb +2 -25
- data/spec/spec_helper.rb +3 -0
- data/spec/support/github_helpers.rb +1 -1
- data/spec/support/mock_pull_request.rb +17 -17
- data/spec/support/web_mocks.rb +39 -39
- metadata +52 -53
- data/circle.yml +0 -26
- data/lib/git_reflow/commands/deliver.rb +0 -10
- data/lib/git_reflow/commands/refresh.rb +0 -20
- data/lib/git_reflow/commands/review.rb +0 -13
- data/lib/git_reflow/commands/setup.rb +0 -11
- data/lib/git_reflow/commands/stage.rb +0 -9
- data/lib/git_reflow/commands/start.rb +0 -18
- data/lib/git_reflow/commands/status.rb +0 -7
- data/lib/git_reflow/workflows/flat_merge.rb +0 -10
- 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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
117
|
+
@connection.connection_options = {headers: {"X-GitHub-OTP" => options[:two_factor_auth_code]}} if options[:two_factor_auth_code]
|
118
118
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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 "
|
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,
|
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
|
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
|
-
|
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} #{
|
105
|
-
merge_message = File.read(
|
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(
|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
data/lib/git_reflow/rspec.rb
CHANGED
@@ -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,
|
57
|
+
def stub_command(command:, return_value: "", options: {})
|
56
58
|
$stubbed_commands[command] = return_value
|
57
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
data/lib/git_reflow/sandbox.rb
CHANGED
@@ -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
|
25
|
-
|
26
|
-
|
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
|
|
data/lib/git_reflow/version.rb
CHANGED
data/lib/git_reflow/workflow.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|