abt-cli 0.0.19 → 0.0.24

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -3
  3. data/lib/abt.rb +6 -6
  4. data/lib/abt/ari.rb +2 -2
  5. data/lib/abt/ari_list.rb +1 -1
  6. data/lib/abt/base_command.rb +7 -7
  7. data/lib/abt/cli.rb +49 -47
  8. data/lib/abt/cli/arguments_parser.rb +6 -3
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +23 -0
  11. data/lib/abt/cli/global_commands/examples.rb +23 -0
  12. data/lib/abt/cli/global_commands/help.rb +23 -0
  13. data/lib/abt/cli/global_commands/readme.rb +23 -0
  14. data/lib/abt/cli/global_commands/share.rb +36 -0
  15. data/lib/abt/cli/global_commands/version.rb +23 -0
  16. data/lib/abt/cli/prompt.rb +64 -52
  17. data/lib/abt/docs.rb +48 -26
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +10 -7
  20. data/lib/abt/git_config.rb +4 -6
  21. data/lib/abt/helpers.rb +26 -8
  22. data/lib/abt/providers/asana/api.rb +9 -9
  23. data/lib/abt/providers/asana/base_command.rb +12 -10
  24. data/lib/abt/providers/asana/commands/add.rb +13 -12
  25. data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +7 -8
  27. data/lib/abt/providers/asana/commands/current.rb +14 -15
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -18
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +22 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +7 -5
  34. data/lib/abt/providers/asana/commands/start.rb +28 -21
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -6
  36. data/lib/abt/providers/asana/configuration.rb +37 -29
  37. data/lib/abt/providers/asana/path.rb +6 -6
  38. data/lib/abt/providers/devops/api.rb +12 -12
  39. data/lib/abt/providers/devops/base_command.rb +14 -10
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
  42. data/lib/abt/providers/devops/commands/clear.rb +7 -8
  43. data/lib/abt/providers/devops/commands/current.rb +17 -18
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
  45. data/lib/abt/providers/devops/commands/init.rb +21 -14
  46. data/lib/abt/providers/devops/commands/pick.rb +25 -19
  47. data/lib/abt/providers/devops/commands/share.rb +7 -5
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +15 -15
  50. data/lib/abt/providers/devops/path.rb +7 -6
  51. data/lib/abt/providers/git/commands/branch.rb +23 -21
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +10 -8
  54. data/lib/abt/providers/harvest/commands/clear.rb +7 -8
  55. data/lib/abt/providers/harvest/commands/current.rb +13 -14
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -11
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +7 -5
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +12 -12
  62. data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
  63. data/lib/abt/providers/harvest/commands/track.rb +52 -37
  64. data/lib/abt/providers/harvest/configuration.rb +18 -18
  65. data/lib/abt/providers/harvest/path.rb +6 -6
  66. data/lib/abt/version.rb +1 -1
  67. metadata +12 -5
@@ -4,15 +4,15 @@ module Abt
4
4
  module Providers
5
5
  module Asana
6
6
  class Path < String
7
- PATH_REGEX = %r{^(?<project_gid>\d+)?(/(?<task_gid>\d+))?$}.freeze
7
+ PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
8
8
 
9
- def self.from_ids(project_gid = nil, task_gid = nil)
10
- path = project_gid ? [project_gid, *task_gid].join('/') : ''
11
- new path
9
+ def self.from_ids(project_gid: nil, task_gid: nil)
10
+ path = project_gid ? [project_gid, *task_gid].join("/") : ""
11
+ new(path)
12
12
  end
13
13
 
14
- def initialize(path = '')
15
- raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
14
+ def initialize(path = "")
15
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
16
16
 
17
17
  super
18
18
  end
@@ -4,9 +4,9 @@ module Abt
4
4
  module Providers
5
5
  module Devops
6
6
  class Api
7
- VERBS = %i[get post put].freeze
7
+ VERBS = [:get, :post, :put].freeze
8
8
 
9
- CONDITIONAL_ACCESS_POLICY_ERROR_CODE = 'VS403463'
9
+ CONDITIONAL_ACCESS_POLICY_ERROR_CODE = "VS403463"
10
10
 
11
11
  attr_reader :organization_name, :project_name, :username, :access_token, :cli
12
12
 
@@ -26,18 +26,18 @@ module Abt
26
26
 
27
27
  def get_paged(path, query = {})
28
28
  result = request(:get, path, query)
29
- result['value']
29
+ result["value"]
30
30
 
31
31
  # TODO: Loop if necessary
32
32
  end
33
33
 
34
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'] }
35
+ response = post("wit/wiql", Oj.dump({ query: wiql }, mode: :json))
36
+ ids = response["workItems"].map { |work_item| work_item["id"] }
37
37
 
38
38
  work_items = []
39
39
  ids.each_slice(200) do |page_ids|
40
- work_items += get_paged('wit/workitems', ids: page_ids.join(','))
40
+ work_items += get_paged("wit/workitems", ids: page_ids.join(","))
41
41
  end
42
42
 
43
43
  work_items
@@ -50,7 +50,7 @@ module Abt
50
50
  Oj.load(response.body)
51
51
  else
52
52
  error_class = Abt::HttpError.error_class_for_status(response.status)
53
- encoded_response_body = response.body.force_encoding('utf-8')
53
+ encoded_response_body = response.body.force_encoding("utf-8")
54
54
  raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
55
55
  end
56
56
  rescue Abt::HttpError::ForbiddenError => e
@@ -75,9 +75,9 @@ module Abt
75
75
 
76
76
  def connection
77
77
  @connection ||= Faraday.new(api_endpoint) do |connection|
78
- connection.basic_auth username, access_token
79
- connection.headers['Content-Type'] = 'application/json'
80
- connection.headers['Accept'] = 'application/json; api-version=6.0'
78
+ connection.basic_auth(username, access_token)
79
+ connection.headers["Content-Type"] = "application/json"
80
+ connection.headers["Accept"] = "application/json; api-version=6.0"
81
81
  end
82
82
  end
83
83
 
@@ -87,14 +87,14 @@ module Abt
87
87
  # https://apidock.com/ruby/ERB/Util/url_encode
88
88
  def rfc_3986_encode_path_segment(string)
89
89
  string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
90
- format('%%%02X', match.unpack1('C')) # rubocop:disable Style/FormatStringToken
90
+ format("%%%02X", match.unpack1("C"))
91
91
  end
92
92
  end
93
93
 
94
94
  def handle_denied_by_conditional_access_policy!(exception)
95
95
  raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
96
96
 
97
- cli.abort <<~TXT
97
+ cli.abort(<<~TXT)
98
98
  Access denied by conditional access policy.
99
99
  Try logging into the board using the URL below, then retry the command.
100
100
 
@@ -19,44 +19,48 @@ module Abt
19
19
 
20
20
  private
21
21
 
22
+ def require_local_config!
23
+ abort("Must be run inside a git repository") unless config.local_available?
24
+ end
25
+
22
26
  def require_board!
23
27
  return if organization_name && project_name && board_id
24
28
 
25
- abort 'No current/specified board. Did you initialize DevOps?'
29
+ abort("No current/specified board. Did you initialize DevOps?")
26
30
  end
27
31
 
28
32
  def require_work_item!
29
33
  unless organization_name && project_name && board_id
30
- abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
34
+ abort("No current/specified board. Did you initialize DevOps and pick a work item?")
31
35
  end
32
36
 
33
37
  return if work_item_id
34
38
 
35
- abort 'No current/specified work item. Did you pick a DevOps work item?'
39
+ abort("No current/specified work item. Did you pick a DevOps work item?")
36
40
  end
37
41
 
38
42
  def sanitize_work_item(work_item)
39
43
  return nil if work_item.nil?
40
44
 
41
45
  work_item.merge(
42
- 'id' => work_item['id'].to_s,
43
- 'name' => work_item['fields']['System.Title'],
44
- 'url' => api.url_for_work_item(work_item)
46
+ "id" => work_item["id"].to_s,
47
+ "name" => work_item["fields"]["System.Title"],
48
+ "url" => api.url_for_work_item(work_item)
45
49
  )
46
50
  end
47
51
 
48
52
  def print_board(organization_name, project_name, board)
49
53
  path = "#{organization_name}/#{project_name}/#{board['id']}"
50
54
 
51
- cli.print_ari('devops', path, board['name'])
52
- warn api.url_for_board(board) if cli.output.isatty
55
+ cli.print_ari("devops", path, board["name"])
56
+ warn(api.url_for_board(board)) if cli.output.isatty
53
57
  end
54
58
 
55
59
  def print_work_item(organization, project, board, work_item)
56
60
  path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
57
61
 
58
- cli.print_ari('devops', path, work_item['name'])
59
- warn work_item['url'] if work_item.key?('url') && cli.output.isatty
62
+ cli.print_ari("devops", path, work_item["name"])
63
+ warn(work_item["url"]) if work_item.key?("url") && cli.output.isatty
60
64
  end
61
65
 
62
66
  def api
@@ -6,18 +6,16 @@ module Abt
6
6
  module Commands
7
7
  class Boards < BaseCommand
8
8
  def self.usage
9
- 'abt boards devops'
9
+ "abt boards devops"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'List all boards - useful for piping into grep etc'
13
+ "List all boards - useful for piping into grep etc"
14
14
  end
15
15
 
16
16
  def perform
17
- if organization_name.nil?
18
- abort 'No organization selected. Did you initialize DevOps?'
19
- end
20
- abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
17
+ abort("No organization selected. Did you initialize DevOps?") if organization_name.nil?
18
+ abort("No project selected. Did you initialize DevOps?") if project_name.nil?
21
19
 
22
20
  boards.map do |board|
23
21
  print_board(organization_name, project_name, board)
@@ -27,7 +25,7 @@ module Abt
27
25
  private
28
26
 
29
27
  def boards
30
- @boards ||= api.get_paged('work/boards')
28
+ @boards ||= api.get_paged("work/boards")
31
29
  end
32
30
  end
33
31
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class BranchName < BaseCommand
8
8
  def self.usage
9
- 'abt branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
9
+ "abt branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Suggest a git branch name for the current/specified work-item.'
13
+ "Suggest a git branch name for the current/specified work-item."
14
14
  end
15
15
 
16
16
  def perform
@@ -21,24 +21,24 @@ module Abt
21
21
  args = [organization_name, project_name, board_id, work_item_id].compact
22
22
 
23
23
  error_message = [
24
- 'Unable to find work item for configuration:',
24
+ "Unable to find work item for configuration:",
25
25
  "devops:#{args.join('/')}"
26
26
  ].join("\n")
27
- abort error_message
27
+ abort(error_message)
28
28
  end
29
29
 
30
30
  private
31
31
 
32
32
  def name
33
- str = work_item['id']
34
- str += '-'
35
- str += work_item['name'].downcase.gsub(/[^\w]/, '-')
36
- str.gsub(/-+/, '-')
33
+ str = work_item["id"]
34
+ str += "-"
35
+ str += work_item["name"].downcase.gsub(/[^\w]/, "-")
36
+ str.squeeze("-").gsub(/(^-|-$)/, "")
37
37
  end
38
38
 
39
39
  def work_item
40
40
  @work_item ||= begin
41
- work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
41
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
42
42
  sanitize_work_item(work_item)
43
43
  end
44
44
  end
@@ -6,29 +6,28 @@ module Abt
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
8
  def self.usage
9
- 'abt clear devops'
9
+ "abt clear devops"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear DevOps configuration'
13
+ "Clear DevOps configuration"
14
14
  end
15
15
 
16
16
  def self.flags
17
17
  [
18
- ['-g', '--global', 'Clear global instead of local DevOp configuration (credentials etc.)'],
19
- ['-a', '--all', 'Clear all DevOp configuration']
18
+ ["-g", "--global",
19
+ "Clear global instead of local DevOp configuration (credentials etc.)"],
20
+ ["-a", "--all", "Clear all DevOp configuration"]
20
21
  ]
21
22
  end
22
23
 
23
24
  def perform
24
- if flags[:global] && flags[:all]
25
- abort('Flags --global and --all cannot be used at the same time')
26
- end
25
+ abort("Flags --global and --all cannot be used at the same time") if flags[:global] && flags[:all]
27
26
 
28
27
  config.clear_local unless flags[:global]
29
28
  config.clear_global if flags[:global] || flags[:all]
30
29
 
31
- warn 'Configuration cleared'
30
+ warn("Configuration cleared")
32
31
  end
33
32
  end
34
33
  end
@@ -6,22 +6,21 @@ module Abt
6
6
  module Commands
7
7
  class Current < BaseCommand
8
8
  def self.usage
9
- 'abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
9
+ "abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Get or set DevOps configuration for current git repository'
13
+ "Get or set DevOps configuration for current git repository"
14
14
  end
15
15
 
16
16
  def perform
17
- abort 'Must be run inside a git repository' unless config.local_available?
18
-
17
+ require_local_config!
19
18
  require_board!
20
19
  ensure_valid_configuration!
21
20
 
22
21
  if path != config.path && config.local_available?
23
22
  config.path = path
24
- warn 'Configuration updated'
23
+ warn("Configuration updated")
25
24
  end
26
25
 
27
26
  print_configuration
@@ -39,28 +38,28 @@ module Abt
39
38
 
40
39
  def ensure_valid_configuration!
41
40
  if board.nil?
42
- abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
41
+ abort("Board could not be found, ensure that settings for organization, project, and board are correct")
43
42
  end
44
- abort "No such work item: ##{work_item_id}" if work_item_id && work_item.nil?
43
+ abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
45
44
  end
46
45
 
47
46
  def board
48
47
  @board ||= begin
49
- warn 'Fetching board...'
50
- api.get("work/boards/#{board_id}")
51
- rescue HttpError::NotFoundError
52
- nil
53
- end
48
+ warn("Fetching board...")
49
+ api.get("work/boards/#{board_id}")
50
+ rescue HttpError::NotFoundError
51
+ nil
52
+ end
54
53
  end
55
54
 
56
55
  def work_item
57
56
  @work_item ||= begin
58
- warn 'Fetching work item...'
59
- work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
60
- sanitize_work_item(work_item)
61
- rescue HttpError::NotFoundError
62
- nil
63
- end
57
+ warn("Fetching work item...")
58
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
59
+ sanitize_work_item(work_item)
60
+ rescue HttpError::NotFoundError
61
+ nil
62
+ end
64
63
  end
65
64
  end
66
65
  end
@@ -6,51 +6,53 @@ module Abt
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
8
  def self.usage
9
- 'abt harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
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
- 'Print Harvest time entry data for DevOps work item as json. Used by harvest start script.'
13
+ "Print Harvest time entry data for DevOps work item as json. Used by harvest start script."
14
14
  end
15
15
 
16
16
  def perform
17
17
  require_work_item!
18
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
19
  puts Oj.dump(body, mode: :json)
29
20
  rescue HttpError::NotFoundError
30
21
  args = [organization_name, project_name, board_id, work_item_id].compact
31
22
 
32
23
  error_message = [
33
- 'Unable to find work item for configuration:',
24
+ "Unable to find work item for configuration:",
34
25
  "devops:#{args.join('/')}"
35
26
  ].join("\n")
36
- abort error_message
27
+ abort(error_message)
37
28
  end
38
29
 
39
30
  private
40
31
 
32
+ def body
33
+ {
34
+ notes: notes,
35
+ external_reference: {
36
+ id: work_item["id"],
37
+ group_id: "AzureDevOpsWorkItem",
38
+ permalink: work_item["url"]
39
+ }
40
+ }
41
+ end
42
+
41
43
  def notes
42
44
  [
43
- 'Azure DevOps',
44
- work_item['fields']['System.WorkItemType'],
45
+ "Azure DevOps",
46
+ work_item["fields"]["System.WorkItemType"],
45
47
  "##{work_item['id']}",
46
- '-',
47
- work_item['name']
48
- ].join(' ')
48
+ "-",
49
+ work_item["name"]
50
+ ].join(" ")
49
51
  end
50
52
 
51
53
  def work_item
52
54
  @work_item ||= begin
53
- work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
55
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
54
56
  sanitize_work_item(work_item)
55
57
  end
56
58
  end
@@ -9,26 +9,29 @@ module Abt
9
9
  VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
10
10
 
11
11
  def self.usage
12
- 'abt init devops'
12
+ "abt init devops"
13
13
  end
14
14
 
15
15
  def self.description
16
- 'Pick DevOps board for current git repository'
16
+ "Pick DevOps board for current git repository"
17
17
  end
18
18
 
19
19
  def perform
20
- abort 'Must be run inside a git repository' unless config.local_available?
20
+ require_local_config!
21
+ board = cli.prompt.choice("Select a project work board", boards)
21
22
 
22
- board = cli.prompt.choice 'Select a project work board', boards
23
-
24
- config.path = Path.from_ids(organization_name, project_name, board['id'])
23
+ config.path = Path.from_ids(
24
+ organization_name: organization_name,
25
+ project_name: project_name,
26
+ board_id: board["id"]
27
+ )
25
28
  print_board(organization_name, project_name, board)
26
29
  end
27
30
 
28
31
  private
29
32
 
30
33
  def boards
31
- @boards ||= api.get_paged('work/boards')
34
+ @boards ||= api.get_paged("work/boards")
32
35
  end
33
36
 
34
37
  def project_name
@@ -52,19 +55,23 @@ module Abt
52
55
  def project_url
53
56
  @project_url ||= begin
54
57
  loop do
55
- url = cli.prompt.text([
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"))
58
+ url = cli.prompt.text(project_url_prompt_text)
61
59
 
62
60
  break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
63
61
 
64
- warn 'Invalid URL'
62
+ warn("Invalid URL")
65
63
  end
66
64
  end
67
65
  end
66
+
67
+ def project_url_prompt_text
68
+ <<~TXT
69
+ Please provide the URL for the devops project
70
+ For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
71
+
72
+ Enter URL
73
+ TXT
74
+ end
68
75
  end
69
76
  end
70
77
  end