abt-cli 0.0.21 → 0.0.22

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