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 Devops
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
- def self.command
9
- 'harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
8
+ def self.usage
9
+ 'abt harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Print Harvest time entry data for DevOps work item as json. Used by harvest start script.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_work_item!
18
18
 
19
19
  body = {
@@ -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
@@ -8,24 +8,20 @@ module Abt
8
8
  AZURE_DEV_URL_REGEX = %r{^https://dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)}.freeze
9
9
  VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
10
10
 
11
- def self.command
12
- 'init devops'
11
+ def self.usage
12
+ 'abt init devops'
13
13
  end
14
14
 
15
15
  def self.description
16
16
  'Pick DevOps board for current git repository'
17
17
  end
18
18
 
19
- def call
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
19
+ def perform
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
@@ -5,44 +5,44 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
- def self.command
9
- 'pick devops[:<organization-name>/<project-name>/<board-id>]'
8
+ def self.usage
9
+ 'abt pick devops[:<organization-name>/<project-name>/<board-id>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Pick work item 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 self.flags
17
+ [
18
+ ['-d', '--dry-run', 'Keep existing configuration']
19
+ ]
20
+ end
21
+
22
+ def perform
23
+ abort 'Must be run inside a git repository' unless config.local_available?
18
24
  require_board!
19
25
 
20
- cli.warn "#{project_name} - #{board['name']}"
26
+ warn "#{project_name} - #{board['name']}"
21
27
 
22
28
  work_item = select_work_item
29
+ print_work_item(organization_name, project_name, board, work_item)
23
30
 
24
- update_config!(work_item)
31
+ return if flags[:"dry-run"]
25
32
 
26
- print_work_item(organization_name, project_name, board, work_item)
33
+ config.path = Path.from_ids(organization_name, project_name, board_id, work_item['id'])
27
34
  end
28
35
 
29
36
  private
30
37
 
31
- def update_config!(work_item)
32
- config.organization_name = organization_name
33
- config.project_name = project_name
34
- config.board_id = board_id
35
- config.work_item_id = work_item['id']
36
- end
37
-
38
38
  def select_work_item
39
39
  loop do
40
40
  column = cli.prompt.choice 'Which column?', columns
41
- cli.warn 'Fetching work items...'
41
+ warn 'Fetching work items...'
42
42
  work_items = work_items_in_column(column)
43
43
 
44
44
  if work_items.length.zero?
45
- cli.warn 'Section is empty'
45
+ warn 'Section is empty'
46
46
  next
47
47
  end
48
48
 
@@ -5,19 +5,18 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class Share < BaseCommand
8
- def self.command
9
- 'share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
8
+ def self.usage
9
+ 'abt share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
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
- def call
17
- require_work_item!
16
+ def perform
17
+ require_board!
18
18
 
19
- args = [organization_name, project_name, board_id, work_item_id].compact
20
- cli.print_provider_command('devops', args.join('/'))
19
+ cli.print_ari('devops', path)
21
20
  end
22
21
  end
23
22
  end
@@ -5,15 +5,15 @@ module Abt
5
5
  module Devops
6
6
  module Commands
7
7
  class WorkItems < BaseCommand
8
- def self.command
9
- 'work-items devops'
8
+ def self.usage
9
+ 'abt work-items devops'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'List all work items on board - useful for piping into grep etc.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_board!
18
18
 
19
19
  work_items.each do |work_item|
@@ -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,77 +8,34 @@ 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
21
+ def path=(new_path)
22
+ git['path'] = new_path
24
23
  end
25
24
 
26
- def board_id
27
- local_available? ? git['boardId'] : nil
25
+ def clear_local(verbose: true)
26
+ git.clear(output: verbose ? cli.err_output : nil)
28
27
  end
29
28
 
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
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
58
- end
59
-
60
- def clear_local
61
- cli.abort 'No local configuration was found' unless local_available?
62
-
63
- git['organizationName'] = nil
64
- git['projectName'] = nil
65
- git['boardId'] = nil
66
- git['workItemId'] = nil
67
- end
68
-
69
- def clear_global
70
- git.global.keys.each do |key|
71
- cli.puts 'Deleting configuration: ' + key
72
- git.global[key] = nil
73
- end
29
+ def clear_global(verbose: true)
30
+ git_global.clear(output: verbose ? cli.err_output : nil)
74
31
  end
75
32
 
76
33
  def username_for_organization(organization_name)
77
34
  username_key = "organizations.#{organization_name}.username"
78
35
 
79
- return git.global[username_key] unless git.global[username_key].nil?
36
+ return git_global[username_key] unless git_global[username_key].nil?
80
37
 
81
- git.global[username_key] = cli.prompt.text([
38
+ git_global[username_key] = cli.prompt.text([
82
39
  "Please provide your username for the DevOps organization (#{organization_name}).",
83
40
  '',
84
41
  'Enter username'
@@ -88,9 +45,9 @@ module Abt
88
45
  def access_token_for_organization(organization_name)
89
46
  access_token_key = "organizations.#{organization_name}.accessToken"
90
47
 
91
- 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?
92
49
 
93
- git.global[access_token_key] = cli.prompt.text([
50
+ git_global[access_token_key] = cli.prompt.text([
94
51
  "Please provide your personal access token for the DevOps organization (#{organization_name}).",
95
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',
96
53
  '',
@@ -103,7 +60,13 @@ module Abt
103
60
 
104
61
  private
105
62
 
106
- 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
107
70
  end
108
71
  end
109
72
  end
@@ -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