abt-cli 0.0.9 → 0.0.14

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +0 -6
  3. data/lib/abt.rb +8 -0
  4. data/lib/abt/cli.rb +5 -5
  5. data/lib/abt/cli/dialogs.rb +28 -9
  6. data/lib/abt/git_config.rb +28 -11
  7. data/lib/abt/helpers.rb +1 -1
  8. data/lib/abt/providers/asana/base_command.rb +11 -0
  9. data/lib/abt/providers/asana/commands/add.rb +75 -0
  10. data/lib/abt/providers/asana/commands/branch-name.rb +44 -0
  11. data/lib/abt/providers/asana/commands/current.rb +3 -3
  12. data/lib/abt/providers/asana/commands/finalize.rb +1 -1
  13. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
  14. data/lib/abt/providers/asana/commands/init.rb +5 -0
  15. data/lib/abt/providers/asana/commands/pick.rb +1 -0
  16. data/lib/abt/providers/asana/commands/share.rb +3 -3
  17. data/lib/abt/providers/asana/commands/start.rb +1 -1
  18. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  19. data/lib/abt/providers/asana/configuration.rb +4 -2
  20. data/lib/abt/providers/devops.rb +19 -0
  21. data/lib/abt/providers/devops/api.rb +95 -0
  22. data/lib/abt/providers/devops/base_command.rb +98 -0
  23. data/lib/abt/providers/devops/commands/boards.rb +34 -0
  24. data/lib/abt/providers/devops/commands/branch-name.rb +45 -0
  25. data/lib/abt/providers/devops/commands/clear.rb +24 -0
  26. data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
  27. data/lib/abt/providers/devops/commands/current.rb +93 -0
  28. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
  29. data/lib/abt/providers/devops/commands/init.rb +72 -0
  30. data/lib/abt/providers/devops/commands/pick.rb +78 -0
  31. data/lib/abt/providers/devops/commands/share.rb +26 -0
  32. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  33. data/lib/abt/providers/devops/configuration.rb +110 -0
  34. data/lib/abt/providers/git.rb +19 -0
  35. data/lib/abt/providers/git/commands/branch.rb +80 -0
  36. data/lib/abt/providers/harvest/base_command.rb +11 -0
  37. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  38. data/lib/abt/providers/harvest/commands/pick.rb +1 -0
  39. data/lib/abt/providers/harvest/commands/start.rb +10 -11
  40. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  41. data/lib/abt/providers/harvest/commands/track.rb +6 -4
  42. data/lib/abt/providers/harvest/configuration.rb +4 -3
  43. data/lib/abt/version.rb +1 -1
  44. metadata +20 -2
@@ -14,9 +14,9 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- if project_gid.nil?
18
- cli.warn 'No project selected'
19
- elsif task_gid.nil?
17
+ require_project!
18
+
19
+ if task_gid.nil?
20
20
  cli.print_provider_command('asana', project_gid)
21
21
  else
22
22
  cli.print_provider_command('asana', "#{project_gid}/#{task_gid}")
@@ -14,7 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- abort 'No current/provided task' if task_gid.nil?
17
+ require_task!
18
18
 
19
19
  maybe_override_current_task
20
20
 
@@ -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
@@ -67,8 +67,10 @@ module Abt
67
67
  end
68
68
 
69
69
  def clear_global
70
- git.global['workspaceGid'] = nil
71
- git.global['accessToken'] = nil
70
+ git.global.keys.each do |key|
71
+ cli.puts 'Deleting configuration: ' + key
72
+ git.global[key] = nil
73
+ end
72
74
  end
73
75
 
74
76
  def access_token
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
4
+ Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
5
+
6
+ module Abt
7
+ module Providers
8
+ module Devops
9
+ def self.command_names
10
+ Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
11
+ end
12
+
13
+ def self.command_class(name)
14
+ const_name = Helpers.command_to_const(name)
15
+ Commands.const_get(const_name) if Commands.const_defined?(const_name)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,95 @@
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
+ CONDITIONAL_ACCESS_POLICY_ERROR_CODE = 'VS403463'
10
+
11
+ attr_reader :organization_name, :project_name, :username, :access_token, :cli
12
+
13
+ def initialize(organization_name:, project_name:, username:, access_token:, cli:)
14
+ @organization_name = organization_name
15
+ @project_name = project_name
16
+ @username = username
17
+ @access_token = access_token
18
+ @cli = cli
19
+ end
20
+
21
+ VERBS.each do |verb|
22
+ define_method(verb) do |*args|
23
+ request(verb, *args)
24
+ end
25
+ end
26
+
27
+ def get_paged(path, query = {})
28
+ result = request(:get, path, query)
29
+ result['value']
30
+
31
+ # TODO: Loop if necessary
32
+ end
33
+
34
+ def work_item_query(wiql)
35
+ response = post('wit/wiql', Oj.dump({ query: wiql }, mode: :json))
36
+ ids = response['workItems'].map { |work_item| work_item['id'] }
37
+
38
+ work_items = []
39
+ ids.each_slice(200) do |page_ids|
40
+ work_items += get_paged('wit/workitems', ids: page_ids.join(','))
41
+ end
42
+
43
+ work_items
44
+ end
45
+
46
+ def request(*args)
47
+ response = connection.public_send(*args)
48
+
49
+ if response.success?
50
+ Oj.load(response.body)
51
+ else
52
+ error_class = Abt::HttpError.error_class_for_status(response.status)
53
+ encoded_response_body = response.body.force_encoding('utf-8')
54
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
55
+ end
56
+ rescue Abt::HttpError::ForbiddenError => e
57
+ handle_denied_by_conditional_access_policy!(e)
58
+ end
59
+
60
+ def base_url
61
+ "https://#{organization_name}.visualstudio.com/#{project_name}"
62
+ end
63
+
64
+ def api_endpoint
65
+ "#{base_url}/_apis"
66
+ end
67
+
68
+ def url_for_work_item(work_item)
69
+ "#{base_url}/_workitems/edit/#{work_item['id']}"
70
+ end
71
+
72
+ def connection
73
+ @connection ||= Faraday.new(api_endpoint) do |connection|
74
+ connection.basic_auth username, access_token
75
+ connection.headers['Content-Type'] = 'application/json'
76
+ connection.headers['Accept'] = 'application/json; api-version=6.0'
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def handle_denied_by_conditional_access_policy!(exception)
83
+ raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
84
+
85
+ cli.abort <<~TXT
86
+ Access denied by conditional access policy.
87
+ Try logging into the board using the URL below, then retry the command.
88
+
89
+ #{base_url}
90
+ TXT
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,98 @@
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
+ cli: cli)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ 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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class BranchName < BaseCommand
8
+ def self.command
9
+ 'branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Suggest a git branch name for the current/specified work-item.'
14
+ end
15
+
16
+ def call
17
+ require_work_item!
18
+
19
+ cli.puts name
20
+ rescue HttpError::NotFoundError
21
+ args = [organization_name, project_name, board_id, work_item_id].compact
22
+ cli.warn 'Unable to find work item for configuration:'
23
+ cli.abort "devops:#{args.join('/')}"
24
+ end
25
+
26
+ private
27
+
28
+ def name
29
+ str = work_item['id']
30
+ str += '-'
31
+ str += work_item['name'].downcase.gsub(/[^\w]/, '-')
32
+ str.gsub(/-+/, '-')
33
+ end
34
+
35
+ def work_item
36
+ @work_item ||= begin
37
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
38
+ sanitize_work_item(work_item)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ 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