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,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,7 +4,7 @@ module Abt
4
4
  module Providers
5
5
  module Git
6
6
  module Commands
7
- class Branch < Abt::Cli::BaseCommand
7
+ class Branch < Abt::BaseCommand
8
8
  def self.usage
9
9
  'abt branch git <scheme>[:<path>]'
10
10
  end
@@ -14,23 +14,23 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- create_and_switch unless switch
18
- cli.warn "Switched to #{branch_name}"
17
+ switch || create_and_switch
18
+ warn "Switched to #{branch_name}"
19
19
  end
20
20
 
21
21
  private
22
22
 
23
23
  def switch
24
24
  success = false
25
- Open3.popen3("git switch #{branch_name}") do |_i, _o, _error_output, thread|
25
+ Open3.popen3("git switch #{branch_name}") do |_i, _o, _e, thread|
26
26
  success = thread.value.success?
27
27
  end
28
28
  success
29
29
  end
30
30
 
31
31
  def create_and_switch
32
- cli.warn "No such branch: #{branch_name}"
33
- 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?'
34
34
 
35
35
  Open3.popen3("git switch -c #{branch_name}") do |_i, _o, _e, thread|
36
36
  thread.value
@@ -40,7 +40,7 @@ module Abt
40
40
  def branch_name # rubocop:disable Metrics/MethodLength
41
41
  @branch_name ||= begin
42
42
  if branch_names_from_aris.empty?
43
- cli.abort [
43
+ abort [
44
44
  'None of the specified ARIs responded to `branch-name`.',
45
45
  'Did you add compatible scheme? e.g.:',
46
46
  ' abt branch git asana',
@@ -49,7 +49,7 @@ module Abt
49
49
  end
50
50
 
51
51
  if branch_names_from_aris.length > 1
52
- cli.abort [
52
+ abort [
53
53
  'Got branch names from multiple ARIs, only one is supported',
54
54
  'Branch names were:',
55
55
  *branch_names_from_aris.map { |name| " #{name}" }
@@ -61,6 +61,12 @@ module Abt
61
61
  end
62
62
 
63
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
+
64
70
  input = StringIO.new(cli.aris.to_s)
65
71
  output = StringIO.new
66
72
  Abt::Cli.new(argv: ['branch-name'], output: output, input: input).perform
@@ -3,36 +3,34 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Harvest
6
- class BaseCommand < Abt::Cli::BaseCommand
7
- attr_reader :path, :flags, :project_id, :task_id, :cli, :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, :project_id, :task_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
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)
@@ -51,22 +49,6 @@ module Abt
51
49
  )
52
50
  end
53
51
 
54
- def use_current_path
55
- @project_id = config.project_id
56
- @task_id = config.task_id
57
- end
58
-
59
- def use_path(path)
60
- args = path.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)
@@ -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,57 +14,50 @@ 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_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
@@ -14,13 +14,12 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
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,7 +27,7 @@ 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
33
  matches = matches_for_string cli.prompt.text('Enter search')
@@ -37,7 +36,7 @@ module Abt
37
36
  next
38
37
  end
39
38
 
40
- cli.warn 'Showing the 10 first matches' if matches.size > 10
39
+ warn 'Showing the 10 first matches' if matches.size > 10
41
40
  choice = cli.prompt.choice 'Select a project', matches[0...10], true
42
41
  break choice['project'] unless choice.nil?
43
42
  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
@@ -20,18 +20,17 @@ 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_project!
25
25
 
26
- cli.warn project['name']
26
+ warn project['name']
27
27
  task = cli.prompt.choice 'Select a task', tasks
28
28
 
29
29
  print_task(project, task)
30
30
 
31
31
  return if flags[:"dry-run"]
32
32
 
33
- config.project_id = project_id # We might have gotten the project ID as a path
34
- config.task_id = task['id']
33
+ config.path = Path.from_ids(project_id, task['id'])
35
34
  end
36
35
 
37
36
  private
@@ -23,7 +23,7 @@ module Abt
23
23
 
24
24
  def projects
25
25
  @projects ||= begin
26
- cli.warn 'Fetching projects...'
26
+ warn 'Fetching projects...'
27
27
  project_assignments.map do |project_assignment|
28
28
  project_assignment['project'].merge('client' => project_assignment['client'])
29
29
  end
@@ -10,17 +10,13 @@ module Abt
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print project/task config string'
13
+ 'Print project/task ARI'
14
14
  end
15
15
 
16
16
  def perform
17
- if project_id.nil?
18
- cli.warn 'No project selected'
19
- elsif task_id.nil?
20
- cli.print_ari('harvest', project_id)
21
- else
22
- cli.print_ari('harvest', "#{project_id}/#{task_id}")
23
- end
17
+ require_project!
18
+
19
+ cli.print_ari('harvest', path)
24
20
  end
25
21
  end
26
22
  end
@@ -14,21 +14,21 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- cli.abort 'No running time entry' if time_entry.nil?
17
+ abort 'No running time entry' if time_entry.nil?
18
18
 
19
19
  stop_time_entry
20
20
 
21
- cli.warn 'Harvest time entry stopped'
21
+ warn 'Harvest time entry stopped'
22
22
  print_task(project, task)
23
- rescue Abt::HttpError::HttpError => e
24
- cli.warn e
25
- cli.abort 'Unable to stop time entry'
26
23
  end
27
24
 
28
25
  private
29
26
 
30
27
  def stop_time_entry
31
28
  api.patch("time_entries/#{time_entry['id']}/stop")
29
+ rescue Abt::HttpError::HttpError => e
30
+ warn e
31
+ abort 'Unable to stop time entry'
32
32
  end
33
33
 
34
34
  def project
@@ -47,8 +47,8 @@ module Abt
47
47
  user_id: config.user_id
48
48
  ).first
49
49
  rescue Abt::HttpError::HttpError => e # rubocop:disable Layout/RescueEnsureAlignment
50
- cli.warn e
51
- cli.abort 'Unable to fetch running time entry'
50
+ warn e
51
+ abort 'Unable to fetch running time entry'
52
52
  end
53
53
  end
54
54
  end
@@ -28,7 +28,10 @@ module Abt
28
28
  end
29
29
 
30
30
  def tasks
31
- @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
31
+ @tasks ||= begin
32
+ warn 'Fetching tasks...'
33
+ project_assignment['task_assignments'].map { |ta| ta['task'] }
34
+ end
32
35
  end
33
36
 
34
37
  def project_assignment
@@ -29,7 +29,7 @@ module Abt
29
29
 
30
30
  maybe_override_current_task
31
31
  rescue Abt::HttpError::HttpError => _e
32
- cli.abort 'Invalid task'
32
+ abort 'Invalid task'
33
33
  end
34
34
 
35
35
  private
@@ -60,14 +60,14 @@ module Abt
60
60
  }
61
61
 
62
62
  if external_link_data
63
- cli.warn <<~TXT
63
+ warn <<~TXT
64
64
  Linking to:
65
65
  #{external_link_data[:notes]}
66
66
  #{external_link_data[:external_reference][:permalink]}
67
67
  TXT
68
68
  body.merge! external_link_data
69
69
  else
70
- cli.warn 'No external link provided'
70
+ warn 'No external link provided'
71
71
  end
72
72
 
73
73
  body[:notes] = flags[:comment] if flags.key?(:comment)
@@ -77,31 +77,38 @@ module Abt
77
77
 
78
78
  def external_link_data
79
79
  @external_link_data ||= begin
80
- input = StringIO.new(cli.aris.to_s)
81
- output = StringIO.new
82
- Abt::Cli.new(argv: ['harvest-time-entry-data'], output: output, input: input).perform
80
+ lines = call_harvest_time_entry_data_for_other_aris
83
81
 
84
- lines = output.string.strip.lines
82
+ if lines.empty?
83
+ nil
84
+ else
85
+ if lines.length > 1
86
+ abort('Got reference data from multiple scheme providers, only one is supported at a time')
87
+ end
85
88
 
86
- return if lines.empty?
87
-
88
- # TODO: Make user choose which reference to use by printing the urls
89
- if lines.length > 1
90
- cli.abort('Got reference data from multiple scheme providers, only one is supported at a time')
89
+ Oj.load(lines.first, symbol_keys: true)
91
90
  end
92
-
93
- Oj.load(lines.first, symbol_keys: true)
94
91
  end
95
92
  end
96
93
 
94
+ def call_harvest_time_entry_data_for_other_aris
95
+ other_aris = cli.aris - [ari]
96
+ return [] if other_aris.empty?
97
+
98
+ input = StringIO.new(other_aris.to_s)
99
+ output = StringIO.new
100
+ Abt::Cli.new(argv: ['harvest-time-entry-data'], output: output, input: input).perform
101
+
102
+ output.string.strip.lines
103
+ end
104
+
97
105
  def maybe_override_current_task
98
106
  return unless flags[:set]
99
- return if same_args_as_config?
107
+ return if path == config.path
100
108
  return unless config.local_available?
101
109
 
102
- input = StringIO.new("harvest:#{project_id}/#{task_id}")
103
- output = StringIO.new
104
- Abt::Cli.new(argv: ['current'], output: output, input: input).perform
110
+ config.path = path
111
+ warn 'Current task updated'
105
112
  end
106
113
  end
107
114
  end