abt-cli 0.0.21 → 0.0.22

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 (66) 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 +1 -1
  5. data/lib/abt/ari_list.rb +1 -1
  6. data/lib/abt/base_command.rb +7 -7
  7. data/lib/abt/cli.rb +27 -40
  8. data/lib/abt/cli/arguments_parser.rb +5 -9
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +2 -2
  11. data/lib/abt/cli/global_commands/examples.rb +2 -2
  12. data/lib/abt/cli/global_commands/help.rb +2 -2
  13. data/lib/abt/cli/global_commands/readme.rb +2 -2
  14. data/lib/abt/cli/global_commands/share.rb +6 -6
  15. data/lib/abt/cli/global_commands/version.rb +2 -2
  16. data/lib/abt/cli/prompt.rb +51 -20
  17. data/lib/abt/docs.rb +39 -33
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +5 -5
  20. data/lib/abt/git_config.rb +4 -6
  21. data/lib/abt/providers/asana/api.rb +9 -9
  22. data/lib/abt/providers/asana/base_command.rb +8 -10
  23. data/lib/abt/providers/asana/commands/add.rb +13 -12
  24. data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
  25. data/lib/abt/providers/asana/commands/clear.rb +7 -8
  26. data/lib/abt/providers/asana/commands/current.rb +14 -14
  27. data/lib/abt/providers/asana/commands/finalize.rb +11 -12
  28. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +11 -11
  29. data/lib/abt/providers/asana/commands/init.rb +8 -41
  30. data/lib/abt/providers/asana/commands/pick.rb +17 -17
  31. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  32. data/lib/abt/providers/asana/commands/share.rb +5 -5
  33. data/lib/abt/providers/asana/commands/start.rb +21 -20
  34. data/lib/abt/providers/asana/commands/tasks.rb +6 -6
  35. data/lib/abt/providers/asana/configuration.rb +25 -25
  36. data/lib/abt/providers/asana/path.rb +5 -5
  37. data/lib/abt/providers/devops/api.rb +12 -12
  38. data/lib/abt/providers/devops/base_command.rb +10 -10
  39. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  40. data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
  41. data/lib/abt/providers/devops/commands/clear.rb +7 -8
  42. data/lib/abt/providers/devops/commands/current.rb +17 -17
  43. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +13 -13
  44. data/lib/abt/providers/devops/commands/init.rb +17 -13
  45. data/lib/abt/providers/devops/commands/pick.rb +11 -11
  46. data/lib/abt/providers/devops/commands/share.rb +5 -5
  47. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  48. data/lib/abt/providers/devops/configuration.rb +19 -15
  49. data/lib/abt/providers/devops/path.rb +5 -4
  50. data/lib/abt/providers/git/commands/branch.rb +17 -19
  51. data/lib/abt/providers/harvest/api.rb +8 -8
  52. data/lib/abt/providers/harvest/base_command.rb +6 -8
  53. data/lib/abt/providers/harvest/commands/clear.rb +7 -8
  54. data/lib/abt/providers/harvest/commands/current.rb +13 -13
  55. data/lib/abt/providers/harvest/commands/init.rb +10 -38
  56. data/lib/abt/providers/harvest/commands/pick.rb +11 -11
  57. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  58. data/lib/abt/providers/harvest/commands/share.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  60. data/lib/abt/providers/harvest/commands/stop.rb +12 -12
  61. data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
  62. data/lib/abt/providers/harvest/commands/track.rb +21 -20
  63. data/lib/abt/providers/harvest/configuration.rb +18 -18
  64. data/lib/abt/providers/harvest/path.rb +5 -5
  65. data/lib/abt/version.rb +1 -1
  66. metadata +6 -5
@@ -6,11 +6,11 @@ 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
@@ -19,9 +19,9 @@ module Abt
19
19
  body = {
20
20
  notes: notes,
21
21
  external_reference: {
22
- id: work_item['id'],
23
- group_id: 'AzureDevOpsWorkItem',
24
- permalink: work_item['url']
22
+ id: work_item["id"],
23
+ group_id: "AzureDevOpsWorkItem",
24
+ permalink: work_item["url"]
25
25
  }
26
26
  }
27
27
 
@@ -30,27 +30,27 @@ module Abt
30
30
  args = [organization_name, project_name, board_id, work_item_id].compact
31
31
 
32
32
  error_message = [
33
- 'Unable to find work item for configuration:',
33
+ "Unable to find work item for configuration:",
34
34
  "devops:#{args.join('/')}"
35
35
  ].join("\n")
36
- abort error_message
36
+ abort(error_message)
37
37
  end
38
38
 
39
39
  private
40
40
 
41
41
  def notes
42
42
  [
43
- 'Azure DevOps',
44
- work_item['fields']['System.WorkItemType'],
43
+ "Azure DevOps",
44
+ work_item["fields"]["System.WorkItemType"],
45
45
  "##{work_item['id']}",
46
- '-',
47
- work_item['name']
48
- ].join(' ')
46
+ "-",
47
+ work_item["name"]
48
+ ].join(" ")
49
49
  end
50
50
 
51
51
  def work_item
52
52
  @work_item ||= begin
53
- work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
53
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
54
54
  sanitize_work_item(work_item)
55
55
  end
56
56
  end
@@ -9,26 +9,26 @@ 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
+ abort("Must be run inside a git repository") unless config.local_available?
21
21
 
22
- board = cli.prompt.choice 'Select a project work board', boards
22
+ board = cli.prompt.choice("Select a project work board", boards)
23
23
 
24
- config.path = Path.from_ids(organization_name, project_name, board['id'])
24
+ config.path = Path.from_ids(organization_name, project_name, board["id"])
25
25
  print_board(organization_name, project_name, board)
26
26
  end
27
27
 
28
28
  private
29
29
 
30
30
  def boards
31
- @boards ||= api.get_paged('work/boards')
31
+ @boards ||= api.get_paged("work/boards")
32
32
  end
33
33
 
34
34
  def project_name
@@ -52,19 +52,23 @@ module Abt
52
52
  def project_url
53
53
  @project_url ||= begin
54
54
  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"))
55
+ url = cli.prompt.text(project_url_prompt_text)
61
56
 
62
57
  break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
63
58
 
64
- warn 'Invalid URL'
59
+ warn("Invalid URL")
65
60
  end
66
61
  end
67
62
  end
63
+
64
+ def project_url_prompt_text
65
+ <<~TXT
66
+ Please provide the URL for the devops project
67
+ For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
68
+
69
+ Enter URL
70
+ TXT
71
+ end
68
72
  end
69
73
  end
70
74
  end
@@ -6,47 +6,47 @@ module Abt
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
8
  def self.usage
9
- 'abt pick devops[:<organization-name>/<project-name>/<board-id>]'
9
+ "abt pick devops[:<organization-name>/<project-name>/<board-id>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Pick work item for current git repository'
13
+ "Pick work item for current git repository"
14
14
  end
15
15
 
16
16
  def self.flags
17
17
  [
18
- ['-d', '--dry-run', 'Keep existing configuration']
18
+ ["-d", "--dry-run", "Keep existing configuration"]
19
19
  ]
20
20
  end
21
21
 
22
22
  def perform
23
- abort 'Must be run inside a git repository' unless config.local_available?
23
+ abort("Must be run inside a git repository") unless config.local_available?
24
24
  require_board!
25
25
 
26
- warn "#{project_name} - #{board['name']}"
26
+ warn("#{project_name} - #{board['name']}")
27
27
 
28
28
  work_item = select_work_item
29
29
  print_work_item(organization_name, project_name, board, work_item)
30
30
 
31
31
  return if flags[:"dry-run"]
32
32
 
33
- config.path = Path.from_ids(organization_name, project_name, board_id, work_item['id'])
33
+ config.path = Path.from_ids(organization_name, project_name, board_id, work_item["id"])
34
34
  end
35
35
 
36
36
  private
37
37
 
38
38
  def select_work_item
39
39
  loop do
40
- column = cli.prompt.choice 'Which column?', columns
41
- warn 'Fetching work items...'
40
+ column = cli.prompt.choice("Which column?", columns)
41
+ warn("Fetching work items...")
42
42
  work_items = work_items_in_column(column)
43
43
 
44
44
  if work_items.length.zero?
45
- warn 'Section is empty'
45
+ warn("Section is empty")
46
46
  next
47
47
  end
48
48
 
49
- work_item = cli.prompt.choice 'Select a work item', work_items, true
49
+ work_item = cli.prompt.choice("Select a work item", work_items, nil_option: true)
50
50
  return work_item if work_item
51
51
  end
52
52
  end
@@ -65,7 +65,7 @@ module Abt
65
65
  end
66
66
 
67
67
  def columns
68
- board['columns']
68
+ board["columns"]
69
69
  end
70
70
 
71
71
  def board
@@ -6,18 +6,18 @@ module Abt
6
6
  module Commands
7
7
  class Share < BaseCommand
8
8
  def self.usage
9
- 'abt share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
9
+ "abt share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print DevOps ARI'
13
+ "Print DevOps ARI"
14
14
  end
15
15
 
16
16
  def perform
17
- if path != ''
18
- cli.print_ari('devops', path)
17
+ if path != ""
18
+ cli.print_ari("devops", path)
19
19
  elsif cli.output.isatty
20
- warn 'No configuration for project. Did you initialize DevOps?'
20
+ warn("No configuration for project. Did you initialize DevOps?")
21
21
  end
22
22
  end
23
23
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class WorkItems < BaseCommand
8
8
  def self.usage
9
- 'abt work-items devops'
9
+ "abt work-items devops"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'List all work items on board - useful for piping into grep etc.'
13
+ "List all work items on board - useful for piping into grep etc."
14
14
  end
15
15
 
16
16
  def perform
@@ -25,7 +25,7 @@ module Abt
25
25
 
26
26
  def work_items
27
27
  @work_items ||= begin
28
- warn 'Fetching work items...'
28
+ warn("Fetching work items...")
29
29
  api.work_item_query(
30
30
  <<~WIQL
31
31
  SELECT [System.Id]
@@ -15,11 +15,11 @@ module Abt
15
15
  end
16
16
 
17
17
  def path
18
- Path.new(local_available? && git['path'] || '')
18
+ Path.new(local_available? && git["path"] || "")
19
19
  end
20
20
 
21
21
  def path=(new_path)
22
- git['path'] = new_path
22
+ git["path"] = new_path
23
23
  end
24
24
 
25
25
  def clear_local(verbose: true)
@@ -37,8 +37,8 @@ module Abt
37
37
 
38
38
  git_global[username_key] = cli.prompt.text([
39
39
  "Please provide your username for the DevOps organization (#{organization_name}).",
40
- '',
41
- 'Enter username'
40
+ "",
41
+ "Enter username"
42
42
  ].join("\n"))
43
43
  end
44
44
 
@@ -47,25 +47,29 @@ module Abt
47
47
 
48
48
  return git_global[access_token_key] unless git_global[access_token_key].nil?
49
49
 
50
- git_global[access_token_key] = cli.prompt.text([
51
- "Please provide your personal access token for the DevOps organization (#{organization_name}).",
52
- 'If you don\'t have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate',
53
- '',
54
- 'The token MUST have "Read" permission for Work Items',
55
- 'Future features will likely require "Write" or "Manage"',
56
- '',
57
- 'Enter access token'
58
- ].join("\n"))
50
+ git_global[access_token_key] = cli.prompt.text(access_token_prompt_text)
59
51
  end
60
52
 
61
53
  private
62
54
 
63
55
  def git
64
- @git ||= GitConfig.new('local', 'abt.devops')
56
+ @git ||= GitConfig.new("local", "abt.devops")
65
57
  end
66
58
 
67
59
  def git_global
68
- @git_global ||= GitConfig.new('global', 'abt.devops')
60
+ @git_global ||= GitConfig.new("global", "abt.devops")
61
+ end
62
+
63
+ def access_token_prompt_text
64
+ <<~TXT
65
+ Please provide your personal access token for the DevOps organization (#{organization_name}).
66
+ If you don't have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate
67
+
68
+ The token MUST have "Read" permission for Work Items
69
+ Future features will likely require "Write" or "Manage
70
+
71
+ Enter access token"
72
+ TXT
69
73
  end
70
74
  end
71
75
  end
@@ -9,16 +9,17 @@ module Abt
9
9
  BOARD_ID_REGEX = /(?<board_id>[a-z0-9\-]+)/.freeze
10
10
  WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
11
11
 
12
- PATH_REGEX = %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX})?(/#{WORK_ITEM_ID_REGEX})?}.freeze
12
+ PATH_REGEX =
13
+ %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX})?(/#{WORK_ITEM_ID_REGEX})?}.freeze
13
14
 
14
15
  def self.from_ids(organization_id = nil, project_name = nil, board_id = nil, work_item_id = nil)
15
16
  return new unless organization_id && project_name && board_id
16
17
 
17
- new [organization_id, project_name, board_id, *work_item_id].join('/')
18
+ new([organization_id, project_name, board_id, *work_item_id].join("/"))
18
19
  end
19
20
 
20
- def initialize(path = '')
21
- raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
21
+ def initialize(path = "")
22
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
22
23
 
23
24
  super
24
25
  end
@@ -6,16 +6,16 @@ module Abt
6
6
  module Commands
7
7
  class Branch < Abt::BaseCommand
8
8
  def self.usage
9
- 'abt branch git <scheme>[:<path>]'
9
+ "abt branch git <scheme>[:<path>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Switch branch. Uses a compatible scheme to generate the branch-name: E.g. `abt branch git asana`'
13
+ "Switch branch. Uses a compatible scheme to generate the branch-name: E.g. `abt branch git asana`"
14
14
  end
15
15
 
16
16
  def perform
17
17
  switch || create_and_switch
18
- warn "Switched to #{branch_name}"
18
+ warn("Switched to #{branch_name}")
19
19
  end
20
20
 
21
21
  private
@@ -29,8 +29,8 @@ module Abt
29
29
  end
30
30
 
31
31
  def create_and_switch
32
- warn "No such branch: #{branch_name}"
33
- abort('Aborting') unless cli.prompt.boolean 'Create branch?'
32
+ warn("No such branch: #{branch_name}")
33
+ abort("Aborting") unless cli.prompt.boolean("Create branch?")
34
34
 
35
35
  Open3.popen3("git switch -c #{branch_name}") do |_i, _o, _e, thread|
36
36
  thread.value
@@ -40,20 +40,20 @@ module Abt
40
40
  def branch_name # rubocop:disable Metrics/MethodLength
41
41
  @branch_name ||= begin
42
42
  if branch_names_from_aris.empty?
43
- abort [
44
- 'None of the specified ARIs responded to `branch-name`.',
45
- 'Did you add compatible scheme? e.g.:',
46
- ' abt branch git asana',
47
- ' abt branch git devops'
48
- ].join("\n")
43
+ abort([
44
+ "None of the specified ARIs responded to `branch-name`.",
45
+ "Did you add compatible scheme? e.g.:",
46
+ " abt branch git asana",
47
+ " abt branch git devops"
48
+ ].join("\n"))
49
49
  end
50
50
 
51
51
  if branch_names_from_aris.length > 1
52
- abort [
53
- 'Got branch names from multiple ARIs, only one is supported',
54
- 'Branch names were:',
52
+ abort([
53
+ "Got branch names from multiple ARIs, only one is supported",
54
+ "Branch names were:",
55
55
  *branch_names_from_aris.map { |name| " #{name}" }
56
- ].join("\n")
56
+ ].join("\n"))
57
57
  end
58
58
 
59
59
  branch_names_from_aris.first
@@ -63,13 +63,11 @@ module Abt
63
63
  def branch_names_from_aris
64
64
  other_aris = cli.aris - [ari]
65
65
 
66
- if other_aris.empty?
67
- abort 'You must provide an additional ARI that responds to: branch-name. E.g., asana'
68
- end
66
+ abort("You must provide an additional ARI that responds to: branch-name. E.g., asana") if other_aris.empty?
69
67
 
70
68
  input = StringIO.new(cli.aris.to_s)
71
69
  output = StringIO.new
72
- Abt::Cli.new(argv: ['branch-name'], output: output, input: input).perform
70
+ Abt::Cli.new(argv: ["branch-name"], output: output, input: input).perform
73
71
 
74
72
  output.string.lines.map(&:strip).compact
75
73
  end
@@ -4,8 +4,8 @@ module Abt
4
4
  module Providers
5
5
  module Harvest
6
6
  class Api
7
- API_ENDPOINT = 'https://api.harvestapp.com/v2'
8
- VERBS = %i[get post patch].freeze
7
+ API_ENDPOINT = "https://api.harvestapp.com/v2"
8
+ VERBS = [:get, :post, :patch].freeze
9
9
 
10
10
  attr_reader :access_token, :account_id
11
11
 
@@ -21,7 +21,7 @@ module Abt
21
21
  end
22
22
 
23
23
  def get_paged(path, query = {})
24
- result_key = path.split('?').first.split('/').last
24
+ result_key = path.split("?").first.split("/").last
25
25
 
26
26
  page = 1
27
27
  records = []
@@ -29,7 +29,7 @@ module Abt
29
29
  loop do
30
30
  result = get(path, query.merge(page: page))
31
31
  records += result[result_key]
32
- break if result['total_pages'] == page
32
+ break if result["total_pages"] == page
33
33
 
34
34
  page += 1
35
35
  end
@@ -44,16 +44,16 @@ module Abt
44
44
  Oj.load(response.body)
45
45
  else
46
46
  error_class = Abt::HttpError.error_class_for_status(response.status)
47
- encoded_response_body = response.body.force_encoding('utf-8')
47
+ encoded_response_body = response.body.force_encoding("utf-8")
48
48
  raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
49
49
  end
50
50
  end
51
51
 
52
52
  def connection
53
53
  @connection ||= Faraday.new(API_ENDPOINT) do |connection|
54
- connection.headers['Authorization'] = "Bearer #{access_token}"
55
- connection.headers['Harvest-Account-Id'] = account_id
56
- connection.headers['Content-Type'] = 'application/json'
54
+ connection.headers["Authorization"] = "Bearer #{access_token}"
55
+ connection.headers["Harvest-Account-Id"] = account_id
56
+ connection.headers["Content-Type"] = "application/json"
57
57
  end
58
58
  end
59
59
  end