abt-cli 0.0.8 → 0.0.13

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +0 -6
  3. data/lib/abt.rb +6 -0
  4. data/lib/abt/cli.rb +5 -5
  5. data/lib/abt/cli/dialogs.rb +36 -14
  6. data/lib/abt/git_config.rb +22 -5
  7. data/lib/abt/providers/asana/base_command.rb +11 -0
  8. data/lib/abt/providers/asana/commands/add.rb +75 -0
  9. data/lib/abt/providers/asana/commands/current.rb +3 -3
  10. data/lib/abt/providers/asana/commands/finalize.rb +1 -1
  11. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
  12. data/lib/abt/providers/asana/commands/init.rb +5 -0
  13. data/lib/abt/providers/asana/commands/pick.rb +17 -4
  14. data/lib/abt/providers/asana/commands/share.rb +3 -3
  15. data/lib/abt/providers/asana/commands/start.rb +1 -1
  16. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  17. data/lib/abt/providers/asana/configuration.rb +9 -3
  18. data/lib/abt/providers/devops.rb +19 -0
  19. data/lib/abt/providers/devops/api.rb +95 -0
  20. data/lib/abt/providers/devops/base_command.rb +98 -0
  21. data/lib/abt/providers/devops/commands/boards.rb +34 -0
  22. data/lib/abt/providers/devops/commands/clear.rb +24 -0
  23. data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
  24. data/lib/abt/providers/devops/commands/current.rb +93 -0
  25. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
  26. data/lib/abt/providers/devops/commands/init.rb +72 -0
  27. data/lib/abt/providers/devops/commands/pick.rb +78 -0
  28. data/lib/abt/providers/devops/commands/share.rb +26 -0
  29. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  30. data/lib/abt/providers/devops/configuration.rb +110 -0
  31. data/lib/abt/providers/harvest/base_command.rb +11 -0
  32. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  33. data/lib/abt/providers/harvest/commands/pick.rb +1 -0
  34. data/lib/abt/providers/harvest/commands/start.rb +10 -11
  35. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  36. data/lib/abt/providers/harvest/commands/track.rb +6 -4
  37. data/lib/abt/providers/harvest/configuration.rb +4 -3
  38. data/lib/abt/version.rb +1 -1
  39. metadata +16 -2
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Init < BaseCommand
8
+ AZURE_DEV_URL_REGEX = %r{^https://dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)}.freeze
9
+ VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
10
+
11
+ def self.command
12
+ 'init devops'
13
+ end
14
+
15
+ def self.description
16
+ 'Pick DevOps board for current git repository'
17
+ end
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
24
+
25
+ board = cli.prompt_choice 'Select a project work board', boards
26
+
27
+ config.board_id = board['id']
28
+
29
+ print_board(organization_name, project_name, board)
30
+ end
31
+
32
+ private
33
+
34
+ def boards
35
+ @boards ||= api.get_paged('work/boards')
36
+ end
37
+
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]
42
+ end
43
+ end
44
+
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]
49
+ end
50
+ end
51
+
52
+ def project_url
53
+ @project_url ||= begin
54
+ loop do
55
+ url = cli.prompt([
56
+ 'Please provide the URL for the devops project',
57
+ 'For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}',
58
+ '',
59
+ 'Enter URL'
60
+ ].join("\n"))
61
+
62
+ break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
63
+
64
+ cli.warn 'Invalid URL'
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Pick < BaseCommand
8
+ def self.command
9
+ 'pick devops[:<organization-name>/<project-name>/<board-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick work item for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+ require_board!
19
+
20
+ cli.warn "#{project_name} - #{board['name']}"
21
+
22
+ work_item = select_work_item
23
+
24
+ update_config!(work_item)
25
+
26
+ print_work_item(organization_name, project_name, board, work_item)
27
+ end
28
+
29
+ private
30
+
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
+ def select_work_item
39
+ loop do
40
+ column = cli.prompt_choice 'Which column?', columns
41
+ cli.warn 'Fetching work items...'
42
+ work_items = work_items_in_column(column)
43
+
44
+ if work_items.length.zero?
45
+ cli.warn 'Section is empty'
46
+ next
47
+ end
48
+
49
+ work_item = cli.prompt_choice 'Select a work item', work_items, true
50
+ return work_item if work_item
51
+ end
52
+ end
53
+
54
+ def work_items_in_column(column)
55
+ work_items = api.work_item_query(
56
+ <<~WIQL
57
+ SELECT [System.Id]
58
+ FROM WorkItems
59
+ WHERE [System.BoardColumn] = '#{column['name']}'
60
+ ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
61
+ WIQL
62
+ )
63
+
64
+ work_items.map { |work_item| sanitize_work_item(work_item) }
65
+ end
66
+
67
+ def columns
68
+ board['columns']
69
+ end
70
+
71
+ def board
72
+ @board ||= api.get("work/boards/#{board_id}")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Share < BaseCommand
8
+ def self.command
9
+ 'share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print DevOps config string'
14
+ end
15
+
16
+ def call
17
+ require_work_item!
18
+
19
+ args = [organization_name, project_name, board_id, work_item_id].compact
20
+ cli.print_provider_command('devops', args.join('/'))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class WorkItems < BaseCommand
8
+ def self.command
9
+ 'work-items devops'
10
+ end
11
+
12
+ def self.description
13
+ 'List all work items on board - useful for piping into grep etc.'
14
+ end
15
+
16
+ def call
17
+ require_board!
18
+
19
+ work_items.each do |work_item|
20
+ print_work_item(organization_name, project_name, board, work_item)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def work_items
27
+ @work_items ||= begin
28
+ cli.warn 'Fetching work items...'
29
+ api.work_item_query(
30
+ <<~WIQL
31
+ SELECT [System.Id]
32
+ FROM WorkItems
33
+ ORDER BY [System.Title] ASC
34
+ WIQL
35
+ ).map { |work_item| sanitize_work_item(work_item) }
36
+ end
37
+ end
38
+
39
+ def board
40
+ @board ||= api.get("work/boards/#{board_id}")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Configuration
7
+ attr_accessor :cli
8
+
9
+ def initialize(cli:)
10
+ @cli = cli
11
+ @git = GitConfig.new(namespace: 'abt.devops')
12
+ end
13
+
14
+ def local_available?
15
+ GitConfig.local_available?
16
+ end
17
+
18
+ def organization_name
19
+ local_available? ? git['organizationName'] : nil
20
+ end
21
+
22
+ def project_name
23
+ local_available? ? git['projectName'] : nil
24
+ end
25
+
26
+ def board_id
27
+ local_available? ? git['boardId'] : nil
28
+ end
29
+
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
74
+ end
75
+
76
+ def username_for_organization(organization_name)
77
+ username_key = "organizations.#{organization_name}.username"
78
+
79
+ return git.global[username_key] unless git.global[username_key].nil?
80
+
81
+ git.global[username_key] = cli.prompt([
82
+ "Please provide your username for the DevOps organization (#{organization_name}).",
83
+ '',
84
+ 'Enter username'
85
+ ].join("\n"))
86
+ end
87
+
88
+ def access_token_for_organization(organization_name)
89
+ access_token_key = "organizations.#{organization_name}.accessToken"
90
+
91
+ return git.global[access_token_key] unless git.global[access_token_key].nil?
92
+
93
+ git.global[access_token_key] = cli.prompt([
94
+ "Please provide your personal access token for the DevOps organization (#{organization_name}).",
95
+ '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
+ '',
97
+ 'The token MUST have "Read" permission for Work Items',
98
+ 'Future features will likely require "Write" or "Manage"',
99
+ '',
100
+ 'Enter access token'
101
+ ].join("\n"))
102
+ end
103
+
104
+ private
105
+
106
+ attr_reader :git
107
+ end
108
+ end
109
+ end
110
+ end
@@ -20,6 +20,17 @@ module Abt
20
20
 
21
21
  private
22
22
 
23
+ def require_project!
24
+ cli.abort 'No current/specified project. Did you initialize Harvest?' if project_id.nil?
25
+ end
26
+
27
+ def require_task!
28
+ if project_id.nil?
29
+ cli.abort 'No current/specified project. Did you initialize Harvest and pick a task?'
30
+ end
31
+ cli.abort 'No current/specified task. Did you pick a Harvest task?' if task_id.nil?
32
+ end
33
+
23
34
  def same_args_as_config?
24
35
  project_id == config.project_id && task_id == config.task_id
25
36
  end
@@ -14,6 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
+ require_project!
18
+
17
19
  if same_args_as_config? || !config.local_available?
18
20
  show_current_configuration
19
21
  else
@@ -25,9 +27,7 @@ module Abt
25
27
  private
26
28
 
27
29
  def show_current_configuration
28
- if project_id.nil?
29
- cli.warn 'No project selected'
30
- elsif task_id.nil?
30
+ if task_id.nil?
31
31
  print_project(project)
32
32
  else
33
33
  print_task(project, task)
@@ -15,6 +15,7 @@ module Abt
15
15
 
16
16
  def call
17
17
  cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+ require_project!
18
19
 
19
20
  cli.warn project['name']
20
21
  task = cli.prompt_choice 'Select a task', tasks
@@ -14,10 +14,10 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- start_output = call_start
18
- puts start_output
17
+ track_output = call_track
18
+ puts track_output
19
19
 
20
- use_arg_str(arg_str_from_start_output(start_output))
20
+ use_arg_str(arg_str_from_track_output(track_output))
21
21
 
22
22
  maybe_override_current_task
23
23
  rescue Abt::HttpError::HttpError => e
@@ -27,18 +27,17 @@ module Abt
27
27
 
28
28
  private
29
29
 
30
- def arg_str_from_start_output(output)
30
+ def arg_str_from_track_output(output)
31
31
  output = output.split(' # ').first
32
32
  output.split(':')[1]
33
33
  end
34
34
 
35
- def call_start
35
+ def call_track
36
+ input = StringIO.new(cli.args.join(' '))
36
37
  output = StringIO.new
37
- Abt::Cli.new(argv: ['track', *cli.args], output: output).perform
38
+ Abt::Cli.new(argv: ['track'], output: output, input: input).perform
38
39
 
39
- output_str = output.string.strip
40
- cli.abort 'No task provided' if output_str.empty?
41
- output_str
40
+ output.string.strip
42
41
  end
43
42
 
44
43
  def maybe_override_current_task
@@ -47,9 +46,9 @@ module Abt
47
46
  return unless config.local_available?
48
47
  return unless cli.prompt_boolean 'Set selected task as current?'
49
48
 
49
+ input = StringIO.new("harvest:#{project_id}/#{task_id}")
50
50
  output = StringIO.new
51
- Abt::Cli.new(argv: ['current', "harvest:#{project_id}/#{task_id}"],
52
- output: output).perform
51
+ Abt::Cli.new(argv: ['current'], output: output, input: input).perform
53
52
  end
54
53
  end
55
54
  end
@@ -14,6 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
+ require_project!
18
+
17
19
  tasks.each do |task|
18
20
  print_task(project, task)
19
21
  end