abt-cli 0.0.14 → 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 (61) 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 +89 -54
  8. data/lib/abt/cli/arguments_parser.rb +48 -0
  9. data/lib/abt/cli/{dialogs.rb → prompt.rb} +38 -18
  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 +15 -13
  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 +6 -6
  20. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
  21. data/lib/abt/providers/asana/commands/init.rb +11 -11
  22. data/lib/abt/providers/asana/commands/pick.rb +30 -17
  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 +27 -19
  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 +20 -20
  37. data/lib/abt/providers/devops/commands/pick.rb +18 -18
  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 +10 -11
  47. data/lib/abt/providers/harvest/commands/pick.rb +16 -9
  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 +12 -9
  58. data/lib/abt/cli/io.rb +0 -23
  59. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  60. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  61. data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
@@ -5,58 +5,66 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Start < BaseCommand
8
- def self.command
9
- 'start asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt start asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Set current task and move it to a section (column) of your choice'
13
+ 'Move current or specified task to WIP section (column) and assign it to you'
14
14
  end
15
15
 
16
- def call
16
+ def self.flags
17
+ [
18
+ ['-s', '--set', 'Set specified task as current']
19
+ ]
20
+ end
21
+
22
+ def perform
17
23
  require_task!
18
24
 
19
- maybe_override_current_task
25
+ print_task(project_gid, task)
20
26
 
21
27
  update_assignee_if_needed
22
28
  move_if_needed
29
+ maybe_override_current_task
23
30
  end
24
31
 
25
32
  private
26
33
 
27
34
  def maybe_override_current_task
28
- return if arg_str.nil?
29
- return if same_args_as_config?
35
+ return unless flags[:set]
36
+ return if path.nil?
37
+ return if path == config.path
30
38
  return unless config.local_available?
31
39
 
32
- should_override = cli.prompt_boolean 'Set selected task as current?'
33
- Current.new(arg_str: arg_str, cli: cli).call if should_override
40
+ config.path = path
41
+ warn 'Current task updated'
34
42
  end
35
43
 
36
44
  def update_assignee_if_needed
37
45
  current_assignee = task.dig('assignee')
38
46
 
39
47
  if current_assignee.nil?
40
- cli.warn "Assigning task to user: #{current_user['name']}"
48
+ warn "Assigning task to user: #{current_user['name']}"
41
49
  update_assignee
42
50
  elsif current_assignee['gid'] == current_user['gid']
43
- cli.warn 'You are already assigned to this task'
44
- elsif cli.prompt_boolean "Task is assigned to: #{current_assignee['name']}, take over?"
45
- cli.warn "Reassigning task to user: #{current_user['name']}"
51
+ warn 'You are already assigned to this task'
52
+ elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
53
+ warn "Reassigning task to user: #{current_user['name']}"
46
54
  update_assignee
47
55
  end
48
56
  end
49
57
 
50
58
  def move_if_needed
51
- unless project_gid == config.project_gid
52
- cli.warn 'Task was not moved, this is not implemented for tasks outside current project'
59
+ unless project_gid == config.path.project_gid
60
+ warn 'Task was not moved, this is not implemented for tasks outside current project'
53
61
  return
54
62
  end
55
63
 
56
64
  if task_already_in_wip_section?
57
- cli.warn "Task already in section: #{current_task_section['name']}"
65
+ warn "Task already in section: #{current_task_section['name']}"
58
66
  else
59
- cli.warn "Moving task to section: #{wip_section['name']}"
67
+ warn "Moving task to section: #{wip_section['name']}"
60
68
  move_task
61
69
  end
62
70
  end
@@ -76,7 +84,7 @@ module Abt
76
84
  end
77
85
 
78
86
  def wip_section
79
- @wip_section ||= api.get("sections/#{config.wip_section_gid}")
87
+ @wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: 'name')
80
88
  end
81
89
 
82
90
  def move_task
@@ -96,7 +104,7 @@ module Abt
96
104
  end
97
105
 
98
106
  def task
99
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name')
107
+ @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name,permalink_url')
100
108
  end
101
109
  end
102
110
  end
@@ -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.filter { |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([
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,8 +88,8 @@ 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")
106
- cli.prompt_choice(message, sections)
91
+ sections = api.get_paged("projects/#{path.project_gid}/sections")
92
+ cli.prompt.choice(message, sections)
107
93
  end
108
94
 
109
95
  def prompt_workspace
@@ -115,10 +101,10 @@ module Abt
115
101
  workspace = workspaces.first
116
102
  cli.warn "Selected Asana workspace #{workspace['name']}"
117
103
  else
118
- workspace = cli.prompt_choice('Select Asana workspace', workspaces)
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