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
@@ -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