abt-cli 0.0.19 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
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