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
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Path < String
7
+ ORGANIZATION_NAME_REGEX = %r{(?<organization_name>[^/ ]+)}.freeze
8
+ PROJECT_NAME_REGEX = %r{(?<project_name>[^/ ]+)}.freeze
9
+ BOARD_ID_REGEX = /(?<board_id>[a-z0-9\-]+)/.freeze
10
+ WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
11
+
12
+ PATH_REGEX = %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX})?(/#{WORK_ITEM_ID_REGEX})?}.freeze
13
+
14
+ def self.from_ids(organization_id = nil, project_name = nil, board_id = nil, work_item_id = nil)
15
+ return new unless organization_id && project_name && board_id
16
+
17
+ new [organization_id, project_name, board_id, *work_item_id].join('/')
18
+ end
19
+
20
+ def initialize(path = '')
21
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
22
+
23
+ super
24
+ end
25
+
26
+ def organization_name
27
+ match[:organization_name]
28
+ end
29
+
30
+ def project_name
31
+ match[:project_name]
32
+ end
33
+
34
+ def board_id
35
+ match[:board_id]
36
+ end
37
+
38
+ def work_item_id
39
+ match[:work_item_id]
40
+ end
41
+
42
+ private
43
+
44
+ def match
45
+ @match ||= PATH_REGEX.match(self)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -4,39 +4,33 @@ module Abt
4
4
  module Providers
5
5
  module Git
6
6
  module Commands
7
- class Branch
8
- attr_reader :cli
9
-
10
- def self.command
11
- 'branch git <provider>'
7
+ class Branch < Abt::BaseCommand
8
+ def self.usage
9
+ 'abt branch git <scheme>[:<path>]'
12
10
  end
13
11
 
14
12
  def self.description
15
- 'Switch branch. Uses a compatible provider to generate the branch-name: E.g. `abt branch git asana`'
16
- end
17
-
18
- def initialize(cli:, **)
19
- @cli = cli
13
+ 'Switch branch. Uses a compatible scheme to generate the branch-name: E.g. `abt branch git asana`'
20
14
  end
21
15
 
22
- def call
23
- create_and_switch unless switch
24
- cli.warn "Switched to #{branch_name}"
16
+ def perform
17
+ switch || create_and_switch
18
+ warn "Switched to #{branch_name}"
25
19
  end
26
20
 
27
21
  private
28
22
 
29
23
  def switch
30
24
  success = false
31
- Open3.popen3("git switch #{branch_name}") do |_i, _o, _error_output, thread|
25
+ Open3.popen3("git switch #{branch_name}") do |_i, _o, _e, thread|
32
26
  success = thread.value.success?
33
27
  end
34
28
  success
35
29
  end
36
30
 
37
31
  def create_and_switch
38
- cli.warn "No such branch: #{branch_name}"
39
- cli.abort('Aborting') unless cli.prompt_boolean 'Create branch?'
32
+ warn "No such branch: #{branch_name}"
33
+ abort('Aborting') unless cli.prompt.boolean 'Create branch?'
40
34
 
41
35
  Open3.popen3("git switch -c #{branch_name}") do |_i, _o, _e, thread|
42
36
  thread.value
@@ -45,29 +39,35 @@ module Abt
45
39
 
46
40
  def branch_name # rubocop:disable Metrics/MethodLength
47
41
  @branch_name ||= begin
48
- if branch_names_from_providers.empty?
49
- cli.abort [
50
- 'None of the specified providers responded to `branch-name`.',
51
- 'Did you add compatible provider? e.g.:',
42
+ if branch_names_from_aris.empty?
43
+ abort [
44
+ 'None of the specified ARIs responded to `branch-name`.',
45
+ 'Did you add compatible scheme? e.g.:',
52
46
  ' abt branch git asana',
53
47
  ' abt branch git devops'
54
48
  ].join("\n")
55
49
  end
56
50
 
57
- if branch_names_from_providers.length > 1
58
- cli.abort [
59
- 'Got branch names from multiple providers, only one is supported',
60
- 'Branch names where:',
61
- *branch_names_from_providers.map { |name| " #{name}" }
51
+ if branch_names_from_aris.length > 1
52
+ abort [
53
+ 'Got branch names from multiple ARIs, only one is supported',
54
+ 'Branch names were:',
55
+ *branch_names_from_aris.map { |name| " #{name}" }
62
56
  ].join("\n")
63
57
  end
64
58
 
65
- branch_names_from_providers.first
59
+ branch_names_from_aris.first
66
60
  end
67
61
  end
68
62
 
69
- def branch_names_from_providers
70
- input = StringIO.new(cli.args.join(' '))
63
+ def branch_names_from_aris
64
+ other_aris = cli.aris - [ari]
65
+
66
+ if other_aris.empty?
67
+ abort 'You must provide an additional ARI that responds to: branch-name. E.g., asana'
68
+ end
69
+
70
+ input = StringIO.new(cli.aris.to_s)
71
71
  output = StringIO.new
72
72
  Abt::Cli.new(argv: ['branch-name'], output: output, input: input).perform
73
73
 
@@ -3,40 +3,38 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Harvest
6
- class BaseCommand
7
- attr_reader :arg_str, :project_id, :task_id, :cli, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
8
 
9
- def initialize(arg_str:, cli:)
10
- @arg_str = arg_str
11
- @config = Configuration.new(cli: cli)
9
+ attr_reader :config, :path
12
10
 
13
- if arg_str.nil?
14
- use_current_args
15
- else
16
- use_arg_str(arg_str)
17
- end
18
- @cli = cli
11
+ def_delegators(:@path, :project_id, :task_id)
12
+
13
+ def initialize(ari:, cli:)
14
+ super
15
+
16
+ @config = Configuration.new(cli: cli)
17
+ @path = ari.path ? Path.new(ari.path) : config.path
19
18
  end
20
19
 
21
20
  private
22
21
 
23
22
  def require_project!
24
- cli.abort 'No current/specified project. Did you initialize Harvest?' if project_id.nil?
23
+ return if project_id
24
+
25
+ abort 'No current/specified project. Did you initialize Harvest?'
25
26
  end
26
27
 
27
28
  def require_task!
28
- if project_id.nil?
29
- cli.abort 'No current/specified project. Did you initialize Harvest and pick a task?'
29
+ unless project_id
30
+ abort 'No current/specified project. Did you initialize Harvest and pick a task?'
30
31
  end
31
- cli.abort 'No current/specified task. Did you pick a Harvest task?' if task_id.nil?
32
- end
33
32
 
34
- def same_args_as_config?
35
- project_id == config.project_id && task_id == config.task_id
33
+ abort 'No current/specified task. Did you pick a Harvest task?' if task_id.nil?
36
34
  end
37
35
 
38
36
  def print_project(project)
39
- cli.print_provider_command(
37
+ cli.print_ari(
40
38
  'harvest',
41
39
  project['id'],
42
40
  "#{project['client']['name']} > #{project['name']}"
@@ -44,29 +42,13 @@ module Abt
44
42
  end
45
43
 
46
44
  def print_task(project, task)
47
- cli.print_provider_command(
45
+ cli.print_ari(
48
46
  'harvest',
49
47
  "#{project['id']}/#{task['id']}",
50
48
  "#{project['name']} > #{task['name']}"
51
49
  )
52
50
  end
53
51
 
54
- def use_current_args
55
- @project_id = config.project_id
56
- @task_id = config.task_id
57
- end
58
-
59
- def use_arg_str(arg_str)
60
- args = arg_str.to_s.split('/')
61
- @project_id = args[0].to_s
62
- @project_id = nil if project_id.empty?
63
-
64
- return if project_id.nil?
65
-
66
- @task_id = args[1].to_s
67
- @task_id = nil if @task_id.empty?
68
- end
69
-
70
52
  def api
71
53
  @api ||= Abt::Providers::Harvest::Api.new(access_token: config.access_token,
72
54
  account_id: config.account_id)
@@ -5,17 +5,30 @@ module Abt
5
5
  module Harvest
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
- def self.command
9
- 'clear harvest'
8
+ def self.usage
9
+ 'abt clear harvest'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear project/task for current git repository'
13
+ 'Clear harvest configuration'
14
14
  end
15
15
 
16
- def call
17
- cli.warn 'Clearing Harvest project configuration'
18
- config.clear_local
16
+ def self.flags
17
+ [
18
+ ['-g', '--global', 'Clear global instead of local harvest configuration (credentials etc.)'],
19
+ ['-a', '--all', 'Clear all harvest 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,66 +5,59 @@ module Abt
5
5
  module Harvest
6
6
  module Commands
7
7
  class Current < BaseCommand
8
- def self.command
9
- 'current harvest[:<project-id>[/<task-id>]]'
8
+ def self.usage
9
+ 'abt current harvest[:<project-id>[/<task-id>]]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Get or set project and or task 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_project!
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
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 task_id.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_project_is_valid!
39
- config.project_id = project_id
30
+ private
40
31
 
32
+ def print_configuration
41
33
  if task_id.nil?
42
34
  print_project(project)
43
- config.task_id = nil
44
35
  else
45
- ensure_task_is_valid!
46
- config.task_id = task_id
47
-
48
36
  print_task(project, task)
49
37
  end
50
38
  end
51
39
 
52
- def ensure_project_is_valid!
53
- cli.abort "Invalid project: #{project_id}" if project.nil?
54
- end
55
-
56
- def ensure_task_is_valid!
57
- cli.abort "Invalid task: #{task_id}" if task.nil?
40
+ def ensure_valid_configuration!
41
+ abort "Invalid project: #{project_id}" if project.nil?
42
+ abort "Invalid task: #{task_id}" if task_id && task.nil?
58
43
  end
59
44
 
60
45
  def project
61
- @project ||= project_assignment['project'].merge('client' => project_assignment['client'])
46
+ return @project if instance_variable_defined? :@project
47
+
48
+ @project = if project_assignment
49
+ project_assignment['project'].merge('client' => project_assignment['client'])
50
+ end
62
51
  end
63
52
 
64
53
  def task
65
- @task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
66
- task['id'].to_s == task_id
67
- end
54
+ return @task if instance_variable_defined? :@task
55
+
56
+ @task = if project_assignment
57
+ project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
58
+ task['id'].to_s == task_id
59
+ end
60
+ end
68
61
  end
69
62
 
70
63
  def project_assignment
@@ -5,22 +5,21 @@ module Abt
5
5
  module Harvest
6
6
  module Commands
7
7
  class Init < BaseCommand
8
- def self.command
9
- 'init harvest'
8
+ def self.usage
9
+ 'abt init harvest'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Pick Harvest project for current git repository'
14
14
  end
15
15
 
16
- def call
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
16
+ def perform
17
+ abort 'Must be run inside a git repository' unless config.local_available?
18
18
 
19
19
  projects # Load projects up front to make it obvious that searches are instant
20
20
  project = find_search_result
21
21
 
22
- config.project_id = project['id']
23
- config.task_id = nil
22
+ config.path = Path.from_ids(project['id'])
24
23
 
25
24
  print_project(project)
26
25
  end
@@ -28,17 +27,17 @@ module Abt
28
27
  private
29
28
 
30
29
  def find_search_result
31
- cli.warn 'Select a project'
30
+ warn 'Select a project'
32
31
 
33
32
  loop do
34
- matches = matches_for_string cli.prompt('Enter search')
33
+ matches = matches_for_string cli.prompt.text('Enter search')
35
34
  if matches.empty?
36
35
  warn 'No matches'
37
36
  next
38
37
  end
39
38
 
40
- cli.warn 'Showing the 10 first matches' if matches.size > 10
41
- choice = cli.prompt_choice 'Select a project', matches[0...10], true
39
+ warn 'Showing the 10 first matches' if matches.size > 10
40
+ choice = cli.prompt.choice 'Select a project', matches[0...10], true
42
41
  break choice['project'] unless choice.nil?
43
42
  end
44
43
  end
@@ -66,7 +65,7 @@ module Abt
66
65
 
67
66
  def projects
68
67
  @projects ||= begin
69
- cli.warn 'Fetching projects...'
68
+ warn 'Fetching projects...'
70
69
  project_assignments.map do |project_assignment|
71
70
  project_assignment['project'].merge('client' => project_assignment['client'])
72
71
  end
@@ -5,25 +5,32 @@ module Abt
5
5
  module Harvest
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
- def self.command
9
- 'pick harvest[:<project-id>]'
8
+ def self.usage
9
+ 'abt pick harvest[:<project-id>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Pick task for current git repository'
14
14
  end
15
15
 
16
- def call
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
18
- require_project!
16
+ def self.flags
17
+ [
18
+ ['-d', '--dry-run', 'Keep existing configuration']
19
+ ]
20
+ end
19
21
 
20
- cli.warn project['name']
21
- task = cli.prompt_choice 'Select a task', tasks
22
+ def perform
23
+ abort 'Must be run inside a git repository' unless config.local_available?
24
+ require_project!
22
25
 
23
- config.project_id = project_id # We might have gotten the project ID as an argument
24
- config.task_id = task['id']
26
+ warn project['name']
27
+ task = cli.prompt.choice 'Select a task', tasks
25
28
 
26
29
  print_task(project, task)
30
+
31
+ return if flags[:"dry-run"]
32
+
33
+ config.path = Path.from_ids(project_id, task['id'])
27
34
  end
28
35
 
29
36
  private