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
@@ -6,48 +6,54 @@ 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
+ require_local_config!
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
+ update_config(work_item)
34
34
  end
35
35
 
36
36
  private
37
37
 
38
+ def update_config(work_item)
39
+ config.path = Path.from_ids(
40
+ organization_name: organization_name,
41
+ project_name: project_name,
42
+ board_id: board_id,
43
+ work_item_id: work_item["id"]
44
+ )
45
+ end
46
+
38
47
  def select_work_item
39
- loop do
40
- column = cli.prompt.choice 'Which column?', columns
41
- warn 'Fetching work items...'
42
- work_items = work_items_in_column(column)
43
-
44
- if work_items.length.zero?
45
- warn 'Section is empty'
46
- next
47
- end
48
-
49
- work_item = cli.prompt.choice 'Select a work item', work_items, true
50
- return work_item if work_item
48
+ column = cli.prompt.choice("Which column?", columns)
49
+ warn("Fetching work items...")
50
+ work_items = work_items_in_column(column)
51
+
52
+ if work_items.length.zero?
53
+ warn("Section is empty")
54
+ select_work_item
55
+ else
56
+ cli.prompt.choice("Select a work item", work_items, nil_option: true) || select_work_item
51
57
  end
52
58
  end
53
59
 
@@ -65,7 +71,7 @@ module Abt
65
71
  end
66
72
 
67
73
  def columns
68
- board['columns']
74
+ board["columns"]
69
75
  end
70
76
 
71
77
  def board
@@ -6,17 +6,19 @@ 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
- require_board!
18
-
19
- cli.print_ari('devops', path)
17
+ if path != ""
18
+ cli.print_ari("devops", path)
19
+ elsif cli.output.isatty
20
+ warn("No configuration for project. Did you initialize DevOps?")
21
+ end
20
22
  end
21
23
  end
22
24
  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,25 @@ 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(<<~TXT)
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
+ TXT
59
59
  end
60
60
 
61
61
  private
62
62
 
63
63
  def git
64
- @git ||= GitConfig.new('local', 'abt.devops')
64
+ @git ||= GitConfig.new("local", "abt.devops")
65
65
  end
66
66
 
67
67
  def git_global
68
- @git_global ||= GitConfig.new('global', 'abt.devops')
68
+ @git_global ||= GitConfig.new("global", "abt.devops")
69
69
  end
70
70
  end
71
71
  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
- def self.from_ids(organization_id = nil, project_name = nil, board_id = nil, work_item_id = nil)
15
- return new unless organization_id && project_name && board_id
15
+ def self.from_ids(organization_name: nil, project_name: nil, board_id: nil, work_item_id: nil)
16
+ return new unless organization_name && project_name && board_id
16
17
 
17
- new [organization_id, project_name, board_id, *work_item_id].join('/')
18
+ new([organization_name, 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
@@ -61,17 +61,19 @@ module Abt
61
61
  end
62
62
 
63
63
  def branch_names_from_aris
64
- other_aris = cli.aris - [ari]
64
+ return @branch_names_from_aris if instance_variable_defined?(:@branch_names_from_aris)
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
71
+
72
+ @branch_names_from_aris = output.string.lines.map(&:strip).compact
73
+ end
73
74
 
74
- output.string.lines.map(&:strip).compact
75
+ def other_aris
76
+ @other_aris ||= cli.aris - [ari]
75
77
  end
76
78
  end
77
79
  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
@@ -19,31 +19,33 @@ 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_project!
23
27
  return if project_id
24
28
 
25
- abort 'No current/specified project. Did you initialize Harvest?'
29
+ abort("No current/specified project. Did you initialize Harvest?")
26
30
  end
27
31
 
28
32
  def require_task!
29
- unless project_id
30
- abort 'No current/specified project. Did you initialize Harvest and pick a task?'
31
- end
33
+ abort("No current/specified project. Did you initialize Harvest and pick a task?") unless project_id
32
34
 
33
- abort 'No current/specified task. Did you pick a Harvest task?' if task_id.nil?
35
+ abort("No current/specified task. Did you pick a Harvest task?") if task_id.nil?
34
36
  end
35
37
 
36
38
  def print_project(project)
37
39
  cli.print_ari(
38
- 'harvest',
39
- project['id'],
40
+ "harvest",
41
+ project["id"],
40
42
  "#{project['client']['name']} > #{project['name']}"
41
43
  )
42
44
  end
43
45
 
44
46
  def print_task(project, task)
45
47
  cli.print_ari(
46
- 'harvest',
48
+ "harvest",
47
49
  "#{project['id']}/#{task['id']}",
48
50
  "#{project['name']} > #{task['name']}"
49
51
  )
@@ -6,29 +6,28 @@ module Abt
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
8
  def self.usage
9
- 'abt clear harvest'
9
+ "abt clear harvest"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear harvest configuration'
13
+ "Clear harvest configuration"
14
14
  end
15
15
 
16
16
  def self.flags
17
17
  [
18
- ['-g', '--global', 'Clear global instead of local harvest configuration (credentials etc.)'],
19
- ['-a', '--all', 'Clear all harvest configuration']
18
+ ["-g", "--global",
19
+ "Clear global instead of local harvest configuration (credentials etc.)"],
20
+ ["-a", "--all", "Clear all harvest 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 harvest[:<project-id>[/<task-id>]]'
9
+ "abt current harvest[:<project-id>[/<task-id>]]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Get or set project and or task for current git repository'
13
+ "Get or set project and or task 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_project!
20
19
  ensure_valid_configuration!
21
20
 
22
21
  if path != config.path
23
22
  config.path = path
24
- warn 'Configuration updated'
23
+ warn("Configuration updated")
25
24
  end
26
25
 
27
26
  print_configuration
@@ -38,36 +37,36 @@ module Abt
38
37
  end
39
38
 
40
39
  def ensure_valid_configuration!
41
- abort "Invalid project: #{project_id}" if project.nil?
42
- abort "Invalid task: #{task_id}" if task_id && task.nil?
40
+ abort("Invalid project: #{project_id}") if project.nil?
41
+ abort("Invalid task: #{task_id}") if task_id && task.nil?
43
42
  end
44
43
 
45
44
  def project
46
- return @project if instance_variable_defined? :@project
45
+ return @project if instance_variable_defined?(:@project)
47
46
 
48
47
  @project = if project_assignment
49
- project_assignment['project'].merge('client' => project_assignment['client'])
48
+ project_assignment["project"].merge("client" => project_assignment["client"])
50
49
  end
51
50
  end
52
51
 
53
52
  def task
54
- return @task if instance_variable_defined? :@task
53
+ return @task if instance_variable_defined?(:@task)
55
54
 
56
55
  @task = if project_assignment
57
- project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
58
- task['id'].to_s == task_id
56
+ project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
57
+ task["id"].to_s == task_id
59
58
  end
60
59
  end
61
60
  end
62
61
 
63
62
  def project_assignment
64
63
  @project_assignment ||= begin
65
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
64
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
66
65
  end
67
66
  end
68
67
 
69
68
  def project_assignments
70
- @project_assignments ||= api.get_paged('users/me/project_assignments')
69
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
71
70
  end
72
71
  end
73
72
  end