abt-cli 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/abt/ari.rb +20 -0
  3. data/lib/abt/ari_list.rb +13 -0
  4. data/lib/abt/base_command.rb +63 -0
  5. data/lib/abt/cli.rb +6 -9
  6. data/lib/abt/cli/arguments_parser.rb +1 -23
  7. data/lib/abt/cli/prompt.rb +5 -4
  8. data/lib/abt/docs.rb +6 -5
  9. data/lib/abt/docs/markdown.rb +1 -1
  10. data/lib/abt/git_config.rb +20 -36
  11. data/lib/abt/providers/asana/base_command.rb +13 -33
  12. data/lib/abt/providers/asana/commands/add.rb +9 -7
  13. data/lib/abt/providers/asana/commands/branch_name.rb +9 -4
  14. data/lib/abt/providers/asana/commands/clear.rb +2 -0
  15. data/lib/abt/providers/asana/commands/current.rb +19 -34
  16. data/lib/abt/providers/asana/commands/finalize.rb +3 -3
  17. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +9 -4
  18. data/lib/abt/providers/asana/commands/init.rb +6 -6
  19. data/lib/abt/providers/asana/commands/pick.rb +16 -11
  20. data/lib/abt/providers/asana/commands/projects.rb +1 -1
  21. data/lib/abt/providers/asana/commands/share.rb +2 -6
  22. data/lib/abt/providers/asana/commands/start.rb +14 -12
  23. data/lib/abt/providers/asana/commands/tasks.rb +4 -3
  24. data/lib/abt/providers/asana/configuration.rb +18 -24
  25. data/lib/abt/providers/asana/path.rb +36 -0
  26. data/lib/abt/providers/devops/api.rb +12 -0
  27. data/lib/abt/providers/devops/base_command.rb +13 -38
  28. data/lib/abt/providers/devops/commands/boards.rb +2 -2
  29. data/lib/abt/providers/devops/commands/branch_name.rb +7 -3
  30. data/lib/abt/providers/devops/commands/clear.rb +2 -0
  31. data/lib/abt/providers/devops/commands/current.rb +14 -38
  32. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +9 -1
  33. data/lib/abt/providers/devops/commands/init.rb +15 -15
  34. data/lib/abt/providers/devops/commands/pick.rb +5 -12
  35. data/lib/abt/providers/devops/commands/share.rb +3 -4
  36. data/lib/abt/providers/devops/commands/work-items.rb +1 -1
  37. data/lib/abt/providers/devops/configuration.rb +17 -46
  38. data/lib/abt/providers/devops/path.rb +50 -0
  39. data/lib/abt/providers/git/commands/branch.rb +14 -8
  40. data/lib/abt/providers/harvest/base_command.rb +14 -32
  41. data/lib/abt/providers/harvest/commands/clear.rb +2 -0
  42. data/lib/abt/providers/harvest/commands/current.rb +24 -31
  43. data/lib/abt/providers/harvest/commands/init.rb +5 -6
  44. data/lib/abt/providers/harvest/commands/pick.rb +3 -4
  45. data/lib/abt/providers/harvest/commands/projects.rb +1 -1
  46. data/lib/abt/providers/harvest/commands/share.rb +4 -8
  47. data/lib/abt/providers/harvest/commands/stop.rb +7 -7
  48. data/lib/abt/providers/harvest/commands/tasks.rb +4 -1
  49. data/lib/abt/providers/harvest/commands/track.rb +25 -18
  50. data/lib/abt/providers/harvest/configuration.rb +20 -29
  51. data/lib/abt/providers/harvest/path.rb +36 -0
  52. data/lib/abt/version.rb +1 -1
  53. metadata +8 -3
  54. data/lib/abt/cli/base_command.rb +0 -61
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ class Path < String
7
+ PATH_REGEX = %r{^(?<project_gid>\d+)?(/(?<task_gid>\d+))?$}.freeze
8
+
9
+ def self.from_ids(project_gid = nil, task_gid = nil)
10
+ path = project_gid ? [project_gid, *task_gid].join('/') : ''
11
+ new path
12
+ end
13
+
14
+ def initialize(path = '')
15
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
16
+
17
+ super
18
+ end
19
+
20
+ def project_gid
21
+ match[:project_gid]
22
+ end
23
+
24
+ def task_gid
25
+ match[:task_gid]
26
+ end
27
+
28
+ private
29
+
30
+ def match
31
+ @match ||= PATH_REGEX.match(self)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -69,6 +69,10 @@ module Abt
69
69
  "#{base_url}/_workitems/edit/#{work_item['id']}"
70
70
  end
71
71
 
72
+ def url_for_board(board)
73
+ "#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
74
+ end
75
+
72
76
  def connection
73
77
  @connection ||= Faraday.new(api_endpoint) do |connection|
74
78
  connection.basic_auth username, access_token
@@ -79,6 +83,14 @@ module Abt
79
83
 
80
84
  private
81
85
 
86
+ # Shamelessly copied from ERB::Util.url_encode
87
+ # https://apidock.com/ruby/ERB/Util/url_encode
88
+ def rfc_3986_encode_path_segment(string)
89
+ string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
90
+ format('%%%02X', match.unpack1('C')) # rubocop:disable Style/FormatStringToken
91
+ end
92
+ end
93
+
82
94
  def handle_denied_by_conditional_access_policy!(exception)
83
95
  raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
84
96
 
@@ -3,19 +3,18 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Devops
6
- class BaseCommand < Abt::Cli::BaseCommand
7
- attr_reader :organization_name, :project_name, :board_id, :work_item_id, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
8
 
9
- def initialize(path:, cli:, **)
9
+ attr_reader :config, :path
10
+
11
+ def_delegators(:@path, :organization_name, :project_name, :board_id, :work_item_id)
12
+
13
+ def initialize(ari:, cli:)
10
14
  super
11
15
 
12
16
  @config = Configuration.new(cli: cli)
13
-
14
- if path.nil?
15
- use_current_path
16
- else
17
- use_path(path)
18
- end
17
+ @path = ari.path ? Path.new(ari.path) : config.path
19
18
  end
20
19
 
21
20
  private
@@ -23,17 +22,17 @@ module Abt
23
22
  def require_board!
24
23
  return if organization_name && project_name && board_id
25
24
 
26
- cli.abort 'No current/specified board. Did you initialize DevOps?'
25
+ abort 'No current/specified board. Did you initialize DevOps?'
27
26
  end
28
27
 
29
28
  def require_work_item!
30
29
  unless organization_name && project_name && board_id
31
- cli.abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
30
+ abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
32
31
  end
33
32
 
34
33
  return if work_item_id
35
34
 
36
- cli.abort 'No current/specified work item. Did you pick a DevOps work item?'
35
+ abort 'No current/specified work item. Did you pick a DevOps work item?'
37
36
  end
38
37
 
39
38
  def sanitize_work_item(work_item)
@@ -46,42 +45,18 @@ module Abt
46
45
  )
47
46
  end
48
47
 
49
- def same_args_as_config?
50
- organization_name == config.organization_name &&
51
- project_name == config.project_name &&
52
- board_id == config.board_id &&
53
- work_item_id == config.work_item_id
54
- end
55
-
56
48
  def print_board(organization_name, project_name, board)
57
49
  path = "#{organization_name}/#{project_name}/#{board['id']}"
58
50
 
59
51
  cli.print_ari('devops', path, board['name'])
60
- # cli.warn board['url'] if board.key?('url') && cli.output.isatty # TODO: Web URL
52
+ warn api.url_for_board(board) if cli.output.isatty
61
53
  end
62
54
 
63
55
  def print_work_item(organization, project, board, work_item)
64
56
  path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
65
57
 
66
58
  cli.print_ari('devops', path, work_item['name'])
67
- cli.warn work_item['url'] if work_item.key?('url') && cli.output.isatty
68
- end
69
-
70
- def use_current_path
71
- @organization_name = config.organization_name
72
- @project_name = config.project_name
73
- @board_id = config.board_id
74
- @work_item_id = config.work_item_id
75
- end
76
-
77
- def use_path(path)
78
- args = path.to_s.split('/')
79
-
80
- if args.length < 3
81
- cli.abort 'Argument format is <organization>/<project>/<board-id>[/<work-item-id>]'
82
- end
83
-
84
- (@organization_name, @project_name, @board_id, @work_item_id) = args
59
+ warn work_item['url'] if work_item.key?('url') && cli.output.isatty
85
60
  end
86
61
 
87
62
  def api
@@ -15,9 +15,9 @@ module Abt
15
15
 
16
16
  def perform
17
17
  if organization_name.nil?
18
- cli.abort 'No organization selected. Did you initialize DevOps?'
18
+ abort 'No organization selected. Did you initialize DevOps?'
19
19
  end
20
- cli.abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
20
+ abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
21
21
 
22
22
  boards.map do |board|
23
23
  print_board(organization_name, project_name, board)
@@ -16,11 +16,15 @@ module Abt
16
16
  def perform
17
17
  require_work_item!
18
18
 
19
- cli.puts name
19
+ puts name
20
20
  rescue HttpError::NotFoundError
21
21
  args = [organization_name, project_name, board_id, work_item_id].compact
22
- cli.warn 'Unable to find work item for configuration:'
23
- cli.abort "devops:#{args.join('/')}"
22
+
23
+ error_message = [
24
+ 'Unable to find work item for configuration:',
25
+ "devops:#{args.join('/')}"
26
+ ].join("\n")
27
+ abort error_message
24
28
  end
25
29
 
26
30
  private
@@ -27,6 +27,8 @@ module Abt
27
27
 
28
28
  config.clear_local unless flags[:global]
29
29
  config.clear_global if flags[:global] || flags[:all]
30
+
31
+ warn 'Configuration cleared'
30
32
  end
31
33
  end
32
34
  end
@@ -14,63 +14,39 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
+ abort 'Must be run inside a git repository' unless config.local_available?
18
+
17
19
  require_board!
20
+ ensure_valid_configuration!
18
21
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
22
+ if path != config.path && config.local_available?
23
+ config.path = path
24
+ warn 'Configuration updated'
24
25
  end
25
- end
26
-
27
- private
28
26
 
29
- def show_current_configuration
30
- if work_item_id.nil?
31
- print_board(organization_name, project_name, board)
32
- else
33
- print_work_item(organization_name, project_name, board, work_item)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_board_is_valid!
30
+ private
39
31
 
32
+ def print_configuration
40
33
  if work_item_id.nil?
41
- update_board_config
42
- config.work_item_id = nil
43
-
44
34
  print_board(organization_name, project_name, board)
45
35
  else
46
- ensure_work_item_is_valid!
47
-
48
- update_board_config
49
- config.work_item_id = work_item_id
50
-
51
36
  print_work_item(organization_name, project_name, board, work_item)
52
37
  end
53
38
  end
54
39
 
55
- def update_board_config
56
- config.organization_name = organization_name
57
- config.project_name = project_name
58
- config.board_id = board_id
59
- end
60
-
61
- def ensure_board_is_valid!
40
+ def ensure_valid_configuration!
62
41
  if board.nil?
63
- cli.abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
42
+ abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
64
43
  end
65
- end
66
-
67
- def ensure_work_item_is_valid!
68
- cli.abort "No such work item: ##{work_item_id}" if work_item.nil?
44
+ abort "No such work item: ##{work_item_id}" if work_item_id && work_item.nil?
69
45
  end
70
46
 
71
47
  def board
72
48
  @board ||= begin
73
- cli.warn 'Fetching board...'
49
+ warn 'Fetching board...'
74
50
  api.get("work/boards/#{board_id}")
75
51
  rescue HttpError::NotFoundError
76
52
  nil
@@ -79,7 +55,7 @@ module Abt
79
55
 
80
56
  def work_item
81
57
  @work_item ||= begin
82
- cli.warn 'Fetching work item...'
58
+ warn 'Fetching work item...'
83
59
  work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
84
60
  sanitize_work_item(work_item)
85
61
  rescue HttpError::NotFoundError
@@ -25,7 +25,15 @@ module Abt
25
25
  }
26
26
  }
27
27
 
28
- cli.puts Oj.dump(body, mode: :json)
28
+ puts Oj.dump(body, mode: :json)
29
+ rescue HttpError::NotFoundError
30
+ args = [organization_name, project_name, board_id, work_item_id].compact
31
+
32
+ error_message = [
33
+ 'Unable to find work item for configuration:',
34
+ "devops:#{args.join('/')}"
35
+ ].join("\n")
36
+ abort error_message
29
37
  end
30
38
 
31
39
  private
@@ -17,15 +17,11 @@ module Abt
17
17
  end
18
18
 
19
19
  def perform
20
- cli.abort 'Must be run inside a git repository' unless config.local_available?
21
-
22
- @organization_name = config.organization_name = organization_name_from_url
23
- @project_name = config.project_name = project_name_from_url
20
+ abort 'Must be run inside a git repository' unless config.local_available?
24
21
 
25
22
  board = cli.prompt.choice 'Select a project work board', boards
26
23
 
27
- config.board_id = board['id']
28
-
24
+ config.path = Path.from_ids(organization_name, project_name, board['id'])
29
25
  print_board(organization_name, project_name, board)
30
26
  end
31
27
 
@@ -35,17 +31,21 @@ module Abt
35
31
  @boards ||= api.get_paged('work/boards')
36
32
  end
37
33
 
38
- def project_name_from_url
39
- if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
40
- (match = VS_URL_REGEX.match(project_url))
41
- match[:project]
34
+ def project_name
35
+ @project_name ||= begin
36
+ if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
37
+ (match = VS_URL_REGEX.match(project_url))
38
+ match[:project]
39
+ end
42
40
  end
43
41
  end
44
42
 
45
- def organization_name_from_url
46
- if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
47
- (match = VS_URL_REGEX.match(project_url))
48
- match[:organization]
43
+ def organization_name
44
+ @organization_name ||= begin
45
+ if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
46
+ (match = VS_URL_REGEX.match(project_url))
47
+ match[:organization]
48
+ end
49
49
  end
50
50
  end
51
51
 
@@ -61,7 +61,7 @@ module Abt
61
61
 
62
62
  break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
63
63
 
64
- cli.warn 'Invalid URL'
64
+ warn 'Invalid URL'
65
65
  end
66
66
  end
67
67
  end
@@ -20,36 +20,29 @@ module Abt
20
20
  end
21
21
 
22
22
  def perform
23
- cli.abort 'Must be run inside a git repository' unless config.local_available?
23
+ abort 'Must be run inside a git repository' unless config.local_available?
24
24
  require_board!
25
25
 
26
- cli.warn "#{project_name} - #{board['name']}"
26
+ warn "#{project_name} - #{board['name']}"
27
27
 
28
28
  work_item = select_work_item
29
29
  print_work_item(organization_name, project_name, board, work_item)
30
30
 
31
31
  return if flags[:"dry-run"]
32
32
 
33
- update_config!(work_item)
33
+ config.path = Path.from_ids(organization_name, project_name, board_id, work_item['id'])
34
34
  end
35
35
 
36
36
  private
37
37
 
38
- def update_config!(work_item)
39
- config.organization_name = organization_name
40
- config.project_name = project_name
41
- config.board_id = board_id
42
- config.work_item_id = work_item['id']
43
- end
44
-
45
38
  def select_work_item
46
39
  loop do
47
40
  column = cli.prompt.choice 'Which column?', columns
48
- cli.warn 'Fetching work items...'
41
+ warn 'Fetching work items...'
49
42
  work_items = work_items_in_column(column)
50
43
 
51
44
  if work_items.length.zero?
52
- cli.warn 'Section is empty'
45
+ warn 'Section is empty'
53
46
  next
54
47
  end
55
48
 
@@ -10,14 +10,13 @@ module Abt
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print DevOps config string'
13
+ 'Print DevOps ARI'
14
14
  end
15
15
 
16
16
  def perform
17
- require_work_item!
17
+ require_board!
18
18
 
19
- args = [organization_name, project_name, board_id, work_item_id].compact
20
- cli.print_ari('devops', args.join('/'))
19
+ cli.print_ari('devops', path)
21
20
  end
22
21
  end
23
22
  end
@@ -25,7 +25,7 @@ module Abt
25
25
 
26
26
  def work_items
27
27
  @work_items ||= begin
28
- cli.warn 'Fetching work items...'
28
+ warn 'Fetching work items...'
29
29
  api.work_item_query(
30
30
  <<~WIQL
31
31
  SELECT [System.Id]
@@ -8,53 +8,18 @@ module Abt
8
8
 
9
9
  def initialize(cli:)
10
10
  @cli = cli
11
- @git = GitConfig.new(namespace: 'abt.devops')
12
11
  end
13
12
 
14
13
  def local_available?
15
- GitConfig.local_available?
14
+ git.available?
16
15
  end
17
16
 
18
- def organization_name
19
- local_available? ? git['organizationName'] : nil
17
+ def path
18
+ Path.new(local_available? && git['path'] || '')
20
19
  end
21
20
 
22
- def project_name
23
- local_available? ? git['projectName'] : nil
24
- end
25
-
26
- def board_id
27
- local_available? ? git['boardId'] : nil
28
- end
29
-
30
- def work_item_id
31
- local_available? ? git['workItemId'] : nil
32
- end
33
-
34
- def organization_name=(value)
35
- return if organization_name == value
36
-
37
- clear_local(verbose: false)
38
- git['organizationName'] = value unless value.nil?
39
- end
40
-
41
- def project_name=(value)
42
- return if project_name == value
43
-
44
- git['projectName'] = value unless value.nil?
45
- git['boardId'] = nil
46
- git['workItemId'] = nil
47
- end
48
-
49
- def board_id=(value)
50
- return if board_id == value
51
-
52
- git['boardId'] = value unless value.nil?
53
- git['workItemId'] = nil
54
- end
55
-
56
- def work_item_id=(value)
57
- git['workItemId'] = value
21
+ def path=(new_path)
22
+ git['path'] = new_path
58
23
  end
59
24
 
60
25
  def clear_local(verbose: true)
@@ -62,15 +27,15 @@ module Abt
62
27
  end
63
28
 
64
29
  def clear_global(verbose: true)
65
- git.global.clear(output: verbose ? cli.err_output : nil)
30
+ git_global.clear(output: verbose ? cli.err_output : nil)
66
31
  end
67
32
 
68
33
  def username_for_organization(organization_name)
69
34
  username_key = "organizations.#{organization_name}.username"
70
35
 
71
- return git.global[username_key] unless git.global[username_key].nil?
36
+ return git_global[username_key] unless git_global[username_key].nil?
72
37
 
73
- git.global[username_key] = cli.prompt.text([
38
+ git_global[username_key] = cli.prompt.text([
74
39
  "Please provide your username for the DevOps organization (#{organization_name}).",
75
40
  '',
76
41
  'Enter username'
@@ -80,9 +45,9 @@ module Abt
80
45
  def access_token_for_organization(organization_name)
81
46
  access_token_key = "organizations.#{organization_name}.accessToken"
82
47
 
83
- return git.global[access_token_key] unless git.global[access_token_key].nil?
48
+ return git_global[access_token_key] unless git_global[access_token_key].nil?
84
49
 
85
- git.global[access_token_key] = cli.prompt.text([
50
+ git_global[access_token_key] = cli.prompt.text([
86
51
  "Please provide your personal access token for the DevOps organization (#{organization_name}).",
87
52
  'If you don\'t have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate',
88
53
  '',
@@ -95,7 +60,13 @@ module Abt
95
60
 
96
61
  private
97
62
 
98
- attr_reader :git
63
+ def git
64
+ @git ||= GitConfig.new('local', 'abt.devops')
65
+ end
66
+
67
+ def git_global
68
+ @git_global ||= GitConfig.new('global', 'abt.devops')
69
+ end
99
70
  end
100
71
  end
101
72
  end