abt-cli 0.0.17 → 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 (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 +20 -0
  5. data/lib/abt/ari_list.rb +13 -0
  6. data/lib/abt/base_command.rb +63 -0
  7. data/lib/abt/cli.rb +58 -59
  8. data/lib/abt/cli/arguments_parser.rb +8 -27
  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 +52 -20
  17. data/lib/abt/docs.rb +48 -25
  18. data/lib/abt/docs/cli.rb +7 -7
  19. data/lib/abt/docs/markdown.rb +13 -12
  20. data/lib/abt/git_config.rb +21 -39
  21. data/lib/abt/providers/asana/api.rb +9 -9
  22. data/lib/abt/providers/asana/base_command.rb +16 -38
  23. data/lib/abt/providers/asana/commands/add.rb +18 -15
  24. data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
  25. data/lib/abt/providers/asana/commands/clear.rb +8 -7
  26. data/lib/abt/providers/asana/commands/current.rb +23 -38
  27. data/lib/abt/providers/asana/commands/finalize.rb +11 -16
  28. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +14 -9
  29. data/lib/abt/providers/asana/commands/init.rb +8 -41
  30. data/lib/abt/providers/asana/commands/pick.rb +22 -17
  31. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  32. data/lib/abt/providers/asana/commands/share.rb +6 -8
  33. data/lib/abt/providers/asana/commands/start.rb +26 -23
  34. data/lib/abt/providers/asana/commands/tasks.rb +6 -5
  35. data/lib/abt/providers/asana/configuration.rb +34 -40
  36. data/lib/abt/providers/asana/path.rb +36 -0
  37. data/lib/abt/providers/devops/api.rb +23 -11
  38. data/lib/abt/providers/devops/base_command.rb +18 -43
  39. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  40. data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
  41. data/lib/abt/providers/devops/commands/clear.rb +8 -7
  42. data/lib/abt/providers/devops/commands/current.rb +25 -49
  43. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +20 -12
  44. data/lib/abt/providers/devops/commands/init.rb +29 -25
  45. data/lib/abt/providers/devops/commands/pick.rb +11 -18
  46. data/lib/abt/providers/devops/commands/share.rb +7 -6
  47. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  48. data/lib/abt/providers/devops/configuration.rb +31 -56
  49. data/lib/abt/providers/devops/path.rb +51 -0
  50. data/lib/abt/providers/git/commands/branch.rb +29 -25
  51. data/lib/abt/providers/harvest/api.rb +8 -8
  52. data/lib/abt/providers/harvest/base_command.rb +18 -38
  53. data/lib/abt/providers/harvest/commands/clear.rb +8 -7
  54. data/lib/abt/providers/harvest/commands/current.rb +28 -35
  55. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  56. data/lib/abt/providers/harvest/commands/pick.rb +11 -12
  57. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  58. data/lib/abt/providers/harvest/commands/share.rb +6 -8
  59. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  60. data/lib/abt/providers/harvest/commands/stop.rb +13 -13
  61. data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
  62. data/lib/abt/providers/harvest/commands/track.rb +40 -32
  63. data/lib/abt/providers/harvest/configuration.rb +28 -37
  64. data/lib/abt/providers/harvest/path.rb +36 -0
  65. data/lib/abt/version.rb +1 -1
  66. metadata +18 -6
  67. data/lib/abt/cli/base_command.rb +0 -61
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Path < String
7
+ ORGANIZATION_NAME_REGEX = %r{(?<organization_name>[^/ ]+)}.freeze
8
+ PROJECT_NAME_REGEX = %r{(?<project_name>[^/ ]+)}.freeze
9
+ BOARD_ID_REGEX = /(?<board_id>[a-z0-9\-]+)/.freeze
10
+ WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
11
+
12
+ PATH_REGEX =
13
+ %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX})?(/#{WORK_ITEM_ID_REGEX})?}.freeze
14
+
15
+ def self.from_ids(organization_id = nil, project_name = nil, board_id = nil, work_item_id = nil)
16
+ return new unless organization_id && project_name && board_id
17
+
18
+ new([organization_id, project_name, board_id, *work_item_id].join("/"))
19
+ end
20
+
21
+ def initialize(path = "")
22
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
23
+
24
+ super
25
+ end
26
+
27
+ def organization_name
28
+ match[:organization_name]
29
+ end
30
+
31
+ def project_name
32
+ match[:project_name]
33
+ end
34
+
35
+ def board_id
36
+ match[:board_id]
37
+ end
38
+
39
+ def work_item_id
40
+ match[:work_item_id]
41
+ end
42
+
43
+ private
44
+
45
+ def match
46
+ @match ||= PATH_REGEX.match(self)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -4,33 +4,33 @@ module Abt
4
4
  module Providers
5
5
  module Git
6
6
  module Commands
7
- class Branch < Abt::Cli::BaseCommand
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
- create_and_switch unless switch
18
- cli.warn "Switched to #{branch_name}"
17
+ switch || create_and_switch
18
+ warn("Switched to #{branch_name}")
19
19
  end
20
20
 
21
21
  private
22
22
 
23
23
  def switch
24
24
  success = false
25
- Open3.popen3("git switch #{branch_name}") do |_i, _o, _error_output, thread|
25
+ Open3.popen3("git switch #{branch_name}") do |_i, _o, _e, thread|
26
26
  success = thread.value.success?
27
27
  end
28
28
  success
29
29
  end
30
30
 
31
31
  def create_and_switch
32
- cli.warn "No such branch: #{branch_name}"
33
- cli.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
@@ -39,31 +39,35 @@ module Abt
39
39
 
40
40
  def branch_name # rubocop:disable Metrics/MethodLength
41
41
  @branch_name ||= begin
42
- if branch_names_from_scheme_arguments.empty?
43
- cli.abort [
44
- 'None of the specified scheme arguments 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")
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"))
49
49
  end
50
50
 
51
- if branch_names_from_scheme_arguments.length > 1
52
- cli.abort [
53
- 'Got branch names from multiple scheme arguments, only one is supported',
54
- 'Branch names were:',
55
- *branch_names_from_scheme_arguments.map { |name| " #{name}" }
56
- ].join("\n")
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:",
55
+ *branch_names_from_aris.map { |name| " #{name}" }
56
+ ].join("\n"))
57
57
  end
58
58
 
59
- branch_names_from_scheme_arguments.first
59
+ branch_names_from_aris.first
60
60
  end
61
61
  end
62
62
 
63
- def branch_names_from_scheme_arguments
64
- input = StringIO.new(cli.scheme_arguments.to_s)
63
+ def branch_names_from_aris
64
+ other_aris = cli.aris - [ari]
65
+
66
+ abort("You must provide an additional ARI that responds to: branch-name. E.g., asana") if other_aris.empty?
67
+
68
+ input = StringIO.new(cli.aris.to_s)
65
69
  output = StringIO.new
66
- Abt::Cli.new(argv: ['branch-name'], output: output, input: input).perform
70
+ Abt::Cli.new(argv: ["branch-name"], output: output, input: input).perform
67
71
 
68
72
  output.string.lines.map(&:strip).compact
69
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
@@ -3,70 +3,50 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Harvest
6
- class BaseCommand < Abt::Cli::BaseCommand
7
- attr_reader :path, :flags, :project_id, :task_id, :cli, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
8
 
9
- def initialize(path:, cli:, **)
9
+ attr_reader :config, :path
10
+
11
+ def_delegators(:@path, :project_id, :task_id)
12
+
13
+ def initialize(ari:, cli:)
10
14
  super
11
15
 
12
16
  @config = Configuration.new(cli: cli)
13
-
14
- if path.nil?
15
- use_current_path
16
- else
17
- use_path(path)
18
- end
17
+ @path = ari.path ? Path.new(ari.path) : config.path
19
18
  end
20
19
 
21
20
  private
22
21
 
23
22
  def require_project!
24
- cli.abort 'No current/specified project. Did you initialize Harvest?' if project_id.nil?
23
+ return if project_id
24
+
25
+ abort("No current/specified project. Did you initialize Harvest?")
25
26
  end
26
27
 
27
28
  def require_task!
28
- if project_id.nil?
29
- cli.abort 'No current/specified project. Did you initialize Harvest and pick a task?'
30
- end
31
- cli.abort 'No current/specified task. Did you pick a Harvest task?' if task_id.nil?
32
- end
29
+ abort("No current/specified project. Did you initialize Harvest and pick a task?") unless project_id
33
30
 
34
- def same_args_as_config?
35
- project_id == config.project_id && task_id == config.task_id
31
+ abort("No current/specified task. Did you pick a Harvest task?") if task_id.nil?
36
32
  end
37
33
 
38
34
  def print_project(project)
39
- cli.print_scheme_argument(
40
- 'harvest',
41
- project['id'],
35
+ cli.print_ari(
36
+ "harvest",
37
+ project["id"],
42
38
  "#{project['client']['name']} > #{project['name']}"
43
39
  )
44
40
  end
45
41
 
46
42
  def print_task(project, task)
47
- cli.print_scheme_argument(
48
- 'harvest',
43
+ cli.print_ari(
44
+ "harvest",
49
45
  "#{project['id']}/#{task['id']}",
50
46
  "#{project['name']} > #{task['name']}"
51
47
  )
52
48
  end
53
49
 
54
- def use_current_path
55
- @project_id = config.project_id
56
- @task_id = config.task_id
57
- end
58
-
59
- def use_path(path)
60
- args = path.to_s.split('/')
61
- @project_id = args[0].to_s
62
- @project_id = nil if project_id.empty?
63
-
64
- return if project_id.nil?
65
-
66
- @task_id = args[1].to_s
67
- @task_id = nil if @task_id.empty?
68
- end
69
-
70
50
  def api
71
51
  @api ||= Abt::Providers::Harvest::Api.new(access_token: config.access_token,
72
52
  account_id: config.account_id)
@@ -6,27 +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]
29
+
30
+ warn("Configuration cleared")
30
31
  end
31
32
  end
32
33
  end
@@ -6,75 +6,68 @@ 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
19
  require_project!
20
+ ensure_valid_configuration!
18
21
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
22
+ if path != config.path
23
+ config.path = path
24
+ warn("Configuration updated")
24
25
  end
25
- end
26
-
27
- private
28
26
 
29
- def show_current_configuration
30
- if task_id.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_project_is_valid!
39
- config.project_id = project_id
30
+ private
40
31
 
32
+ def print_configuration
41
33
  if task_id.nil?
42
34
  print_project(project)
43
- config.task_id = nil
44
35
  else
45
- ensure_task_is_valid!
46
- config.task_id = task_id
47
-
48
36
  print_task(project, task)
49
37
  end
50
38
  end
51
39
 
52
- def ensure_project_is_valid!
53
- cli.abort "Invalid project: #{project_id}" if project.nil?
54
- end
55
-
56
- def ensure_task_is_valid!
57
- cli.abort "Invalid task: #{task_id}" if task.nil?
40
+ def ensure_valid_configuration!
41
+ abort("Invalid project: #{project_id}") if project.nil?
42
+ abort("Invalid task: #{task_id}") if task_id && task.nil?
58
43
  end
59
44
 
60
45
  def project
61
- @project ||= project_assignment['project'].merge('client' => project_assignment['client'])
46
+ return @project if instance_variable_defined?(:@project)
47
+
48
+ @project = if project_assignment
49
+ project_assignment["project"].merge("client" => project_assignment["client"])
50
+ end
62
51
  end
63
52
 
64
53
  def task
65
- @task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
66
- task['id'].to_s == task_id
67
- end
54
+ return @task if instance_variable_defined?(:@task)
55
+
56
+ @task = if project_assignment
57
+ project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
58
+ task["id"].to_s == task_id
59
+ end
60
+ end
68
61
  end
69
62
 
70
63
  def project_assignment
71
64
  @project_assignment ||= begin
72
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
65
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
73
66
  end
74
67
  end
75
68
 
76
69
  def project_assignments
77
- @project_assignments ||= api.get_paged('users/me/project_assignments')
70
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
78
71
  end
79
72
  end
80
73
  end
@@ -6,75 +6,46 @@ module Abt
6
6
  module Commands
7
7
  class Init < BaseCommand
8
8
  def self.usage
9
- 'abt init harvest'
9
+ "abt init harvest"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Pick Harvest project for current git repository'
13
+ "Pick Harvest project for current git repository"
14
14
  end
15
15
 
16
16
  def perform
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
17
+ abort("Must be run inside a git repository") unless config.local_available?
18
18
 
19
19
  projects # Load projects up front to make it obvious that searches are instant
20
- project = find_search_result
20
+ project = cli.prompt.search("Select a project", searchable_projects)["project"]
21
21
 
22
- config.project_id = project['id']
23
- config.task_id = nil
22
+ config.path = Path.from_ids(project["id"])
24
23
 
25
24
  print_project(project)
26
25
  end
27
26
 
28
27
  private
29
28
 
30
- def find_search_result
31
- cli.warn 'Select a project'
32
-
33
- loop do
34
- matches = matches_for_string cli.prompt.text('Enter search')
35
- if matches.empty?
36
- warn 'No matches'
37
- next
38
- end
39
-
40
- cli.warn 'Showing the 10 first matches' if matches.size > 10
41
- choice = cli.prompt.choice 'Select a project', matches[0...10], true
42
- break choice['project'] unless choice.nil?
43
- end
44
- end
45
-
46
- def matches_for_string(string)
47
- search_string = sanitize_string(string)
48
-
49
- searchable_projects.select do |project|
50
- sanitize_string(project['name']).include?(search_string)
51
- end
52
- end
53
-
54
- def sanitize_string(string)
55
- string.downcase.gsub(/[^\w]/, '')
56
- end
57
-
58
29
  def searchable_projects
59
30
  @searchable_projects ||= projects.map do |project|
60
31
  {
61
- 'name' => "#{project['client']['name']} > #{project['name']}",
62
- 'project' => project
32
+ "name" => "#{project['client']['name']} > #{project['name']}",
33
+ "project" => project
63
34
  }
64
35
  end
65
36
  end
66
37
 
67
38
  def projects
68
39
  @projects ||= begin
69
- cli.warn 'Fetching projects...'
40
+ warn("Fetching projects...")
70
41
  project_assignments.map do |project_assignment|
71
- project_assignment['project'].merge('client' => project_assignment['client'])
42
+ project_assignment["project"].merge("client" => project_assignment["client"])
72
43
  end
73
44
  end
74
45
  end
75
46
 
76
47
  def project_assignments
77
- @project_assignments ||= api.get_paged('users/me/project_assignments')
48
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
78
49
  end
79
50
  end
80
51
  end