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.
@@ -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.issues.find(options[:id])]
19
+ [@project.client.Issue.find(options[:id])]
19
20
  else
20
- # rubocop:disable Layout/LineLength
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
- TYPE = 'jira'
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
- @type = TYPE
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 api
56
- raise 'API key must be specified' unless @api_key
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
@@ -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
- # TODO: Config manager should be responsible for handling the
21
- # configuration and the story branch should only initialize one
22
- # config manager that has attr accessors for needed values
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.type != 'pivotal'
79
- prompt.say 'The configured tracker does not support this feature'
80
- return false
81
- end
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 @current_story if @current_story
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
- prompt.error('No tracked feature associated with this branch')
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
- if project_id.nil?
273
- prompt.say 'Project ID not set'
274
- exit 0
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
- attr_reader :type
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 api
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,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