abt-cli 0.0.15 → 0.0.20

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +4 -3
  4. data/lib/abt/ari.rb +20 -0
  5. data/lib/abt/ari_list.rb +13 -0
  6. data/lib/abt/base_command.rb +63 -0
  7. data/lib/abt/cli.rb +68 -49
  8. data/lib/abt/cli/arguments_parser.rb +48 -0
  9. data/lib/abt/cli/prompt.rb +7 -6
  10. data/lib/abt/docs.rb +35 -28
  11. data/lib/abt/docs/cli.rb +42 -11
  12. data/lib/abt/docs/markdown.rb +38 -11
  13. data/lib/abt/git_config.rb +26 -31
  14. data/lib/abt/providers/asana/base_command.rb +17 -37
  15. data/lib/abt/providers/asana/commands/add.rb +12 -10
  16. data/lib/abt/providers/asana/commands/{branch-name.rb → branch_name.rb} +12 -7
  17. data/lib/abt/providers/asana/commands/clear.rb +19 -6
  18. data/lib/abt/providers/asana/commands/current.rb +22 -37
  19. data/lib/abt/providers/asana/commands/finalize.rb +8 -12
  20. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
  21. data/lib/abt/providers/asana/commands/init.rb +9 -9
  22. data/lib/abt/providers/asana/commands/pick.rb +28 -15
  23. data/lib/abt/providers/asana/commands/projects.rb +4 -4
  24. data/lib/abt/providers/asana/commands/share.rb +5 -9
  25. data/lib/abt/providers/asana/commands/start.rb +26 -18
  26. data/lib/abt/providers/asana/commands/tasks.rb +7 -6
  27. data/lib/abt/providers/asana/configuration.rb +23 -37
  28. data/lib/abt/providers/asana/path.rb +36 -0
  29. data/lib/abt/providers/devops/api.rb +12 -0
  30. data/lib/abt/providers/devops/base_command.rb +18 -44
  31. data/lib/abt/providers/devops/commands/boards.rb +7 -5
  32. data/lib/abt/providers/devops/commands/{branch-name.rb → branch_name.rb} +10 -6
  33. data/lib/abt/providers/devops/commands/clear.rb +19 -6
  34. data/lib/abt/providers/devops/commands/current.rb +17 -41
  35. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +12 -4
  36. data/lib/abt/providers/devops/commands/init.rb +18 -18
  37. data/lib/abt/providers/devops/commands/pick.rb +16 -16
  38. data/lib/abt/providers/devops/commands/share.rb +6 -7
  39. data/lib/abt/providers/devops/commands/work-items.rb +4 -4
  40. data/lib/abt/providers/devops/configuration.rb +20 -57
  41. data/lib/abt/providers/devops/path.rb +50 -0
  42. data/lib/abt/providers/git/commands/branch.rb +28 -28
  43. data/lib/abt/providers/harvest/base_command.rb +18 -36
  44. data/lib/abt/providers/harvest/commands/clear.rb +19 -6
  45. data/lib/abt/providers/harvest/commands/current.rb +27 -34
  46. data/lib/abt/providers/harvest/commands/init.rb +8 -9
  47. data/lib/abt/providers/harvest/commands/pick.rb +15 -8
  48. data/lib/abt/providers/harvest/commands/projects.rb +4 -4
  49. data/lib/abt/providers/harvest/commands/share.rb +7 -11
  50. data/lib/abt/providers/harvest/commands/start.rb +6 -42
  51. data/lib/abt/providers/harvest/commands/stop.rb +10 -10
  52. data/lib/abt/providers/harvest/commands/tasks.rb +7 -4
  53. data/lib/abt/providers/harvest/commands/track.rb +66 -21
  54. data/lib/abt/providers/harvest/configuration.rb +23 -38
  55. data/lib/abt/providers/harvest/path.rb +36 -0
  56. data/lib/abt/version.rb +1 -1
  57. metadata +11 -7
  58. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  59. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  60. data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
@@ -5,15 +5,15 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Tasks < BaseCommand
8
- def self.command
9
- 'tasks asana'
8
+ def self.usage
9
+ 'abt tasks asana'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'List available tasks on project - useful for piping into grep etc.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_project!
18
18
 
19
19
  tasks.each do |task|
@@ -25,14 +25,15 @@ module Abt
25
25
 
26
26
  def project
27
27
  @project ||= begin
28
- api.get("projects/#{project_gid}")
28
+ api.get("projects/#{project_gid}", opt_fields: 'name')
29
29
  end
30
30
  end
31
31
 
32
32
  def tasks
33
33
  @tasks ||= begin
34
- cli.warn 'Fetching tasks...'
35
- api.get_paged('tasks', project: project['gid'], opt_fields: 'name')
34
+ warn 'Fetching tasks...'
35
+ tasks = api.get_paged('tasks', project: project['gid'], opt_fields: 'name,completed')
36
+ tasks.select { |task| !task['completed'] }
36
37
  end
37
38
  end
38
39
  end
@@ -8,24 +8,23 @@ module Abt
8
8
 
9
9
  def initialize(cli:)
10
10
  @cli = cli
11
- @git = GitConfig.new(namespace: 'abt.asana')
12
11
  end
13
12
 
14
13
  def local_available?
15
- GitConfig.local_available?
14
+ git.available?
16
15
  end
17
16
 
18
- def project_gid
19
- local_available? ? git['projectGid'] : nil
17
+ def path
18
+ Path.new(local_available? && git['path'] || '')
20
19
  end
21
20
 
22
- def task_gid
23
- local_available? ? git['taskGid'] : nil
21
+ def path=(new_path)
22
+ git['path'] = new_path
24
23
  end
25
24
 
26
25
  def workspace_gid
27
26
  @workspace_gid ||= begin
28
- current = git.global['workspaceGid']
27
+ current = git_global['workspaceGid']
29
28
  if current.nil?
30
29
  prompt_workspace['gid']
31
30
  else
@@ -46,37 +45,18 @@ module Abt
46
45
  @finalized_section_gid ||= git['finalizedSectionGid'] || prompt_finalized_section['gid']
47
46
  end
48
47
 
49
- def project_gid=(value)
50
- return if project_gid == value
51
-
52
- clear_local
53
- git['projectGid'] = value unless value.nil?
54
- end
55
-
56
- def task_gid=(value)
57
- git['taskGid'] = value
58
- end
59
-
60
- def clear_local
61
- cli.abort 'No local configuration was found' unless local_available?
62
-
63
- git['projectGid'] = nil
64
- git['taskGid'] = nil
65
- git['wipSectionGid'] = nil
66
- git['finalizedSectionGid'] = nil
48
+ def clear_local(verbose: true)
49
+ git.clear(output: verbose ? cli.err_output : nil)
67
50
  end
68
51
 
69
- def clear_global
70
- git.global.keys.each do |key|
71
- cli.puts 'Deleting configuration: ' + key
72
- git.global[key] = nil
73
- end
52
+ def clear_global(verbose: true)
53
+ git_global.clear(output: verbose ? cli.err_output : nil)
74
54
  end
75
55
 
76
56
  def access_token
77
- return git.global['accessToken'] unless git.global['accessToken'].nil?
57
+ return git_global['accessToken'] unless git_global['accessToken'].nil?
78
58
 
79
- git.global['accessToken'] = cli.prompt.text([
59
+ git_global['accessToken'] = cli.prompt.text([
80
60
  'Please provide your personal access token for Asana.',
81
61
  'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
82
62
  '',
@@ -86,7 +66,13 @@ module Abt
86
66
 
87
67
  private
88
68
 
89
- attr_reader :git
69
+ def git
70
+ @git ||= GitConfig.new('local', 'abt.asana')
71
+ end
72
+
73
+ def git_global
74
+ @git_global ||= GitConfig.new('global', 'abt.asana')
75
+ end
90
76
 
91
77
  def prompt_finalized_section
92
78
  section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
@@ -102,23 +88,23 @@ module Abt
102
88
 
103
89
  def prompt_section(message)
104
90
  cli.warn 'Fetching sections...'
105
- sections = api.get_paged("projects/#{project_gid}/sections")
91
+ sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: 'name')
106
92
  cli.prompt.choice(message, sections)
107
93
  end
108
94
 
109
95
  def prompt_workspace
110
96
  cli.warn 'Fetching workspaces...'
111
- workspaces = api.get_paged('workspaces')
97
+ workspaces = api.get_paged('workspaces', opt_fields: 'name')
112
98
  if workspaces.empty?
113
99
  cli.abort 'Your asana access token does not have access to any workspaces'
114
100
  elsif workspaces.one?
115
101
  workspace = workspaces.first
116
- cli.warn "Selected Asana workspace #{workspace['name']}"
102
+ cli.warn "Selected Asana workspace: #{workspace['name']}"
117
103
  else
118
104
  workspace = cli.prompt.choice('Select Asana workspace', workspaces)
119
105
  end
120
106
 
121
- git.global['workspaceGid'] = workspace['gid']
107
+ git_global['workspaceGid'] = workspace['gid']
122
108
  workspace
123
109
  end
124
110
 
@@ -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,20 +3,18 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Devops
6
- class BaseCommand
7
- attr_reader :arg_str, :organization_name, :project_name, :board_id, :work_item_id, :cli, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
8
 
9
- def initialize(arg_str:, cli:)
10
- @arg_str = arg_str
9
+ attr_reader :config, :path
11
10
 
12
- @config = Configuration.new(cli: cli)
13
- @cli = cli
11
+ def_delegators(:@path, :organization_name, :project_name, :board_id, :work_item_id)
14
12
 
15
- if arg_str.nil?
16
- use_current_args
17
- else
18
- use_arg_str(arg_str)
19
- end
13
+ def initialize(ari:, cli:)
14
+ super
15
+
16
+ @config = Configuration.new(cli: cli)
17
+ @path = ari.path ? Path.new(ari.path) : config.path
20
18
  end
21
19
 
22
20
  private
@@ -24,17 +22,17 @@ module Abt
24
22
  def require_board!
25
23
  return if organization_name && project_name && board_id
26
24
 
27
- cli.abort 'No current/specified board. Did you initialize DevOps?'
25
+ abort 'No current/specified board. Did you initialize DevOps?'
28
26
  end
29
27
 
30
28
  def require_work_item!
31
29
  unless organization_name && project_name && board_id
32
- 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?'
33
31
  end
34
32
 
35
33
  return if work_item_id
36
34
 
37
- 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?'
38
36
  end
39
37
 
40
38
  def sanitize_work_item(work_item)
@@ -47,42 +45,18 @@ module Abt
47
45
  )
48
46
  end
49
47
 
50
- def same_args_as_config?
51
- organization_name == config.organization_name &&
52
- project_name == config.project_name &&
53
- board_id == config.board_id &&
54
- work_item_id == config.work_item_id
55
- end
56
-
57
48
  def print_board(organization_name, project_name, board)
58
- arg_str = "#{organization_name}/#{project_name}/#{board['id']}"
49
+ path = "#{organization_name}/#{project_name}/#{board['id']}"
59
50
 
60
- cli.print_provider_command('devops', arg_str, board['name'])
61
- # cli.warn board['url'] if board.key?('url') && cli.output.isatty # TODO: Web URL
51
+ cli.print_ari('devops', path, board['name'])
52
+ warn api.url_for_board(board) if cli.output.isatty
62
53
  end
63
54
 
64
55
  def print_work_item(organization, project, board, work_item)
65
- arg_str = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
66
-
67
- cli.print_provider_command('devops', arg_str, work_item['name'])
68
- cli.warn work_item['url'] if work_item.key?('url') && cli.output.isatty
69
- end
70
-
71
- def use_current_args
72
- @organization_name = config.organization_name
73
- @project_name = config.project_name
74
- @board_id = config.board_id
75
- @work_item_id = config.work_item_id
76
- end
77
-
78
- def use_arg_str(arg_str)
79
- args = arg_str.to_s.split('/')
80
-
81
- if args.length < 3
82
- cli.abort 'Argument format is <organization>/<project>/<board-id>[/<work-item-id>]'
83
- end
56
+ path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
84
57
 
85
- (@organization_name, @project_name, @board_id, @work_item_id) = args
58
+ cli.print_ari('devops', path, work_item['name'])
59
+ warn work_item['url'] if work_item.key?('url') && cli.output.isatty
86
60
  end
87
61
 
88
62
  def api
@@ -5,17 +5,19 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class Boards < BaseCommand
8
- def self.command
9
- 'boards devops'
8
+ def self.usage
9
+ 'abt boards devops'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'List all boards - useful for piping into grep etc'
14
14
  end
15
15
 
16
- def call
17
- cli.abort 'No organization selected. Did you initialize DevOps?' if organization_name.nil?
18
- cli.abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
16
+ def perform
17
+ if organization_name.nil?
18
+ abort 'No organization selected. Did you initialize DevOps?'
19
+ end
20
+ abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
19
21
 
20
22
  boards.map do |board|
21
23
  print_board(organization_name, project_name, board)
@@ -5,22 +5,26 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class BranchName < BaseCommand
8
- def self.command
9
- 'branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
8
+ def self.usage
9
+ 'abt branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Suggest a git branch name for the current/specified work-item.'
14
14
  end
15
15
 
16
- def call
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
@@ -5,17 +5,30 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
- def self.command
9
- 'clear devops'
8
+ def self.usage
9
+ 'abt clear devops'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear DevOps config for current git repository'
13
+ 'Clear DevOps configuration'
14
14
  end
15
15
 
16
- def call
17
- cli.warn 'Clearing configuration'
18
- config.clear_local
16
+ def self.flags
17
+ [
18
+ ['-g', '--global', 'Clear global instead of local DevOp configuration (credentials etc.)'],
19
+ ['-a', '--all', 'Clear all DevOp configuration']
20
+ ]
21
+ end
22
+
23
+ def perform
24
+ if flags[:global] && flags[:all]
25
+ abort('Flags --global and --all cannot be used at the same time')
26
+ end
27
+
28
+ config.clear_local unless flags[:global]
29
+ config.clear_global if flags[:global] || flags[:all]
30
+
31
+ warn 'Configuration cleared'
19
32
  end
20
33
  end
21
34
  end
@@ -5,72 +5,48 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class Current < BaseCommand
8
- def self.command
9
- 'current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
8
+ def self.usage
9
+ 'abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Get or set DevOps configuration for current git repository'
14
14
  end
15
15
 
16
- def call
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