abt-cli 0.0.10 → 0.0.15

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 (47) 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 +27 -11
  5. data/lib/abt/cli/prompt.rb +124 -0
  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 +7 -2
  15. data/lib/abt/providers/asana/commands/pick.rb +3 -2
  16. data/lib/abt/providers/asana/commands/share.rb +3 -3
  17. data/lib/abt/providers/asana/commands/start.rb +3 -3
  18. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  19. data/lib/abt/providers/asana/configuration.rb +7 -5
  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/init.rb +2 -2
  39. data/lib/abt/providers/harvest/commands/pick.rb +2 -1
  40. data/lib/abt/providers/harvest/commands/start.rb +2 -4
  41. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  42. data/lib/abt/providers/harvest/commands/track.rb +2 -3
  43. data/lib/abt/providers/harvest/configuration.rb +6 -5
  44. data/lib/abt/version.rb +1 -1
  45. metadata +21 -4
  46. data/lib/abt/cli/dialogs.rb +0 -86
  47. data/lib/abt/cli/io.rb +0 -23
@@ -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_gid.nil?
29
- cli.warn 'No project selected'
30
- elsif task_gid.nil?
30
+ if task_gid.nil?
31
31
  print_project(project)
32
32
  else
33
33
  print_task(project, task)
@@ -17,7 +17,7 @@ module Abt
17
17
  unless config.local_available?
18
18
  cli.abort 'This is a no-op for tasks outside the current project'
19
19
  end
20
- cli.abort 'No current or specified task' if task.nil?
20
+ require_task!
21
21
  print_task(project_gid, task)
22
22
 
23
23
  if task_already_in_finalized_section?
@@ -13,7 +13,8 @@ module Abt
13
13
  'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
14
14
  end
15
15
 
16
- def call # rubocop:disable Metrics/MethodLength
16
+ def call
17
+ require_task!
17
18
  ensure_current_is_valid!
18
19
 
19
20
  body = {
@@ -21,9 +22,7 @@ module Abt
21
22
  external_reference: {
22
23
  id: task_gid.to_i,
23
24
  group_id: project_gid.to_i,
24
- permalink: task['permalink_url'],
25
- service: 'app.asana.com',
26
- service_icon_url: 'https://proxy.harvestfiles.com/production_harvestapp_public/uploads/platform_icons/app.asana.com.png'
25
+ permalink: task['permalink_url']
27
26
  }
28
27
  }
29
28
 
@@ -13,6 +13,11 @@ module Abt
13
13
  'Pick Asana project for current git repository'
14
14
  end
15
15
 
16
+ def initialize(cli:, **)
17
+ @config = Configuration.new(cli: cli)
18
+ @cli = cli
19
+ end
20
+
16
21
  def call
17
22
  cli.abort 'Must be run inside a git repository' unless config.local_available?
18
23
 
@@ -30,14 +35,14 @@ module Abt
30
35
  cli.warn 'Select a project'
31
36
 
32
37
  loop do
33
- matches = matches_for_string cli.prompt('Enter search')
38
+ matches = matches_for_string cli.prompt.text('Enter search')
34
39
  if matches.empty?
35
40
  cli.warn 'No matches'
36
41
  next
37
42
  end
38
43
 
39
44
  cli.warn 'Showing the 10 first matches' if matches.size > 10
40
- choice = cli.prompt_choice 'Select a project', matches[0...10], true
45
+ choice = cli.prompt.choice 'Select a project', matches[0...10], true
41
46
  break choice unless choice.nil?
42
47
  end
43
48
  end
@@ -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
 
@@ -34,7 +35,7 @@ module Abt
34
35
 
35
36
  def select_task
36
37
  loop do
37
- section = cli.prompt_choice 'Which section?', sections
38
+ section = cli.prompt.choice 'Which section?', sections
38
39
  cli.warn 'Fetching tasks...'
39
40
  tasks = tasks_in_section(section)
40
41
 
@@ -43,7 +44,7 @@ module Abt
43
44
  next
44
45
  end
45
46
 
46
- task = cli.prompt_choice 'Select a task', tasks, true
47
+ task = cli.prompt.choice 'Select a task', tasks, true
47
48
  return task if task
48
49
  end
49
50
  end
@@ -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
 
@@ -29,7 +29,7 @@ module Abt
29
29
  return if same_args_as_config?
30
30
  return unless config.local_available?
31
31
 
32
- should_override = cli.prompt_boolean 'Set selected task as current?'
32
+ should_override = cli.prompt.boolean 'Set selected task as current?'
33
33
  Current.new(arg_str: arg_str, cli: cli).call if should_override
34
34
  end
35
35
 
@@ -41,7 +41,7 @@ module Abt
41
41
  update_assignee
42
42
  elsif current_assignee['gid'] == current_user['gid']
43
43
  cli.warn 'You are already assigned to this task'
44
- elsif cli.prompt_boolean "Task is assigned to: #{current_assignee['name']}, take over?"
44
+ elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
45
45
  cli.warn "Reassigning task to user: #{current_user['name']}"
46
46
  update_assignee
47
47
  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
@@ -67,14 +67,16 @@ 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
75
77
  return git.global['accessToken'] unless git.global['accessToken'].nil?
76
78
 
77
- git.global['accessToken'] = cli.prompt([
79
+ git.global['accessToken'] = cli.prompt.text([
78
80
  'Please provide your personal access token for Asana.',
79
81
  'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
80
82
  '',
@@ -101,7 +103,7 @@ module Abt
101
103
  def prompt_section(message)
102
104
  cli.warn 'Fetching sections...'
103
105
  sections = api.get_paged("projects/#{project_gid}/sections")
104
- cli.prompt_choice(message, sections)
106
+ cli.prompt.choice(message, sections)
105
107
  end
106
108
 
107
109
  def prompt_workspace
@@ -113,7 +115,7 @@ module Abt
113
115
  workspace = workspaces.first
114
116
  cli.warn "Selected Asana workspace #{workspace['name']}"
115
117
  else
116
- workspace = cli.prompt_choice('Select Asana workspace', workspaces)
118
+ workspace = cli.prompt.choice('Select Asana workspace', workspaces)
117
119
  end
118
120
 
119
121
  git.global['workspaceGid'] = workspace['gid']
@@ -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