story_branch 0.6.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +24 -7
- data/.github/weekly-digest.yml +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +42 -0
- data/.ruby-version +1 -1
- data/Gemfile.lock +65 -37
- data/README.md +14 -8
- data/docs/index.md +168 -0
- data/lib/story_branch/cli.rb +20 -20
- data/lib/story_branch/commands/configure.rb +84 -0
- data/lib/story_branch/commands/open_issue.rb +21 -0
- data/lib/story_branch/config_manager.rb +157 -8
- data/lib/story_branch/git_utils.rb +2 -19
- data/lib/story_branch/git_wrapper.rb +2 -2
- data/lib/story_branch/github/issue.rb +3 -4
- data/lib/story_branch/github/label.rb +1 -0
- data/lib/story_branch/github/tracker.rb +6 -10
- data/lib/story_branch/jira/issue.rb +8 -1
- data/lib/story_branch/jira/project.rb +15 -7
- data/lib/story_branch/jira/tracker.rb +11 -11
- data/lib/story_branch/main.rb +35 -105
- data/lib/story_branch/pivotal/tracker.rb +5 -9
- data/lib/story_branch/string_utils.rb +1 -1
- data/lib/story_branch/templates/open_issue/.gitkeep +1 -0
- data/lib/story_branch/tracker_base.rb +56 -0
- data/lib/story_branch/url_opener.rb +17 -0
- data/lib/story_branch/version.rb +1 -1
- data/story_branch.gemspec +9 -6
- data/tools/prep_changelog.rb +48 -0
- metadata +70 -16
- data/lib/story_branch/commands/add.rb +0 -94
- data/lib/story_branch/commands/migrate.rb +0 -103
@@ -4,7 +4,7 @@ module StoryBranch
|
|
4
4
|
module Jira
|
5
5
|
# Jira Issue representation
|
6
6
|
class Issue
|
7
|
-
attr_accessor :title, :id
|
7
|
+
attr_accessor :title, :id, :html_url
|
8
8
|
|
9
9
|
# TODO: Add component and labels to the info of the issue
|
10
10
|
def initialize(jira_issue, project)
|
@@ -12,6 +12,7 @@ module StoryBranch
|
|
12
12
|
@story = jira_issue
|
13
13
|
@title = jira_issue.summary
|
14
14
|
@id = jira_issue.key
|
15
|
+
@html_url = transform_url(jira_issue.self)
|
15
16
|
end
|
16
17
|
|
17
18
|
def update_state
|
@@ -25,6 +26,12 @@ module StoryBranch
|
|
25
26
|
def dashed_title
|
26
27
|
StoryBranch::StringUtils.normalised_branch_name @title
|
27
28
|
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def transform_url(url)
|
33
|
+
url.gsub(%r{rest/api.*$}, "browse/#{@id}")
|
34
|
+
end
|
28
35
|
end
|
29
36
|
end
|
30
37
|
end
|
@@ -6,8 +6,9 @@ module StoryBranch
|
|
6
6
|
module Jira
|
7
7
|
# Jira Project representation
|
8
8
|
class Project
|
9
|
-
def initialize(jira_project)
|
9
|
+
def initialize(jira_project, query_addon = '')
|
10
10
|
@project = jira_project
|
11
|
+
@query_addon = query_addon
|
11
12
|
end
|
12
13
|
|
13
14
|
# Returns an array of Jira issues (Issue Class)
|
@@ -15,17 +16,24 @@ module StoryBranch
|
|
15
16
|
# Probably will need a specific query builder per tracker
|
16
17
|
def stories(options = {})
|
17
18
|
stories = if options[:id]
|
18
|
-
[@project.
|
19
|
+
[@project.client.Issue.find(options[:id])]
|
19
20
|
else
|
20
|
-
|
21
|
-
@project.client.Issue.jql(
|
22
|
-
"project=#{@project.key} AND status='To Do' AND assignee=currentUser()"
|
23
|
-
)
|
24
|
-
# rubocop:enable Layout/LineLength
|
21
|
+
@project.client.Issue.jql(jql_query)
|
25
22
|
end
|
26
23
|
|
27
24
|
stories.map { |s| Issue.new(s, @project) }
|
28
25
|
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def jql_query
|
30
|
+
base_query = "project=#{@project.key} AND assignee=currentUser()"
|
31
|
+
if @query_addon.length.positive?
|
32
|
+
[base_query, @query_addon].join(' AND ')
|
33
|
+
else
|
34
|
+
base_query
|
35
|
+
end
|
36
|
+
end
|
29
37
|
end
|
30
38
|
end
|
31
39
|
end
|
@@ -5,22 +5,22 @@
|
|
5
5
|
# my tracker and issues will still provide a similar api. This jira-ruby
|
6
6
|
# is used to get the data.
|
7
7
|
require 'jira-ruby'
|
8
|
+
require_relative '../tracker_base'
|
8
9
|
require_relative './project'
|
9
10
|
|
10
11
|
module StoryBranch
|
11
12
|
module Jira
|
12
13
|
# JIRA API wrapper for story branch tracker
|
13
|
-
class Tracker
|
14
|
-
|
14
|
+
class Tracker < StoryBranch::TrackerBase
|
15
|
+
def initialize(tracker_domain:, project_id:, api_key:, username:, extra_query:)
|
16
|
+
super
|
15
17
|
|
16
|
-
attr_reader :type
|
17
|
-
|
18
|
-
def initialize(tracker_domain:, project_id:, api_key:, username:)
|
19
18
|
@tracker_url = "https://#{tracker_domain}.atlassian.net"
|
20
19
|
@project_id = project_id
|
20
|
+
@issue_regex = Regexp.new("#{@project_id}-(\\d+)")
|
21
21
|
@api_key = api_key
|
22
22
|
@username = username
|
23
|
-
@
|
23
|
+
@extra_query = extra_query
|
24
24
|
end
|
25
25
|
|
26
26
|
def valid?
|
@@ -52,10 +52,8 @@ module StoryBranch
|
|
52
52
|
}
|
53
53
|
end
|
54
54
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
@api ||= JIRA::Client.new(options)
|
55
|
+
def configure_api
|
56
|
+
JIRA::Client.new(options)
|
59
57
|
end
|
60
58
|
|
61
59
|
def project
|
@@ -63,7 +61,9 @@ module StoryBranch
|
|
63
61
|
raise 'project key must be set' unless @project_id
|
64
62
|
|
65
63
|
jira_project = api.Project.find(@project_id)
|
66
|
-
@project = Project.new(jira_project)
|
64
|
+
@project = Project.new(jira_project, @extra_query)
|
65
|
+
rescue JIRA::HTTPError => e
|
66
|
+
raise "failed to authenticate: #{e.inspect}"
|
67
67
|
end
|
68
68
|
end
|
69
69
|
end
|
data/lib/story_branch/main.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative './jira/tracker'
|
|
6
6
|
require_relative './git_utils'
|
7
7
|
require_relative './git_wrapper'
|
8
8
|
require_relative './config_manager'
|
9
|
+
require_relative './url_opener'
|
9
10
|
require 'tty-prompt'
|
10
11
|
|
11
12
|
module StoryBranch
|
@@ -17,14 +18,9 @@ module StoryBranch
|
|
17
18
|
attr_accessor :tracker
|
18
19
|
|
19
20
|
def initialize
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# Read local config and decide what Utility to use
|
24
|
-
# (e.g. PivotalUtils, GithubUtils, ...)
|
25
|
-
@local_config = ConfigManager.init_config('.')
|
26
|
-
@global_config = ConfigManager.init_config(Dir.home)
|
27
|
-
initialize_tracker
|
21
|
+
@config = ConfigManager.new
|
22
|
+
abort(@config.errors.join("\n")) unless @config.valid?
|
23
|
+
@tracker = initialize_tracker
|
28
24
|
abort('Invalid tracker configuration setting.') unless @tracker.valid?
|
29
25
|
end
|
30
26
|
|
@@ -72,34 +68,32 @@ module StoryBranch
|
|
72
68
|
update_status('started', 'unstarted', 'unstart')
|
73
69
|
end
|
74
70
|
|
71
|
+
def open_current_url
|
72
|
+
if current_story
|
73
|
+
prompt.say 'Opening story in browser...'
|
74
|
+
StoryBranch::UrlOpener.open_url(current_story.html_url)
|
75
|
+
else
|
76
|
+
prompt.say 'Could not find matching story in configured tracker'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
75
80
|
private
|
76
81
|
|
77
82
|
def require_pivotal
|
78
|
-
if @tracker.
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
true
|
83
|
+
return true if @tracker.class.name.match?('Pivotal')
|
84
|
+
|
85
|
+
prompt.say 'The configured tracker does not support this feature'
|
86
|
+
false
|
83
87
|
end
|
84
88
|
|
85
89
|
def current_story
|
86
|
-
return
|
87
|
-
|
88
|
-
current_story = GitUtils.current_branch_story_parts
|
89
|
-
|
90
|
-
unless current_story.empty?
|
91
|
-
@current_story = @tracker.get_story_by_id(current_story[:id])
|
92
|
-
return @current_story if @current_story
|
93
|
-
end
|
90
|
+
return nil unless @tracker
|
94
91
|
|
95
|
-
|
96
|
-
nil
|
92
|
+
@tracker.current_story
|
97
93
|
end
|
98
94
|
|
99
95
|
def unstaged_changes?
|
100
|
-
unless GitUtils.status?(:untracked) || GitUtils.status?(:modified)
|
101
|
-
return false
|
102
|
-
end
|
96
|
+
return false unless GitUtils.status?(:untracked) || GitUtils.status?(:modified)
|
103
97
|
|
104
98
|
message = <<~MESSAGE
|
105
99
|
There are unstaged changes
|
@@ -159,54 +153,26 @@ module StoryBranch
|
|
159
153
|
@prompt ||= TTY::Prompt.new(interrupt: :exit)
|
160
154
|
end
|
161
155
|
|
162
|
-
def finish_tag
|
163
|
-
return @finish_tag if @finish_tag
|
164
|
-
|
165
|
-
fallback = @global_config.fetch(project_id,
|
166
|
-
:finish_tag,
|
167
|
-
default: 'Finishes')
|
168
|
-
@finish_tag = @local_config.fetch(:finish_tag, default: fallback)
|
169
|
-
@finish_tag
|
170
|
-
end
|
171
|
-
|
172
|
-
def issue_placement
|
173
|
-
return @issue_placement if @issue_placement
|
174
|
-
|
175
|
-
fallback = @global_config.fetch(project_id,
|
176
|
-
:issue_placement,
|
177
|
-
default: 'End')
|
178
|
-
@issue_placement = @local_config.fetch(:issue_placement,
|
179
|
-
default: fallback)
|
180
|
-
@issue_placement
|
181
|
-
end
|
182
|
-
|
183
156
|
def build_finish_message
|
184
|
-
message_tag = [finish_tag, "##{current_story.id}"].join(' ').strip
|
157
|
+
message_tag = [@config.finish_tag, "##{current_story.id}"].join(' ').strip
|
185
158
|
"[#{message_tag}] #{current_story.title}"
|
186
159
|
end
|
187
160
|
|
188
161
|
def create_feature_branch(story)
|
189
162
|
return if story.nil?
|
190
163
|
|
191
|
-
if GitUtils.branch_for_story_exists? story.id
|
192
|
-
prompt.error("An existing branch has the same story id: #{story.id}")
|
193
|
-
return
|
194
|
-
end
|
195
|
-
|
196
164
|
branch_name = valid_branch_name(story)
|
197
165
|
return unless branch_name
|
198
166
|
|
199
|
-
# rubocop:disable Layout/LineLength
|
200
167
|
feature_branch_name_with_story_id = build_branch_name(branch_name, story.id)
|
168
|
+
|
201
169
|
prompt.say("Creating: #{feature_branch_name_with_story_id} with #{current_branch} as parent")
|
202
|
-
# rubocop:enable Layout/LineLength
|
203
170
|
GitWrapper.create_branch feature_branch_name_with_story_id
|
204
171
|
end
|
205
172
|
|
206
173
|
def valid_branch_name(story)
|
207
174
|
prompt.say "You are checked out at: #{current_branch}"
|
208
|
-
branch_name = prompt.ask('Provide a new branch name',
|
209
|
-
default: story.dashed_title)
|
175
|
+
branch_name = prompt.ask('Provide a new branch name', default: story.dashed_title)
|
210
176
|
feature_branch_name = StringUtils.truncate(branch_name.chomp)
|
211
177
|
|
212
178
|
validate_branch_name(feature_branch_name)
|
@@ -216,8 +182,7 @@ module StoryBranch
|
|
216
182
|
# rubocop:disable Metrics/MethodLength
|
217
183
|
def validate_branch_name(name)
|
218
184
|
if GitUtils.similar_branch? name
|
219
|
-
prompt.warn('This name is very similar to an existing branch.'
|
220
|
-
' It is recommended to use a more unique name.')
|
185
|
+
prompt.warn('This name is very similar to an existing branch. It is recommended to use a more unique name.')
|
221
186
|
decision = prompt.select('What to do?') do |menu|
|
222
187
|
menu.choice 'Rename the branch', 1
|
223
188
|
menu.choice 'Proceed with branch name', 2
|
@@ -233,65 +198,30 @@ module StoryBranch
|
|
233
198
|
# rubocop:enable Metrics/MethodLength
|
234
199
|
|
235
200
|
def build_branch_name(branch_name, story_id)
|
236
|
-
if issue_placement.casecmp('beginning').zero?
|
201
|
+
if @config.issue_placement.casecmp('beginning').zero?
|
237
202
|
"#{story_id}-#{branch_name}"
|
238
203
|
else
|
239
204
|
"#{branch_name}-#{story_id}"
|
240
205
|
end
|
241
206
|
end
|
242
207
|
|
243
|
-
def project_id
|
244
|
-
return @project_id if @project_id
|
245
|
-
|
246
|
-
project_ids = @local_config.fetch(:project_id)
|
247
|
-
@project_id = choose_project_id(project_ids)
|
248
|
-
end
|
249
|
-
|
250
|
-
def choose_project_id(project_ids)
|
251
|
-
return project_ids unless project_ids.is_a? Array
|
252
|
-
return project_ids[0] unless project_ids.length > 1
|
253
|
-
|
254
|
-
prompt.select('Which project you want to fetch from?', project_ids)
|
255
|
-
end
|
256
|
-
|
257
|
-
def api_key
|
258
|
-
@api_key ||= @global_config.fetch(project_id, :api_key)
|
259
|
-
end
|
260
|
-
|
261
|
-
def username
|
262
|
-
@username ||= @global_config.fetch(project_id, :username)
|
263
|
-
end
|
264
|
-
|
265
208
|
def current_branch
|
266
209
|
@current_branch ||= GitWrapper.current_branch
|
267
210
|
end
|
268
211
|
|
269
|
-
# rubocop:disable Metrics/AbcSize
|
270
|
-
# rubocop:disable Metrics/MethodLength
|
271
212
|
def initialize_tracker
|
272
|
-
|
273
|
-
|
274
|
-
|
213
|
+
# TODO: Ideally this would be mapped out somewhere so we don't need to
|
214
|
+
# evaluate anything from the config here
|
215
|
+
tracker_type = @config.tracker_type
|
216
|
+
case tracker_type
|
217
|
+
when 'github'
|
218
|
+
StoryBranch::Github::Tracker.new(**@config.tracker_params)
|
219
|
+
when 'pivotal-tracker'
|
220
|
+
StoryBranch::Pivotal::Tracker.new(**@config.tracker_params)
|
221
|
+
when 'jira'
|
222
|
+
StoryBranch::Jira::Tracker.new(**@config.tracker_params)
|
275
223
|
end
|
276
|
-
tracker_type = @local_config.fetch(:tracker, default: 'pivotal-tracker')
|
277
|
-
@tracker = case tracker_type
|
278
|
-
when 'github'
|
279
|
-
StoryBranch::Github::Tracker.new(project_id, api_key)
|
280
|
-
when 'pivotal-tracker'
|
281
|
-
StoryBranch::Pivotal::Tracker.new(project_id, api_key)
|
282
|
-
when 'jira'
|
283
|
-
tracker_domain, project_key = project_id.split('|')
|
284
|
-
options = {
|
285
|
-
tracker_domain: tracker_domain,
|
286
|
-
project_id: project_key,
|
287
|
-
api_key: api_key,
|
288
|
-
username: username
|
289
|
-
}
|
290
|
-
StoryBranch::Jira::Tracker.new(options)
|
291
|
-
end
|
292
224
|
end
|
293
|
-
# rubocop:enable Metrics/AbcSize
|
294
|
-
# rubocop:enable Metrics/MethodLength
|
295
225
|
end
|
296
226
|
# rubocop:enable Metrics/ClassLength
|
297
227
|
end
|
@@ -1,22 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'blanket'
|
4
|
+
require_relative '../tracker_base'
|
4
5
|
require_relative './project'
|
5
6
|
|
6
7
|
module StoryBranch
|
7
8
|
module Pivotal
|
8
9
|
# Utility class for integration with PivotalTracker. It relies on Blanket
|
9
10
|
# wrapper to communicate with pivotal tracker's api.
|
10
|
-
class Tracker
|
11
|
+
class Tracker < StoryBranch::TrackerBase
|
11
12
|
API_URL = 'https://www.pivotaltracker.com/services/v5/'
|
12
|
-
TYPE = 'pivotal'
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(project_id, api_key)
|
14
|
+
def initialize(project_id:, api_key:, **)
|
15
|
+
super
|
17
16
|
@project_id = project_id
|
18
17
|
@api_key = api_key
|
19
|
-
@type = TYPE
|
20
18
|
end
|
21
19
|
|
22
20
|
def valid?
|
@@ -41,9 +39,7 @@ module StoryBranch
|
|
41
39
|
|
42
40
|
private
|
43
41
|
|
44
|
-
def
|
45
|
-
raise 'API key must be specified' unless @api_key
|
46
|
-
|
42
|
+
def configure_api
|
47
43
|
Blanket.wrap API_URL, headers: { 'X-TrackerToken' => @api_key }
|
48
44
|
end
|
49
45
|
|
@@ -11,7 +11,7 @@ module StoryBranch
|
|
11
11
|
undef: :replace, # Replace anything not defined in ASCII
|
12
12
|
replace: '-' # Use a dash for those replacements
|
13
13
|
}
|
14
|
-
res.encode(Encoding.find('ASCII'), encoding_options)
|
14
|
+
res.encode(Encoding.find('ASCII'), **encoding_options)
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.dashed(text)
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StoryBranch
|
4
|
+
# Base story branch tracker class that will define the expected interface
|
5
|
+
class TrackerBase
|
6
|
+
def initialize(_options = {})
|
7
|
+
@issue_regex = Regexp.new('(\\d+)')
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
raise 'valid? > must be implemented in the custom tracker'
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: This should probably be renamed to something more meaningful
|
15
|
+
# in the sense that it should be workable stories/issues
|
16
|
+
# which depend on the tracker's workflow. PivotalTracker they need to
|
17
|
+
# be started and estimated, while for Github they just need to be open
|
18
|
+
def stories
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_story_by_id(_story_id)
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_story
|
27
|
+
return @current_story if @current_story
|
28
|
+
|
29
|
+
# TODO: This should look at the tracker configuration and search
|
30
|
+
# for the string either in the beginning or the end, according
|
31
|
+
# to what is configured
|
32
|
+
story_from_branch = GitUtils.branch_to_story_string(@issue_regex)
|
33
|
+
if story_from_branch.length == 2
|
34
|
+
@current_story = get_story_by_id(story_from_branch[0])
|
35
|
+
return @current_story
|
36
|
+
end
|
37
|
+
prompt.error('No tracked feature associated with this branch')
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def api
|
44
|
+
raise 'API key must be specified' unless @api_key
|
45
|
+
|
46
|
+
@api ||= configure_api
|
47
|
+
end
|
48
|
+
|
49
|
+
def project
|
50
|
+
return @project if @project
|
51
|
+
raise 'project key must be set' unless @project_id
|
52
|
+
|
53
|
+
raise 'project > must be implemented in the custom tracker'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|