abt-cli 0.0.7 → 0.0.12

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 +1 -0
  3. data/lib/abt/cli.rb +5 -5
  4. data/lib/abt/cli/dialogs.rb +36 -14
  5. data/lib/abt/docs.rb +4 -0
  6. data/lib/abt/git_config.rb +13 -0
  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 +77 -0
  20. data/lib/abt/providers/devops/base_command.rb +97 -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 +21 -65
  35. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  36. data/lib/abt/providers/harvest/commands/track.rb +72 -0
  37. data/lib/abt/providers/harvest/configuration.rb +4 -3
  38. data/lib/abt/version.rb +1 -1
  39. metadata +17 -2
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Api
7
+ VERBS = %i[get post put].freeze
8
+
9
+ attr_reader :organization_name, :project_name, :username, :access_token
10
+
11
+ def initialize(organization_name:, project_name:, username:, access_token:)
12
+ @organization_name = organization_name
13
+ @project_name = project_name
14
+ @username = username
15
+ @access_token = access_token
16
+ end
17
+
18
+ VERBS.each do |verb|
19
+ define_method(verb) do |*args|
20
+ request(verb, *args)
21
+ end
22
+ end
23
+
24
+ def get_paged(path, query = {})
25
+ result = request(:get, path, query)
26
+ result['value']
27
+
28
+ # TODO: Loop if necessary
29
+ end
30
+
31
+ def work_item_query(wiql)
32
+ response = post('wit/wiql', Oj.dump({ query: wiql }, mode: :json))
33
+ ids = response['workItems'].map { |work_item| work_item['id'] }
34
+
35
+ work_items = []
36
+ ids.each_slice(200) do |page_ids|
37
+ work_items += get_paged('wit/workitems', ids: page_ids.join(','))
38
+ end
39
+
40
+ work_items
41
+ end
42
+
43
+ def request(*args)
44
+ response = connection.public_send(*args)
45
+
46
+ if response.success?
47
+ Oj.load(response.body)
48
+ else
49
+ error_class = Abt::HttpError.error_class_for_status(response.status)
50
+ encoded_response_body = response.body.force_encoding('utf-8')
51
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
52
+ end
53
+ end
54
+
55
+ def base_url
56
+ "https://#{organization_name}.visualstudio.com/#{project_name}"
57
+ end
58
+
59
+ def api_endpoint
60
+ "#{base_url}/_apis"
61
+ end
62
+
63
+ def url_for_work_item(work_item)
64
+ "#{base_url}/_workitems/edit/#{work_item['id']}"
65
+ end
66
+
67
+ def connection
68
+ @connection ||= Faraday.new(api_endpoint) do |connection|
69
+ connection.basic_auth username, access_token
70
+ connection.headers['Content-Type'] = 'application/json'
71
+ connection.headers['Accept'] = 'application/json; api-version=6.0'
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class BaseCommand
7
+ attr_reader :arg_str, :organization_name, :project_name, :board_id, :work_item_id, :cli, :config
8
+
9
+ def initialize(arg_str:, cli:)
10
+ @arg_str = arg_str
11
+
12
+ @config = Configuration.new(cli: cli)
13
+ @cli = cli
14
+
15
+ if arg_str.nil?
16
+ use_current_args
17
+ else
18
+ use_arg_str(arg_str)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def require_board!
25
+ return if organization_name && project_name && board_id
26
+
27
+ cli.abort 'No current/specified board. Did you initialize DevOps?'
28
+ end
29
+
30
+ def require_work_item!
31
+ unless organization_name && project_name && board_id
32
+ cli.abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
33
+ end
34
+
35
+ return if work_item_id
36
+
37
+ cli.abort 'No current/specified work item. Did you pick a DevOps work item?'
38
+ end
39
+
40
+ def sanitize_work_item(work_item)
41
+ return nil if work_item.nil?
42
+
43
+ work_item.merge(
44
+ 'id' => work_item['id'].to_s,
45
+ 'name' => work_item['fields']['System.Title'],
46
+ 'url' => api.url_for_work_item(work_item)
47
+ )
48
+ end
49
+
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
+ def print_board(organization_name, project_name, board)
58
+ arg_str = "#{organization_name}/#{project_name}/#{board['id']}"
59
+
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
62
+ end
63
+
64
+ 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
84
+
85
+ (@organization_name, @project_name, @board_id, @work_item_id) = args
86
+ end
87
+
88
+ def api
89
+ Abt::Providers::Devops::Api.new(organization_name: organization_name,
90
+ project_name: project_name,
91
+ username: config.username_for_organization(organization_name),
92
+ access_token: config.access_token_for_organization(organization_name))
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Boards < BaseCommand
8
+ def self.command
9
+ 'boards devops'
10
+ end
11
+
12
+ def self.description
13
+ 'List all boards - useful for piping into grep etc'
14
+ end
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?
19
+
20
+ boards.map do |board|
21
+ print_board(organization_name, project_name, board)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def boards
28
+ @boards ||= api.get_paged('work/boards')
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear devops'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear DevOps config for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing configuration'
18
+ config.clear_local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global devops'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing global DevOps configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set DevOps configuration for current git repository'
14
+ end
15
+
16
+ def call
17
+ require_board!
18
+
19
+ if same_args_as_config? || !config.local_available?
20
+ show_current_configuration
21
+ else
22
+ cli.warn 'Updating configuration'
23
+ update_configuration
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def show_current_configuration
30
+ if work_item_id.nil?
31
+ print_board(organization_name, project_name, board)
32
+ else
33
+ print_work_item(organization_name, project_name, board, work_item)
34
+ end
35
+ end
36
+
37
+ def update_configuration
38
+ ensure_board_is_valid!
39
+
40
+ if work_item_id.nil?
41
+ update_board_config
42
+ config.work_item_id = nil
43
+
44
+ print_board(organization_name, project_name, board)
45
+ else
46
+ ensure_work_item_is_valid!
47
+
48
+ update_board_config
49
+ config.work_item_id = work_item_id
50
+
51
+ print_work_item(organization_name, project_name, board, work_item)
52
+ end
53
+ end
54
+
55
+ def update_board_config
56
+ config.organization_name = organization_name
57
+ config.project_name = project_name
58
+ config.board_id = board_id
59
+ end
60
+
61
+ def ensure_board_is_valid!
62
+ if board.nil?
63
+ cli.abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
64
+ end
65
+ end
66
+
67
+ def ensure_work_item_is_valid!
68
+ cli.abort "No such work item: ##{work_item_id}" if work_item.nil?
69
+ end
70
+
71
+ def board
72
+ @board ||= begin
73
+ cli.warn 'Fetching board...'
74
+ api.get("work/boards/#{board_id}")
75
+ rescue HttpError::NotFoundError
76
+ nil
77
+ end
78
+ end
79
+
80
+ def work_item
81
+ @work_item ||= begin
82
+ cli.warn 'Fetching work item...'
83
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
84
+ sanitize_work_item(work_item)
85
+ rescue HttpError::NotFoundError
86
+ nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class HarvestTimeEntryData < BaseCommand
8
+ def self.command
9
+ 'harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print Harvest time entry data for DevOps work item as json. Used by harvest start script.'
14
+ end
15
+
16
+ def call
17
+ require_work_item!
18
+
19
+ body = {
20
+ notes: notes,
21
+ external_reference: {
22
+ id: work_item['id'],
23
+ group_id: 'AzureDevOpsWorkItem',
24
+ permalink: work_item['url']
25
+ }
26
+ }
27
+
28
+ cli.puts Oj.dump(body, mode: :json)
29
+ end
30
+
31
+ private
32
+
33
+ def notes
34
+ [
35
+ 'Azure DevOps',
36
+ work_item['fields']['System.WorkItemType'],
37
+ "##{work_item['id']}",
38
+ '-',
39
+ work_item['name']
40
+ ].join(' ')
41
+ end
42
+
43
+ def work_item
44
+ @work_item ||= begin
45
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
46
+ sanitize_work_item(work_item)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -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