github-pivotal-flow 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/github_pivotal_flow.rb +16 -0
- data/lib/github_pivotal_flow/command.rb +3 -14
- data/lib/github_pivotal_flow/configuration.rb +106 -42
- data/lib/github_pivotal_flow/finish.rb +1 -1
- data/lib/github_pivotal_flow/git.rb +22 -0
- data/lib/github_pivotal_flow/github_api.rb +10 -2
- data/lib/github_pivotal_flow/project.rb +29 -13
- data/lib/github_pivotal_flow/start.rb +4 -8
- data/lib/github_pivotal_flow/story.rb +68 -51
- data/lib/github_pivotal_flow/version.rb +3 -0
- data/spec/github_pivotal_flow/command_spec.rb +31 -0
- data/spec/github_pivotal_flow/configuration_spec.rb +76 -65
- data/spec/github_pivotal_flow/finish_spec.rb +7 -3
- data/spec/github_pivotal_flow/git_spec.rb +1 -1
- data/spec/github_pivotal_flow/start_spec.rb +10 -7
- data/spec/github_pivotal_flow/story_spec.rb +44 -34
- metadata +10 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 423519e9574e2048faaaec36c2583c3ec79273eb
|
4
|
+
data.tar.gz: 03fb90bd37c4fb777432e1e9c6c9732ac90b25cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2aa8e365a3df85d33431476ca8c8b93bf022edce02ec2f2655f9e32e547469fc6ac36aa9978928b257c09f925505c7d0ce69f77cc24f033b28f0237f9826fa44
|
7
|
+
data.tar.gz: e6a57b30856b8cfffad4b65b6f34e51109a6240b666e1c4cd1026b2275e2e7b0f40a0dfc3f7102432ece7a5ca4354b86f6fc0c1ae3a6a905b1b9e75dded30bf3
|
data/lib/github_pivotal_flow.rb
CHANGED
@@ -20,6 +20,22 @@ require 'pivotal-tracker'
|
|
20
20
|
|
21
21
|
require File.join('core_ext', 'object', 'blank')
|
22
22
|
|
23
|
+
module GithubPivotalFlow
|
24
|
+
KEY_USER_NAME = 'user.name'.freeze
|
25
|
+
KEY_API_TOKEN = 'pivotal.api-token'.freeze
|
26
|
+
KEY_PROJECT_ID = 'pivotal.project-id'.freeze
|
27
|
+
KEY_STORY_ID = 'pivotal-story-id'.freeze
|
28
|
+
KEY_FEATURE_PREFIX = 'gitflow.prefix.feature'.freeze
|
29
|
+
KEY_HOTFIX_PREFIX = 'gitflow.prefix.hotfix'.freeze
|
30
|
+
KEY_RELEASE_PREFIX = 'gitflow.prefix.release'.freeze
|
31
|
+
KEY_DEVELOPMENT_BRANCH = 'gitflow.branch.develop'.freeze
|
32
|
+
KEY_MASTER_BRANCH = 'gitflow.branch.master'.freeze
|
33
|
+
KEY_GITHUB_USERNAME = 'github.username'.freeze
|
34
|
+
KEY_GITHUB_API_TOKEN = 'github.api-token'.freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
require File.join('github_pivotal_flow', 'version')
|
38
|
+
|
23
39
|
require File.join('github_pivotal_flow', 'shell')
|
24
40
|
require File.join('github_pivotal_flow', 'git')
|
25
41
|
require File.join('github_pivotal_flow', 'project')
|
@@ -15,21 +15,10 @@ module GithubPivotalFlow
|
|
15
15
|
@options = {}
|
16
16
|
args = parse_argv(*args)
|
17
17
|
@options[:args] = args
|
18
|
-
|
19
|
-
@repository_root = Git.repository_root
|
20
18
|
@configuration = Configuration.new(@options)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@project = PivotalTracker::Project.find @configuration.project_id
|
26
|
-
|
27
|
-
# Make sure that all the git flow config options are set up
|
28
|
-
@configuration.development_branch
|
29
|
-
@configuration.master_branch
|
30
|
-
@configuration.feature_prefix
|
31
|
-
@configuration.hotfix_prefix
|
32
|
-
@configuration.release_prefix
|
19
|
+
# Validate the configuration to make sure everything is set up correctly
|
20
|
+
@configuration.validate
|
21
|
+
@project = @configuration.project
|
33
22
|
end
|
34
23
|
|
35
24
|
# The main entry point to the command's execution
|
@@ -9,6 +9,31 @@ module GithubPivotalFlow
|
|
9
9
|
@github_password_cache = {}
|
10
10
|
end
|
11
11
|
|
12
|
+
def validate
|
13
|
+
repository_root
|
14
|
+
user_name
|
15
|
+
ensure_github_api_token
|
16
|
+
ensure_pivotal_api_token
|
17
|
+
ensure_gitflow_config
|
18
|
+
project.config = self
|
19
|
+
return true
|
20
|
+
end
|
21
|
+
|
22
|
+
def repository_root
|
23
|
+
@repository_root ||= Git.repository_root
|
24
|
+
end
|
25
|
+
|
26
|
+
def user_name
|
27
|
+
user_name = Git.get_config(KEY_USER_NAME, :inherited).strip
|
28
|
+
if user_name.blank?
|
29
|
+
user_name = ask('Github user name (Should be the same as in your Pivotal profile): ').strip
|
30
|
+
Git.set_config(KEY_USER_NAME, user_name, :local) unless user_name.blank?
|
31
|
+
puts
|
32
|
+
end
|
33
|
+
|
34
|
+
user_name
|
35
|
+
end
|
36
|
+
|
12
37
|
# Returns the user's Pivotal Tracker API token. If this token has not been
|
13
38
|
# configured, prompts the user for the value. The value is checked for in
|
14
39
|
# the _inherited_ Git configuration, but is stored in the _global_ Git
|
@@ -18,15 +43,33 @@ module GithubPivotalFlow
|
|
18
43
|
def api_token
|
19
44
|
api_token = @options[:api_token] || Git.get_config(KEY_API_TOKEN, :inherited)
|
20
45
|
|
21
|
-
if api_token.
|
46
|
+
if api_token.blank?
|
22
47
|
api_token = ask('Pivotal API Token (found at https://www.pivotaltracker.com/profile): ').strip
|
23
|
-
Git.set_config
|
48
|
+
Git.set_config(KEY_API_TOKEN, api_token, :local) unless api_token.blank?
|
24
49
|
puts
|
25
50
|
end
|
26
51
|
|
27
52
|
api_token
|
28
53
|
end
|
29
54
|
|
55
|
+
def ensure_pivotal_api_token
|
56
|
+
PivotalTracker::Client.use_ssl = true
|
57
|
+
while (PivotalTracker::Client.token = self.api_token).blank? || PivotalTracker::Project.all.empty?
|
58
|
+
puts "No projects found."
|
59
|
+
clear_pivotal_api_token!
|
60
|
+
end
|
61
|
+
return true
|
62
|
+
rescue RestClient::Unauthorized => e
|
63
|
+
puts "Invalid Pivotal token"
|
64
|
+
clear_pivotal_api_token!
|
65
|
+
retry
|
66
|
+
end
|
67
|
+
|
68
|
+
def clear_pivotal_api_token!
|
69
|
+
PivotalTracker::Client.token = nil
|
70
|
+
Git.delete_config(KEY_API_TOKEN, :local)
|
71
|
+
end
|
72
|
+
|
30
73
|
# Returns the Pivotal Tracker project id for this repository. If this id
|
31
74
|
# has not been configuration, prompts the user for the value. The value is
|
32
75
|
# checked for in the _inherited_ Git configuration, but is stored in the
|
@@ -45,18 +88,22 @@ module GithubPivotalFlow
|
|
45
88
|
end
|
46
89
|
end
|
47
90
|
|
48
|
-
Git.set_config
|
91
|
+
Git.set_config(KEY_PROJECT_ID, project_id, :local)
|
49
92
|
puts
|
50
93
|
end
|
51
94
|
|
52
95
|
project_id
|
53
96
|
end
|
54
97
|
|
55
|
-
|
98
|
+
def project
|
99
|
+
@project ||= Project.new(config: self)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns the story associated with the branch
|
56
103
|
#
|
57
|
-
# @
|
58
|
-
|
59
|
-
|
104
|
+
# @return [Story] the story associated with the current development branch
|
105
|
+
def story
|
106
|
+
return @story if @story
|
60
107
|
story_id = Git.get_config(KEY_STORY_ID, :branch)
|
61
108
|
if story_id.blank? && (matchdata = /^[a-z0-9_\-]+\/(\d+)(-[a-z0-9_\-]+)?$/i.match(Git.current_branch))
|
62
109
|
story_id = matchdata[1]
|
@@ -67,7 +114,7 @@ module GithubPivotalFlow
|
|
67
114
|
Git.set_config(KEY_STORY_ID, story_id, :branch) unless story_id.blank?
|
68
115
|
end
|
69
116
|
return nil if story_id.blank?
|
70
|
-
Story.new(project.stories.find(story_id.to_i), :
|
117
|
+
return (@story = Story.new(project, project.stories.find(story_id.to_i), branch_name: Git.current_branch))
|
71
118
|
end
|
72
119
|
|
73
120
|
# Stores the story associated with the current development branch
|
@@ -86,7 +133,6 @@ module GithubPivotalFlow
|
|
86
133
|
feature_prefix = 'feature/' if feature_prefix.blank?
|
87
134
|
feature_prefix = "#{feature_prefix}/" unless feature_prefix[-1,1] == '/'
|
88
135
|
Git.set_config KEY_FEATURE_PREFIX, feature_prefix, :local
|
89
|
-
puts
|
90
136
|
end
|
91
137
|
|
92
138
|
feature_prefix
|
@@ -100,7 +146,6 @@ module GithubPivotalFlow
|
|
100
146
|
hotfix_prefix = 'hotfix/' if hotfix_prefix.blank?
|
101
147
|
hotfix_prefix = "#{hotfix_prefix}/" unless hotfix_prefix[-1,1] == '/'
|
102
148
|
Git.set_config KEY_HOTFIX_PREFIX, hotfix_prefix, :local
|
103
|
-
puts
|
104
149
|
end
|
105
150
|
|
106
151
|
hotfix_prefix
|
@@ -113,8 +158,7 @@ module GithubPivotalFlow
|
|
113
158
|
release_prefix = ask('Please enter your git-flow release branch prefix: [release/]').strip
|
114
159
|
release_prefix = 'release' if release_prefix.blank?
|
115
160
|
release_prefix = "#{release_prefix}/" unless release_prefix[-1,1] == '/'
|
116
|
-
Git.set_config
|
117
|
-
puts
|
161
|
+
Git.set_config(KEY_RELEASE_PREFIX, release_prefix, :local)
|
118
162
|
end
|
119
163
|
|
120
164
|
release_prefix
|
@@ -125,9 +169,8 @@ module GithubPivotalFlow
|
|
125
169
|
|
126
170
|
if development_branch.empty?
|
127
171
|
development_branch = ask('Please enter your git-flow development branch name: [development]').strip
|
128
|
-
development_branch = 'development' if development_branch.
|
172
|
+
development_branch = 'development' if development_branch.blank?
|
129
173
|
Git.set_config KEY_DEVELOPMENT_BRANCH, development_branch, :local
|
130
|
-
puts
|
131
174
|
end
|
132
175
|
Git.ensure_branch_exists(development_branch)
|
133
176
|
|
@@ -139,20 +182,27 @@ module GithubPivotalFlow
|
|
139
182
|
|
140
183
|
if master_branch.blank?
|
141
184
|
master_branch = ask('Please enter your git-flow production branch name: [master]').strip
|
142
|
-
master_branch = 'master' if master_branch.
|
185
|
+
master_branch = 'master' if master_branch.blank?
|
143
186
|
Git.set_config KEY_MASTER_BRANCH, master_branch, :local
|
144
|
-
puts
|
145
187
|
end
|
146
188
|
Git.ensure_branch_exists(master_branch)
|
147
189
|
|
148
190
|
master_branch
|
149
191
|
end
|
150
192
|
|
151
|
-
def
|
152
|
-
|
193
|
+
def ensure_gitflow_config
|
194
|
+
development_branch && master_branch && feature_prefix && hotfix_prefix && release_prefix
|
195
|
+
end
|
196
|
+
|
197
|
+
def github_client
|
198
|
+
@ghclient ||= GitHubAPI.new(self, :app_url => 'http://github.com/roomorama/github-pivotal-flow')
|
153
199
|
end
|
154
200
|
|
155
|
-
def
|
201
|
+
def github_host
|
202
|
+
project.host
|
203
|
+
end
|
204
|
+
|
205
|
+
def github_username(host = github_host)
|
156
206
|
return ENV['GITHUB_USER'] unless ENV['GITHUB_USER'].to_s.blank?
|
157
207
|
github_username = Git.get_config KEY_GITHUB_USERNAME, :inherited
|
158
208
|
if github_username.blank?
|
@@ -162,27 +212,54 @@ module GithubPivotalFlow
|
|
162
212
|
github_username
|
163
213
|
end
|
164
214
|
|
165
|
-
def github_username=(
|
166
|
-
Git.set_config KEY_GITHUB_USERNAME,
|
215
|
+
def github_username=(username)
|
216
|
+
Git.set_config KEY_GITHUB_USERNAME, username, :local unless username.blank?
|
167
217
|
end
|
168
218
|
|
169
|
-
def github_password(host, user)
|
170
|
-
return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.
|
171
|
-
|
219
|
+
def github_password(host = github_host, user = nil)
|
220
|
+
return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.blank?
|
221
|
+
user ||= github_username(host)
|
222
|
+
@github_password_cache["#{user}"] ||= ask_github_password(user)
|
172
223
|
end
|
173
224
|
|
174
|
-
def
|
225
|
+
def clear_github_auth_data!
|
226
|
+
@github_password_cache = {}
|
227
|
+
Git.delete_config(KEY_GITHUB_USERNAME, :local)
|
228
|
+
end
|
229
|
+
|
230
|
+
def ensure_github_api_token
|
231
|
+
begin
|
232
|
+
repo_found = github_client.repo_exists?(project)
|
233
|
+
end while github_api_token.blank?
|
234
|
+
raise("Could not find github project") unless repo_found
|
235
|
+
rescue Net::HTTPServerException => e
|
236
|
+
case e.response.code
|
237
|
+
when '401'
|
238
|
+
say "Invalid username/password combination. Please try again:"
|
239
|
+
else
|
240
|
+
say "Unknown error (#{e.response.code}). Please try again: "
|
241
|
+
end
|
242
|
+
clear_github_auth_data!
|
243
|
+
retry
|
244
|
+
end
|
245
|
+
|
246
|
+
def github_api_token(host = nil, user = nil)
|
247
|
+
host ||= github_host
|
248
|
+
user ||= github_username
|
175
249
|
github_token = Git.get_config KEY_GITHUB_API_TOKEN, :inherited
|
176
250
|
if github_token.blank?
|
177
|
-
|
178
|
-
|
251
|
+
if block_given?
|
252
|
+
github_token = yield
|
253
|
+
end
|
254
|
+
Git.set_config(KEY_GITHUB_API_TOKEN, github_token, :global) unless github_token.blank?
|
179
255
|
end
|
180
256
|
github_token
|
181
257
|
end
|
182
258
|
|
183
259
|
# special prompt that has hidden input
|
184
|
-
def
|
185
|
-
|
260
|
+
def ask_github_password(username = nil)
|
261
|
+
username ||= github_username
|
262
|
+
print "Github password for #{username} (never stored): "
|
186
263
|
if $stdin.tty?
|
187
264
|
password = askpass
|
188
265
|
puts ''
|
@@ -247,18 +324,5 @@ module GithubPivotalFlow
|
|
247
324
|
URI.parse proxy
|
248
325
|
end
|
249
326
|
end
|
250
|
-
|
251
|
-
private
|
252
|
-
|
253
|
-
KEY_API_TOKEN = 'pivotal.api-token'.freeze
|
254
|
-
KEY_PROJECT_ID = 'pivotal.project-id'.freeze
|
255
|
-
KEY_STORY_ID = 'pivotal-story-id'.freeze
|
256
|
-
KEY_FEATURE_PREFIX = 'gitflow.prefix.feature'.freeze
|
257
|
-
KEY_HOTFIX_PREFIX = 'gitflow.prefix.hotfix'.freeze
|
258
|
-
KEY_RELEASE_PREFIX = 'gitflow.prefix.release'.freeze
|
259
|
-
KEY_DEVELOPMENT_BRANCH = 'gitflow.branch.develop'.freeze
|
260
|
-
KEY_MASTER_BRANCH = 'gitflow.branch.master'.freeze
|
261
|
-
KEY_GITHUB_USERNAME = 'github.username'.freeze
|
262
|
-
KEY_GITHUB_API_TOKEN = 'github.api-token'.freeze
|
263
327
|
end
|
264
328
|
end
|
@@ -45,6 +45,7 @@ module GithubPivotalFlow
|
|
45
45
|
def self.merge(branch_name, options = {})
|
46
46
|
command = "git merge --quiet"
|
47
47
|
command << " --no-ff" if options[:no_ff]
|
48
|
+
command << " --ff" if options[:ff] && !options[:no_ff]
|
48
49
|
command << " -m \"#{options[:commit_message]}\"" unless options[:commit_message].blank?
|
49
50
|
exec "#{command} #{branch_name}"
|
50
51
|
puts 'OK'
|
@@ -118,6 +119,19 @@ module GithubPivotalFlow
|
|
118
119
|
else
|
119
120
|
raise "Unable to set Git configuration for scope '#{scope}'"
|
120
121
|
end
|
122
|
+
return value
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.delete_config(key, scope = :local)
|
126
|
+
if :branch == scope
|
127
|
+
exec "git config --local --unset branch.#{self.current_branch}.#{key}"
|
128
|
+
elsif :global == scope
|
129
|
+
exec "git config --global --unset #{key}"
|
130
|
+
elsif :local == scope
|
131
|
+
exec "git config --local --unset #{key}"
|
132
|
+
else
|
133
|
+
raise "Unable to delete Git configuration for scope '#{scope}'"
|
134
|
+
end
|
121
135
|
end
|
122
136
|
|
123
137
|
def self.repository_root
|
@@ -149,6 +163,14 @@ module GithubPivotalFlow
|
|
149
163
|
end
|
150
164
|
end
|
151
165
|
|
166
|
+
def self.clean_working_tree?
|
167
|
+
exec("git diff --no-ext-diff --ignore-submodules --quiet --exit-code", false)
|
168
|
+
fail("fatal: Working tree contains unstaged changes. Aborting.") if $?.exitstatus != 0
|
169
|
+
exec("git diff-index --cached --quiet --ignore-submodules HEAD --", false)
|
170
|
+
fail("fatal: Index contains uncommited changes. Aborting.") if $?.exitstatus != 0
|
171
|
+
return true
|
172
|
+
end
|
173
|
+
|
152
174
|
private
|
153
175
|
def self.escape_commit_message(message)
|
154
176
|
message.gsub('"', '\"').sub(/!\z/, '! ')
|
@@ -34,6 +34,15 @@ module GithubPivotalFlow
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
def repo_info project
|
38
|
+
get "https://%s/repos/%s/%s" %
|
39
|
+
[api_host(project.host), project.owner, project.name]
|
40
|
+
end
|
41
|
+
|
42
|
+
def repo_exists? project
|
43
|
+
repo_info(project).success?
|
44
|
+
end
|
45
|
+
|
37
46
|
# Returns parsed data from the new pull request.
|
38
47
|
def create_pullrequest options
|
39
48
|
project = options.fetch(:project)
|
@@ -124,7 +133,7 @@ module GithubPivotalFlow
|
|
124
133
|
create_connection host_url
|
125
134
|
end
|
126
135
|
|
127
|
-
req['User-Agent'] = "
|
136
|
+
req['User-Agent'] = "Github-pivotal-flow #{GithubPivotalFlow::VERSION}"
|
128
137
|
apply_authentication(req, url)
|
129
138
|
yield req if block_given?
|
130
139
|
finalize_request(req, url)
|
@@ -204,7 +213,6 @@ module GithubPivotalFlow
|
|
204
213
|
|
205
214
|
def obtain_oauth_token host, user, two_factor_code = nil
|
206
215
|
auth_url = URI.parse("https://%s@%s/authorizations" % [CGI.escape(user), host])
|
207
|
-
|
208
216
|
# dummy request to trigger a 2FA SMS since a HTTP GET won't do it
|
209
217
|
post(auth_url) if !two_factor_code
|
210
218
|
|
@@ -1,21 +1,29 @@
|
|
1
1
|
module GithubPivotalFlow
|
2
|
-
class Project
|
3
|
-
|
2
|
+
class Project
|
3
|
+
attr_accessor :owner, :name, :host, :config
|
4
|
+
|
5
|
+
def self.find(id)
|
6
|
+
id = id.to_i if id.is_a?(String)
|
7
|
+
return PivotalTracker::Project.find(id)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(args = {})
|
11
|
+
args.each do |k,v|
|
12
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
13
|
+
end
|
14
|
+
url = Git.get_config("remote.#{Git.get_remote}.url")
|
4
15
|
if (matchdata = /^git@([a-z0-9\._-]+):([a-z0-9_-]+)\/([a-z0-9_-]+)(\.git)?$/.match(url.strip))
|
5
|
-
host
|
6
|
-
owner
|
7
|
-
name
|
16
|
+
self.host ||= matchdata[1]
|
17
|
+
self.owner ||= matchdata[2]
|
18
|
+
self.name ||= matchdata[3]
|
8
19
|
else
|
9
20
|
url = URI(url) if !url.is_a?(URI)
|
10
|
-
|
11
|
-
|
21
|
+
path_components = url.path.split('/', 4)
|
22
|
+
self.owner ||= path_components[1]
|
23
|
+
self.name ||= path_components[2]
|
24
|
+
self.host ||= url.host
|
12
25
|
end
|
13
|
-
self.
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(*args)
|
17
|
-
super
|
18
|
-
self.name = self.name.tr(' ', '-')
|
26
|
+
self.name = self.name.tr(' ', '-').sub(/\.git$/, '')
|
19
27
|
self.host ||= 'github.com'
|
20
28
|
self.host = host.sub(/^ssh\./i, '') if 'ssh.github.com' == host.downcase
|
21
29
|
end
|
@@ -27,5 +35,13 @@ module GithubPivotalFlow
|
|
27
35
|
def ==(other)
|
28
36
|
name_with_owner == other.name_with_owner
|
29
37
|
end
|
38
|
+
|
39
|
+
def pivotal_project
|
40
|
+
@pivotal_project ||= self.class.find(self.config.project_id)
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_missing(m, *args, &block)
|
44
|
+
return pivotal_project.send(m, *args, &block)
|
45
|
+
end
|
30
46
|
end
|
31
47
|
end
|
@@ -11,8 +11,10 @@ module GithubPivotalFlow
|
|
11
11
|
@configuration.story = story # Tag the branch with story attributes
|
12
12
|
Git.add_hook 'prepare-commit-msg', File.join(File.dirname(__FILE__), 'prepare-commit-msg.sh')
|
13
13
|
unless story.release?
|
14
|
-
|
15
|
-
|
14
|
+
print "Creating pull-request on Github... "
|
15
|
+
pull_request_params = story.params_for_pull_request.merge(project: @configuration.project)
|
16
|
+
@configuration.github_client.create_pullrequest(pull_request_params)
|
17
|
+
puts 'OK'
|
16
18
|
end
|
17
19
|
story.mark_started!
|
18
20
|
return 0
|
@@ -20,12 +22,6 @@ module GithubPivotalFlow
|
|
20
22
|
|
21
23
|
private
|
22
24
|
|
23
|
-
def create_pull_request_for_story!(story)
|
24
|
-
print "Creating pull-request on Github... "
|
25
|
-
@ghclient.create_pullrequest({:project => @configuration.github_project}.merge(story.params_for_pull_request))
|
26
|
-
puts 'OK'
|
27
|
-
end
|
28
|
-
|
29
25
|
def parse_argv(*args)
|
30
26
|
OptionParser.new do |opts|
|
31
27
|
opts.banner = "Usage: git start <feature|chore|bug|story_id>"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Utilities for dealing with +PivotalTracker::Story+s
|
2
2
|
module GithubPivotalFlow
|
3
3
|
class Story
|
4
|
-
attr_accessor :
|
4
|
+
attr_accessor :pivotal_story, :project, :branch_name, :root_branch_name
|
5
5
|
|
6
6
|
# Print a human readable version of a story. This pretty prints the title,
|
7
7
|
# description, and notes for the story.
|
@@ -28,7 +28,7 @@ module GithubPivotalFlow
|
|
28
28
|
|
29
29
|
# Selects a Pivotal Tracker story by doing the following steps:
|
30
30
|
#
|
31
|
-
# @param [
|
31
|
+
# @param [Project] project the project to select stories from
|
32
32
|
# @param [String, nil] filter a filter for selecting the story to start. This
|
33
33
|
# filter can be either:
|
34
34
|
# * a story id: selects the story represented by the id
|
@@ -42,20 +42,22 @@ module GithubPivotalFlow
|
|
42
42
|
else
|
43
43
|
story = find_story project, filter, limit
|
44
44
|
end
|
45
|
-
self.new(story)
|
45
|
+
self.new(project, story)
|
46
46
|
end
|
47
47
|
|
48
|
-
# @param [
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
# @param [Project] project the Project for this repo
|
49
|
+
# @param [PivotalTracker::Story] pivotal_story the Pivotal tracker story to wrap
|
50
|
+
def initialize(project, pivotal_story, options = {})
|
51
|
+
raise "Invalid PivotalTracker::Story" if pivotal_story.nil?
|
52
|
+
@project = project
|
53
|
+
@pivotal_story = pivotal_story
|
52
54
|
@branch_name = options.delete(:branch_name)
|
53
55
|
@branch_suffix = @branch_name.split('-').last if @branch_name
|
54
56
|
@branch_suffix ||= nil
|
55
57
|
end
|
56
58
|
|
57
59
|
def release?
|
58
|
-
|
60
|
+
story_type == 'release'
|
59
61
|
end
|
60
62
|
|
61
63
|
def unestimated?
|
@@ -63,22 +65,22 @@ module GithubPivotalFlow
|
|
63
65
|
end
|
64
66
|
|
65
67
|
def request_estimation!
|
66
|
-
self.
|
67
|
-
:
|
68
|
+
self.update(
|
69
|
+
estimate: ask('Story is not yet estimated. Please estimate difficulty: ')
|
68
70
|
)
|
69
71
|
end
|
70
72
|
|
71
73
|
def mark_started!
|
72
74
|
print 'Starting story on Pivotal Tracker... '
|
73
|
-
self.
|
74
|
-
:
|
75
|
-
:
|
75
|
+
self.update(
|
76
|
+
current_state: 'started',
|
77
|
+
owned_by: Git.get_config(KEY_USER_NAME, :inherited).strip
|
76
78
|
)
|
77
79
|
puts 'OK'
|
78
80
|
end
|
79
81
|
|
80
82
|
def create_branch!(commit_message = nil, options = {})
|
81
|
-
commit_message ||= "Starting [#{
|
83
|
+
commit_message ||= "Starting [#{story_type} ##{id}]: #{name}"
|
82
84
|
commit_message << " [ci skip]" unless options[:run_ci]
|
83
85
|
print "Creating branch for story with branch name #{branch_name} from #{root_branch_name}... "
|
84
86
|
Git.checkout(root_branch_name)
|
@@ -94,32 +96,44 @@ module GithubPivotalFlow
|
|
94
96
|
|
95
97
|
def merge_to_root!(commit_message = nil, options = {})
|
96
98
|
commit_message ||= "Merge #{branch_name} to #{root_branch_name}"
|
97
|
-
commit_message << "\n\n[#{options[:no_complete] ? '' : 'Completes '}##{
|
99
|
+
commit_message << "\n\n[#{options[:no_complete] ? '' : 'Completes '}##{id}] "
|
98
100
|
print "Merging #{branch_name} to #{root_branch_name}... "
|
99
101
|
Git.checkout(root_branch_name)
|
100
102
|
Git.pull_remote(root_branch_name)
|
101
|
-
|
102
|
-
|
103
|
+
if trivial_merge?
|
104
|
+
Git.merge(branch_name, commit_message: commit_message, ff: true)
|
105
|
+
else
|
106
|
+
Git.merge(branch_name, commit_message: commit_message, no_ff: true)
|
107
|
+
end
|
103
108
|
Git.push(root_branch_name)
|
109
|
+
self.delete_branch!
|
104
110
|
self.cleanup!
|
105
111
|
end
|
106
112
|
|
107
113
|
def merge_release!(commit_message = nil, options = {})
|
108
|
-
commit_message ||= "Release #{
|
109
|
-
commit_message << "\n\n[#{options[:no_complete] ? '' : 'Completes '}##{
|
114
|
+
commit_message ||= "Release #{name}"
|
115
|
+
commit_message << "\n\n[#{options[:no_complete] ? '' : 'Completes '}##{id}] "
|
110
116
|
print "Merging #{branch_name} to #{master_branch_name}... "
|
111
117
|
Git.checkout(master_branch_name)
|
112
118
|
Git.pull_remote(master_branch_name)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
Git.
|
119
|
+
if trivial_merge?(master_branch_name)
|
120
|
+
Git.merge(branch_name, commit_message: commit_message, ff: true)
|
121
|
+
else
|
122
|
+
Git.merge(branch_name, commit_message: commit_message, no_ff: true)
|
123
|
+
end
|
124
|
+
Git.tag(name)
|
125
|
+
print "Merging #{branch_name} to #{development_branch_name}... "
|
126
|
+
Git checkout(development_branch_name)
|
127
|
+
Git.pull_remote(development_branch_name)
|
128
|
+
if trivial_merge?(development_branch_name)
|
129
|
+
Git.merge(branch_name, commit_message: commit_message, ff: true)
|
130
|
+
else
|
131
|
+
Git.merge(branch_name, commit_message: commit_message, no_ff: true)
|
132
|
+
end
|
119
133
|
Git.checkout(master_branch_name)
|
120
|
-
|
121
|
-
Git.push(master_branch_name, root_branch_name)
|
134
|
+
Git.push(master_branch_name, development_branch_name)
|
122
135
|
Git.push_tags
|
136
|
+
self.delete_branch!
|
123
137
|
self.cleanup!
|
124
138
|
end
|
125
139
|
|
@@ -138,17 +152,17 @@ module GithubPivotalFlow
|
|
138
152
|
#end
|
139
153
|
|
140
154
|
def branch_name
|
141
|
-
@branch_name ||= branch_name_from(branch_prefix,
|
155
|
+
@branch_name ||= branch_name_from(branch_prefix, id, branch_suffix)
|
142
156
|
end
|
143
157
|
|
144
158
|
def branch_suffix
|
145
|
-
@branch_suffix ||= ask("Enter branch name (#{branch_name_from(branch_prefix,
|
159
|
+
@branch_suffix ||= ask("Enter branch name (#{branch_name_from(branch_prefix, id, "<branch-name>")}): ")
|
146
160
|
end
|
147
161
|
|
148
162
|
def branch_name_from(branch_prefix, story_id, branch_name)
|
149
163
|
if story_type == 'release'
|
150
164
|
# For release branches the format is release/5.0
|
151
|
-
"#{Git.get_config(
|
165
|
+
"#{Git.get_config(KEY_RELEASE_PREFIX, :inherited)}#{branch_name}"
|
152
166
|
else
|
153
167
|
n = "#{branch_prefix}#{story_id}"
|
154
168
|
n << "-#{branch_name}" unless branch_name.blank?
|
@@ -168,42 +182,43 @@ module GithubPivotalFlow
|
|
168
182
|
end
|
169
183
|
|
170
184
|
def master_branch_name
|
171
|
-
Git.get_config(
|
185
|
+
Git.get_config(KEY_MASTER_BRANCH, :inherited)
|
172
186
|
end
|
173
187
|
|
174
188
|
def development_branch_name
|
175
|
-
Git.get_config(
|
189
|
+
Git.get_config(KEY_DEVELOPMENT_BRANCH, :inherited)
|
176
190
|
end
|
177
191
|
|
178
192
|
def labels
|
179
|
-
return [] if
|
180
|
-
|
193
|
+
return [] if pivotal_story.labels.blank?
|
194
|
+
pivotal_story.labels.split(',').collect(&:strip)
|
181
195
|
end
|
182
196
|
|
183
197
|
def params_for_pull_request
|
184
198
|
{
|
185
|
-
:
|
186
|
-
:
|
187
|
-
:
|
188
|
-
:
|
199
|
+
base: root_branch_name,
|
200
|
+
head: branch_name,
|
201
|
+
title: name,
|
202
|
+
body: description,
|
189
203
|
}
|
190
204
|
end
|
191
205
|
|
192
206
|
def method_missing(m, *args, &block)
|
193
|
-
return @
|
207
|
+
return @pivotal_story.send(m, *args, &block)
|
194
208
|
end
|
195
209
|
|
196
210
|
def can_merge?
|
197
|
-
|
198
|
-
|
199
|
-
root_tip = Shell.exec "git rev-parse #{root_branch_name}"
|
200
|
-
common_ancestor = Shell.exec "git merge-base #{root_branch_name} #{branch_name}"
|
211
|
+
Git.clean_working_tree?
|
212
|
+
end
|
201
213
|
|
214
|
+
def trivial_merge?(to_branch = nil)
|
215
|
+
to_branch ||= root_branch_name
|
216
|
+
root_tip = Shell.exec "git rev-parse #{to_branch}"
|
217
|
+
common_ancestor = Shell.exec "git merge-base #{to_branch} #{branch_name}"
|
202
218
|
if root_tip != common_ancestor
|
203
|
-
|
219
|
+
return false
|
204
220
|
end
|
205
|
-
|
206
|
-
puts 'OK'
|
221
|
+
return true
|
207
222
|
end
|
208
223
|
|
209
224
|
private
|
@@ -260,16 +275,18 @@ module GithubPivotalFlow
|
|
260
275
|
end
|
261
276
|
|
262
277
|
def branch_prefix
|
263
|
-
case
|
278
|
+
case story_type
|
264
279
|
when 'feature'
|
265
|
-
Git.get_config(
|
280
|
+
prefix = Git.get_config(KEY_FEATURE_PREFIX, :inherited)
|
266
281
|
when 'bug'
|
267
|
-
|
282
|
+
prefix = labels.include?('hotfix') ? Git.get_config(KEY_HOTFIX_PREFIX, :inherited) : Git.get_config(KEY_FEATURE_PREFIX, :inherited)
|
268
283
|
when 'release'
|
269
|
-
Git.get_config(
|
284
|
+
prefix = Git.get_config(KEY_RELEASE_PREFIX, :inherited)
|
270
285
|
else
|
271
|
-
'misc/'
|
286
|
+
prefix = 'misc/'
|
272
287
|
end
|
288
|
+
prefix = "#{prefix.strip}/" unless prefix.strip[-1,1] == '/'
|
289
|
+
return prefix.strip
|
273
290
|
end
|
274
291
|
end
|
275
292
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module GithubPivotalFlow
|
4
|
+
describe Command do
|
5
|
+
before do
|
6
|
+
$stdout = StringIO.new
|
7
|
+
$stderr = StringIO.new
|
8
|
+
@configuration = double('configuration')
|
9
|
+
@project = double('project')
|
10
|
+
allow(Configuration).to receive(:new).and_return(@configuration)
|
11
|
+
allow(PivotalTracker::Project).to receive(:find).and_return(@project)
|
12
|
+
@configuration.stub(
|
13
|
+
validate: true,
|
14
|
+
project_id: 123456,
|
15
|
+
project: @project,
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#initialize' do
|
20
|
+
it 'validates the configuration when started' do
|
21
|
+
expect(@configuration).to receive(:validate).once
|
22
|
+
@start = Command.new
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'finds the project corresponding to the current repo' do
|
26
|
+
expect(@configuration).to receive(:project).once.and_return(@project)
|
27
|
+
@start = Command.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -7,105 +7,116 @@ module GithubPivotalFlow
|
|
7
7
|
$stdout = StringIO.new
|
8
8
|
$stderr = StringIO.new
|
9
9
|
@configuration = Configuration.new
|
10
|
+
@project = double('project')
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
-
Git.should_receive(:get_config).with('pivotal.api-token', :inherited).and_return('test_api_token')
|
13
|
+
describe '#validate' do
|
14
14
|
|
15
|
-
api_token = @configuration.api_token
|
16
|
-
|
17
|
-
expect(api_token).to eq('test_api_token')
|
18
15
|
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
Git.should_receive(:set_config).with('pivotal.api-token', 'test_api_token', :global)
|
24
|
-
api_token = @configuration.api_token
|
25
|
-
expect(api_token).to eq('test_api_token')
|
26
|
-
end
|
17
|
+
describe '#api_token' do
|
18
|
+
it 'does not prompt the user for the API token if it is already configured' do
|
19
|
+
Git.should_receive(:get_config).with('pivotal.api-token', :inherited).and_return('test_api_token')
|
27
20
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
21
|
+
api_token = @configuration.api_token
|
22
|
+
|
23
|
+
expect(api_token).to eq('test_api_token')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'prompts the user for the API token if it is not configured and stores it in the local git config' do
|
27
|
+
Git.should_receive(:get_config).with('pivotal.api-token', :inherited).and_return('')
|
28
|
+
@configuration.should_receive(:ask).and_return('test_api_token')
|
29
|
+
Git.should_receive(:set_config).with('pivotal.api-token', 'test_api_token', :local)
|
30
|
+
api_token = @configuration.api_token
|
31
|
+
expect(api_token).to eq('test_api_token')
|
32
|
+
end
|
32
33
|
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
35
|
+
describe '#project_id' do
|
36
|
+
it 'does not prompt the user for the project id if it is already configured' do
|
37
|
+
Git.should_receive(:get_config).with('pivotal.project-id', :inherited).and_return('test_project_id')
|
38
|
+
project_id = @configuration.project_id
|
39
|
+
expect(project_id).to eq('test_project_id')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'prompts the user for the project id if it is not configured' do
|
43
|
+
Git.should_receive(:get_config).with('pivotal.project-id', :inherited).and_return('')
|
44
|
+
menu = double('menu')
|
45
|
+
menu.should_receive(:prompt=)
|
46
|
+
PivotalTracker::Project.should_receive(:all).and_return([
|
47
|
+
PivotalTracker::Project.new(:id => 'id-2', :name => 'name-2'),
|
48
|
+
PivotalTracker::Project.new(:id => 'id-1', :name => 'name-1')])
|
49
|
+
menu.should_receive(:choice).with('name-1')
|
50
|
+
menu.should_receive(:choice).with('name-2')
|
51
|
+
@configuration.should_receive(:choose) { |&arg| arg.call menu }.and_return('test_project_id')
|
52
|
+
Git.should_receive(:set_config).with('pivotal.project-id', 'test_project_id', :local)
|
53
|
+
|
54
|
+
project_id = @configuration.project_id
|
55
|
+
|
56
|
+
expect(project_id).to eq('test_project_id')
|
57
|
+
end
|
49
58
|
end
|
50
59
|
|
51
|
-
|
52
|
-
|
60
|
+
describe '#story=' do
|
61
|
+
it 'persists the story when requested' do
|
62
|
+
expect(Git).to receive(:set_config).with('pivotal-story-id', 12345678, :branch)
|
53
63
|
|
54
|
-
|
64
|
+
@configuration.story = Story.new(@project, PivotalTracker::Story.new(:id => 12345678))
|
65
|
+
end
|
55
66
|
end
|
56
67
|
|
57
|
-
describe 'story' do
|
68
|
+
describe '#story' do
|
69
|
+
let(:project) { double('project') }
|
70
|
+
let(:stories) { double('stories') }
|
71
|
+
let(:pivotal_story) { double('pivotal_story') }
|
72
|
+
|
73
|
+
before do
|
74
|
+
allow(@configuration).to receive(:project).and_return(project)
|
75
|
+
end
|
76
|
+
|
58
77
|
it 'fetches the story based on the story id stored inside the git config' do
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
Git.should_receive(:get_config).with('pivotal-story-id', :branch).and_return('12345678')
|
63
|
-
project.should_receive(:stories).and_return(stories)
|
64
|
-
stories.should_receive(:find).with(12345678).and_return(story)
|
78
|
+
expect(Git).to receive(:get_config).with('pivotal-story-id', :branch).and_return('12345678')
|
79
|
+
expect(project).to receive(:stories).and_return(stories)
|
80
|
+
expect(stories).to receive(:find).with(12345678).and_return(pivotal_story)
|
65
81
|
|
66
|
-
result = @configuration.story
|
82
|
+
result = @configuration.story
|
83
|
+
expect(result).to be_a(Story)
|
67
84
|
|
68
|
-
expect(result.
|
85
|
+
expect(result.pivotal_story).to eq(pivotal_story)
|
69
86
|
end
|
70
87
|
|
71
88
|
it 'uses the branch name to deduce the story id if no git config is found' do
|
72
|
-
project = double('project')
|
73
|
-
stories = double('stories')
|
74
|
-
story = double('story')
|
75
89
|
Git.stub(:current_branch).and_return('feature/12345678-sample_feature')
|
76
|
-
Git.
|
77
|
-
project.
|
78
|
-
stories.
|
90
|
+
expect(Git).to receive(:get_config).with('pivotal-story-id', :branch).and_return(' ')
|
91
|
+
expect(project).to receive(:stories).and_return(stories)
|
92
|
+
expect(stories).to receive(:find).with(12345678).and_return(pivotal_story)
|
79
93
|
|
80
|
-
result = @configuration.story
|
94
|
+
result = @configuration.story
|
81
95
|
|
82
|
-
expect(result.
|
96
|
+
expect(result.pivotal_story).to eq(pivotal_story)
|
83
97
|
end
|
84
98
|
|
85
99
|
it 'prompts for the story id if the branch name does not match the known format' do
|
86
|
-
project = double('project')
|
87
|
-
stories = double('stories')
|
88
|
-
story = double('story')
|
89
100
|
Git.stub(:current_branch).and_return('unknownformat')
|
90
|
-
Git.
|
91
|
-
@configuration.
|
92
|
-
Git.
|
93
|
-
project.
|
94
|
-
stories.
|
101
|
+
expect(Git).to receive(:get_config).with('pivotal-story-id', :branch).and_return(' ')
|
102
|
+
expect(@configuration).to receive(:ask).and_return('12345678')
|
103
|
+
expect(Git).to receive(:set_config).with('pivotal-story-id', '12345678', :branch)
|
104
|
+
expect(project).to receive(:stories).and_return(stories)
|
105
|
+
expect(stories).to receive(:find).with(12345678).and_return(pivotal_story)
|
95
106
|
|
96
|
-
result = @configuration.story
|
107
|
+
result = @configuration.story
|
97
108
|
|
98
|
-
expect(result.
|
109
|
+
expect(result.pivotal_story).to eq(pivotal_story)
|
99
110
|
end
|
100
111
|
end
|
101
112
|
|
102
|
-
describe '
|
113
|
+
describe '#project' do
|
103
114
|
it 'supports working with git urls from the configuration' do
|
104
115
|
Git.should_receive(:get_remote).and_return('origin')
|
105
116
|
Git.should_receive(:get_config).with('remote.origin.url').and_return('git@github.com:roomorama/github-pivotal-flow.git')
|
106
|
-
|
107
|
-
expect(
|
108
|
-
expect(
|
117
|
+
project = @configuration.project
|
118
|
+
expect(project.owner).to eq('roomorama')
|
119
|
+
expect(project.name).to eq('github-pivotal-flow')
|
109
120
|
end
|
110
121
|
end
|
111
122
|
end
|
@@ -18,11 +18,15 @@ module GithubPivotalFlow
|
|
18
18
|
release_prefix: 'release/',
|
19
19
|
api_token: 'token',
|
20
20
|
project_id: '123',
|
21
|
-
|
22
|
-
|
21
|
+
project: @project,
|
22
|
+
github_client: @ghclient,
|
23
|
+
story: @story,
|
24
|
+
validate: true,
|
25
|
+
)
|
23
26
|
allow(Configuration).to receive(:new).and_return(@configuration)
|
24
|
-
allow(
|
27
|
+
allow(Project).to receive(:find).and_return(@project)
|
25
28
|
@finish = Finish.new
|
29
|
+
expect(@configuration).to receive(:story).and_return(@story)
|
26
30
|
end
|
27
31
|
|
28
32
|
it 'merges the branch back to its root by default' do
|
@@ -112,7 +112,7 @@ module GithubPivotalFlow
|
|
112
112
|
expect(File).to receive(:exist?).with(hook).and_return(true)
|
113
113
|
|
114
114
|
Git.add_hook 'prepare-commit-msg', __FILE__
|
115
|
-
|
115
|
+
allow(File).to receive(:exist?).and_call_original
|
116
116
|
expect(File.exist?(hook)).to be_false
|
117
117
|
end
|
118
118
|
end
|
@@ -5,9 +5,11 @@ module GithubPivotalFlow
|
|
5
5
|
before do
|
6
6
|
$stdout = StringIO.new
|
7
7
|
$stderr = StringIO.new
|
8
|
-
|
9
|
-
@project = double('project')
|
10
8
|
@story = double('story')
|
9
|
+
@project = double('project')
|
10
|
+
@project.stub(
|
11
|
+
stories: [@story]
|
12
|
+
)
|
11
13
|
@ghclient = double('ghclient')
|
12
14
|
@ghproject = double('ghproject')
|
13
15
|
@configuration = double('configuration')
|
@@ -19,16 +21,17 @@ module GithubPivotalFlow
|
|
19
21
|
release_prefix: 'release/',
|
20
22
|
api_token: 'token',
|
21
23
|
project_id: '123',
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
project: @project,
|
25
|
+
github_client: @ghclient,
|
26
|
+
story: @story,
|
27
|
+
validate: true,
|
28
|
+
)
|
26
29
|
allow(Configuration).to receive(:new).and_return(@configuration)
|
27
30
|
allow(PivotalTracker::Project).to receive(:find).and_return(@project)
|
28
31
|
@start = Start.new()
|
29
32
|
end
|
30
33
|
|
31
|
-
it 'should
|
34
|
+
it 'should runs correctly' do
|
32
35
|
@start.options[:args] = 'test_filter'
|
33
36
|
@story.stub(:unestimated? => false, :release? => false, params_for_pull_request: {})
|
34
37
|
|
@@ -10,7 +10,10 @@ module GithubPivotalFlow
|
|
10
10
|
@project = double('project')
|
11
11
|
@stories = double('stories')
|
12
12
|
@story = double('story')
|
13
|
+
@pivotal_story = double('pivotal_story')
|
13
14
|
@menu = double('menu')
|
15
|
+
allow(@project).to receive(:stories).and_return(@stories)
|
16
|
+
allow(@story).to receive(:pivotal_story).and_return(@pivotal_story)
|
14
17
|
end
|
15
18
|
|
16
19
|
describe '.pretty_print' do
|
@@ -59,32 +62,28 @@ module GithubPivotalFlow
|
|
59
62
|
|
60
63
|
describe '.select_story' do
|
61
64
|
it 'selects a story directly if the filter is a number' do
|
62
|
-
@
|
63
|
-
@stories.should_receive(:find).with(12345678).and_return(@story)
|
64
|
-
|
65
|
+
expect(@stories).to receive(:find).with(12345678).and_return(@pivotal_story)
|
65
66
|
story = Story.select_story @project, '12345678'
|
66
67
|
|
67
68
|
expect(story).to be_a(Story)
|
68
|
-
expect(story.
|
69
|
+
expect(story.pivotal_story).to eq(@pivotal_story)
|
69
70
|
end
|
70
71
|
|
71
72
|
it 'selects a story if the result of the query is a single story' do
|
72
|
-
@
|
73
|
-
@stories.should_receive(:all).with(
|
73
|
+
expect(@stories).to receive(:all).with(
|
74
74
|
:current_state => %w(rejected unstarted unscheduled),
|
75
75
|
:limit => 1,
|
76
76
|
:story_type => 'release'
|
77
|
-
).and_return([@
|
77
|
+
).and_return([@pivotal_story])
|
78
78
|
|
79
79
|
story = Story.select_story @project, 'release', 1
|
80
80
|
|
81
81
|
expect(story).to be_a(Story)
|
82
|
-
expect(story.
|
82
|
+
expect(story.pivotal_story).to eq(@pivotal_story)
|
83
83
|
end
|
84
84
|
|
85
85
|
it 'prompts the user for a story if the result of the query is more than a single story' do
|
86
|
-
@
|
87
|
-
@stories.should_receive(:all).with(
|
86
|
+
expect(@stories).to receive(:all).with(
|
88
87
|
:current_state => %w(rejected unstarted unscheduled),
|
89
88
|
:limit => 5,
|
90
89
|
:story_type => 'feature'
|
@@ -92,66 +91,77 @@ module GithubPivotalFlow
|
|
92
91
|
PivotalTracker::Story.new(:name => 'name-1'),
|
93
92
|
PivotalTracker::Story.new(:name => 'name-2')
|
94
93
|
])
|
95
|
-
@menu.
|
96
|
-
@menu.
|
97
|
-
@menu.
|
98
|
-
Story.
|
94
|
+
expect(@menu).to receive(:prompt=)
|
95
|
+
expect(@menu).to receive(:choice).with('name-1')
|
96
|
+
expect(@menu).to receive(:choice).with('name-2')
|
97
|
+
expect(Story).to receive(:choose) { |&arg| arg.call @menu }.and_return(@pivotal_story)
|
99
98
|
|
100
99
|
story = Story.select_story @project, 'feature'
|
101
100
|
|
102
101
|
expect(story).to be_a(Story)
|
103
|
-
expect(story.
|
102
|
+
expect(story.pivotal_story).to eq(@pivotal_story)
|
104
103
|
end
|
105
104
|
|
106
105
|
it 'prompts the user with the story type if no filter is specified' do
|
107
|
-
@
|
108
|
-
@stories.should_receive(:all).with(
|
106
|
+
expect(@stories).to receive(:all).with(
|
109
107
|
:current_state => %w(rejected unstarted unscheduled),
|
110
108
|
:limit => 5
|
111
109
|
).and_return([
|
112
110
|
PivotalTracker::Story.new(:story_type => 'chore', :name => 'name-1'),
|
113
111
|
PivotalTracker::Story.new(:story_type => 'bug', :name => 'name-2')
|
114
112
|
])
|
115
|
-
@menu.
|
116
|
-
@menu.
|
117
|
-
@menu.
|
118
|
-
Story.
|
113
|
+
expect(@menu).to receive(:prompt=)
|
114
|
+
expect(@menu).to receive(:choice).with('CHORE name-1')
|
115
|
+
expect(@menu).to receive(:choice).with('BUG name-2')
|
116
|
+
expect(Story).to receive(:choose) { |&arg| arg.call @menu }.and_return(@pivotal_story)
|
119
117
|
|
120
118
|
story = Story.select_story @project
|
121
119
|
|
122
120
|
expect(story).to be_a(Story)
|
123
|
-
expect(story.
|
121
|
+
expect(story.pivotal_story).to eq(@pivotal_story)
|
124
122
|
end
|
125
123
|
end
|
126
124
|
|
127
125
|
describe '#create_branch!' do
|
128
126
|
before do
|
129
|
-
|
130
|
-
|
131
|
-
|
127
|
+
Git.stub(
|
128
|
+
checkout: nil,
|
129
|
+
pull_remote: nil,
|
130
|
+
create_branch: nil,
|
131
|
+
set_config: nil,
|
132
|
+
get_config: nil,
|
133
|
+
push: nil,
|
134
|
+
commit: nil,
|
135
|
+
get_remote: 'origin',
|
136
|
+
)
|
137
|
+
@pivotal_story.stub(
|
138
|
+
story_type: 'feature',
|
139
|
+
id: '123456',
|
140
|
+
name: 'test',
|
141
|
+
description: 'description')
|
142
|
+
@story = GithubPivotalFlow::Story.new(@project, @pivotal_story)
|
143
|
+
allow(@story).to receive(:ask).and_return('test')
|
132
144
|
end
|
133
145
|
|
134
146
|
it 'prompts the user for a branch extension name' do
|
135
|
-
|
136
|
-
expect(@
|
147
|
+
allow(@story).to receive(:branch_prefix).and_return('feature/')
|
148
|
+
expect(@story).to receive(:ask).with("Enter branch name (feature/123456-<branch-name>): ").and_return('super-branch')
|
137
149
|
|
138
|
-
@
|
150
|
+
@story.create_branch!('Message')
|
139
151
|
end
|
140
152
|
|
141
153
|
it 'includes a tag to skip the ci build for the initial blank commit' do
|
142
154
|
@story.stub(branch_name: 'feature/123456-my_branch')
|
143
|
-
Git.
|
144
|
-
Git.should_receive(:commit).with(hash_including(commit_message: 'Message [ci skip]')).and_return(true)
|
155
|
+
expect(Git).to receive(:commit).with(hash_including(commit_message: 'Message [ci skip]')).and_return(true)
|
145
156
|
|
146
|
-
@
|
157
|
+
@story.create_branch!('Message')
|
147
158
|
end
|
148
159
|
|
149
160
|
it 'pushes the local branch and sets the upstream using the -u flag' do
|
150
161
|
@story.stub(branch_name: 'feature/123456-my_branch')
|
151
|
-
Git.
|
152
|
-
Git.should_receive(:push).with(instance_of(String), hash_including(set_upstream: true))
|
162
|
+
expect(Git).to receive(:push).with(instance_of(String), hash_including(set_upstream: true))
|
153
163
|
|
154
|
-
@
|
164
|
+
@story.create_branch!('Message')
|
155
165
|
end
|
156
166
|
end
|
157
167
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: github-pivotal-flow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Donald Piret
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -108,20 +108,6 @@ dependencies:
|
|
108
108
|
- - ~>
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '2.14'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: debugger
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ~>
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '1.6'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ~>
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '1.6'
|
125
111
|
- !ruby/object:Gem::Dependency
|
126
112
|
name: simplecov
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -161,20 +147,22 @@ extra_rdoc_files: []
|
|
161
147
|
files:
|
162
148
|
- LICENSE
|
163
149
|
- README.md
|
150
|
+
- bin/git-finish
|
151
|
+
- bin/git-start
|
164
152
|
- lib/core_ext/object/blank.rb
|
153
|
+
- lib/github_pivotal_flow.rb
|
165
154
|
- lib/github_pivotal_flow/command.rb
|
166
155
|
- lib/github_pivotal_flow/configuration.rb
|
167
156
|
- lib/github_pivotal_flow/finish.rb
|
168
157
|
- lib/github_pivotal_flow/git.rb
|
169
158
|
- lib/github_pivotal_flow/github_api.rb
|
159
|
+
- lib/github_pivotal_flow/prepare-commit-msg.sh
|
170
160
|
- lib/github_pivotal_flow/project.rb
|
171
161
|
- lib/github_pivotal_flow/shell.rb
|
172
162
|
- lib/github_pivotal_flow/start.rb
|
173
163
|
- lib/github_pivotal_flow/story.rb
|
174
|
-
- lib/github_pivotal_flow.rb
|
175
|
-
-
|
176
|
-
- bin/git-finish
|
177
|
-
- bin/git-start
|
164
|
+
- lib/github_pivotal_flow/version.rb
|
165
|
+
- spec/github_pivotal_flow/command_spec.rb
|
178
166
|
- spec/github_pivotal_flow/configuration_spec.rb
|
179
167
|
- spec/github_pivotal_flow/finish_spec.rb
|
180
168
|
- spec/github_pivotal_flow/git_spec.rb
|
@@ -201,11 +189,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
201
189
|
version: '0'
|
202
190
|
requirements: []
|
203
191
|
rubyforge_project:
|
204
|
-
rubygems_version: 2.1
|
192
|
+
rubygems_version: 2.2.1
|
205
193
|
signing_key:
|
206
194
|
specification_version: 4
|
207
195
|
summary: Git commands for integration with Pivotal Tracker and Github pull requests
|
208
196
|
test_files:
|
197
|
+
- spec/github_pivotal_flow/command_spec.rb
|
209
198
|
- spec/github_pivotal_flow/configuration_spec.rb
|
210
199
|
- spec/github_pivotal_flow/finish_spec.rb
|
211
200
|
- spec/github_pivotal_flow/git_spec.rb
|