abt-cli 0.0.18 → 0.0.23

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 (68) 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 +51 -52
  8. data/lib/abt/cli/arguments_parser.rb +7 -26
  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 -51
  17. data/lib/abt/docs.rb +48 -25
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +11 -8
  20. data/lib/abt/git_config.rb +21 -39
  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 +20 -38
  24. data/lib/abt/providers/asana/commands/add.rb +18 -15
  25. data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +8 -7
  27. data/lib/abt/providers/asana/commands/current.rb +22 -38
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -18
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +20 -13
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +27 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +6 -8
  34. data/lib/abt/providers/asana/commands/start.rb +33 -24
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -5
  36. data/lib/abt/providers/asana/configuration.rb +46 -44
  37. data/lib/abt/providers/asana/path.rb +36 -0
  38. data/lib/abt/providers/devops/api.rb +23 -11
  39. data/lib/abt/providers/devops/base_command.rb +22 -43
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
  42. data/lib/abt/providers/devops/commands/clear.rb +8 -7
  43. data/lib/abt/providers/devops/commands/current.rb +24 -49
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +26 -16
  45. data/lib/abt/providers/devops/commands/init.rb +33 -26
  46. data/lib/abt/providers/devops/commands/pick.rb +23 -24
  47. data/lib/abt/providers/devops/commands/share.rb +7 -6
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +27 -56
  50. data/lib/abt/providers/devops/path.rb +51 -0
  51. data/lib/abt/providers/git/commands/branch.rb +25 -19
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +20 -36
  54. data/lib/abt/providers/harvest/commands/clear.rb +8 -7
  55. data/lib/abt/providers/harvest/commands/current.rb +27 -35
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -40
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -12
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +6 -8
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +13 -13
  62. data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
  63. data/lib/abt/providers/harvest/commands/track.rb +60 -38
  64. data/lib/abt/providers/harvest/configuration.rb +28 -37
  65. data/lib/abt/providers/harvest/path.rb +36 -0
  66. data/lib/abt/version.rb +1 -1
  67. metadata +18 -6
  68. data/lib/abt/cli/base_command.rb +0 -61
@@ -8,53 +8,18 @@ module Abt
8
8
 
9
9
  def initialize(cli:)
10
10
  @cli = cli
11
- @git = GitConfig.new(namespace: 'abt.devops')
12
11
  end
13
12
 
14
13
  def local_available?
15
- GitConfig.local_available?
14
+ git.available?
16
15
  end
17
16
 
18
- def organization_name
19
- local_available? ? git['organizationName'] : nil
17
+ def path
18
+ Path.new(local_available? && git["path"] || "")
20
19
  end
21
20
 
22
- def project_name
23
- local_available? ? git['projectName'] : nil
24
- end
25
-
26
- def board_id
27
- local_available? ? git['boardId'] : nil
28
- end
29
-
30
- def work_item_id
31
- local_available? ? git['workItemId'] : nil
32
- end
33
-
34
- def organization_name=(value)
35
- return if organization_name == value
36
-
37
- clear_local(verbose: false)
38
- git['organizationName'] = value unless value.nil?
39
- end
40
-
41
- def project_name=(value)
42
- return if project_name == value
43
-
44
- git['projectName'] = value unless value.nil?
45
- git['boardId'] = nil
46
- git['workItemId'] = nil
47
- end
48
-
49
- def board_id=(value)
50
- return if board_id == value
51
-
52
- git['boardId'] = value unless value.nil?
53
- git['workItemId'] = nil
54
- end
55
-
56
- def work_item_id=(value)
57
- git['workItemId'] = value
21
+ def path=(new_path)
22
+ git["path"] = new_path
58
23
  end
59
24
 
60
25
  def clear_local(verbose: true)
@@ -62,40 +27,46 @@ module Abt
62
27
  end
63
28
 
64
29
  def clear_global(verbose: true)
65
- git.global.clear(output: verbose ? cli.err_output : nil)
30
+ git_global.clear(output: verbose ? cli.err_output : nil)
66
31
  end
67
32
 
68
33
  def username_for_organization(organization_name)
69
34
  username_key = "organizations.#{organization_name}.username"
70
35
 
71
- return git.global[username_key] unless git.global[username_key].nil?
36
+ return git_global[username_key] unless git_global[username_key].nil?
72
37
 
73
- git.global[username_key] = cli.prompt.text([
38
+ git_global[username_key] = cli.prompt.text([
74
39
  "Please provide your username for the DevOps organization (#{organization_name}).",
75
- '',
76
- 'Enter username'
40
+ "",
41
+ "Enter username"
77
42
  ].join("\n"))
78
43
  end
79
44
 
80
45
  def access_token_for_organization(organization_name)
81
46
  access_token_key = "organizations.#{organization_name}.accessToken"
82
47
 
83
- return git.global[access_token_key] unless git.global[access_token_key].nil?
48
+ return git_global[access_token_key] unless git_global[access_token_key].nil?
84
49
 
85
- git.global[access_token_key] = cli.prompt.text([
86
- "Please provide your personal access token for the DevOps organization (#{organization_name}).",
87
- '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',
88
- '',
89
- 'The token MUST have "Read" permission for Work Items',
90
- 'Future features will likely require "Write" or "Manage"',
91
- '',
92
- 'Enter access token'
93
- ].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
94
59
  end
95
60
 
96
61
  private
97
62
 
98
- attr_reader :git
63
+ def git
64
+ @git ||= GitConfig.new("local", "abt.devops")
65
+ end
66
+
67
+ def git_global
68
+ @git_global ||= GitConfig.new("global", "abt.devops")
69
+ end
99
70
  end
100
71
  end
101
72
  end
@@ -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_name: nil, project_name: nil, board_id: nil, work_item_id: nil)
16
+ return new unless organization_name && project_name && board_id
17
+
18
+ new([organization_name, 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
@@ -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
- cli.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
- cli.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,12 +61,18 @@ module Abt
61
61
  end
62
62
 
63
63
  def branch_names_from_aris
64
+ abort("You must provide an additional ARI that responds to: branch-name. E.g., asana") if other_aris.empty?
65
+
64
66
  input = StringIO.new(cli.aris.to_s)
65
67
  output = StringIO.new
66
- Abt::Cli.new(argv: ['branch-name'], output: output, input: input).perform
68
+ Abt::Cli.new(argv: ["branch-name"], output: output, input: input).perform
67
69
 
68
70
  output.string.lines.map(&:strip).compact
69
71
  end
72
+
73
+ def other_aris
74
+ @other_aris ||= cli.aris - [ari]
75
+ end
70
76
  end
71
77
  end
72
78
  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,54 @@
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
 
22
+ def require_local_config!
23
+ abort("Must be run inside a git repository") unless config.local_available?
24
+ end
25
+
23
26
  def require_project!
24
- cli.abort 'No current/specified project. Did you initialize Harvest?' if project_id.nil?
27
+ return if project_id
28
+
29
+ abort("No current/specified project. Did you initialize Harvest?")
25
30
  end
26
31
 
27
32
  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
33
+ abort("No current/specified project. Did you initialize Harvest and pick a task?") unless project_id
33
34
 
34
- def same_args_as_config?
35
- project_id == config.project_id && task_id == config.task_id
35
+ abort("No current/specified task. Did you pick a Harvest task?") if task_id.nil?
36
36
  end
37
37
 
38
38
  def print_project(project)
39
39
  cli.print_ari(
40
- 'harvest',
41
- project['id'],
40
+ "harvest",
41
+ project["id"],
42
42
  "#{project['client']['name']} > #{project['name']}"
43
43
  )
44
44
  end
45
45
 
46
46
  def print_task(project, task)
47
47
  cli.print_ari(
48
- 'harvest',
48
+ "harvest",
49
49
  "#{project['id']}/#{task['id']}",
50
50
  "#{project['name']} > #{task['name']}"
51
51
  )
52
52
  end
53
53
 
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
54
  def api
71
55
  @api ||= Abt::Providers::Harvest::Api.new(access_token: config.access_token,
72
56
  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,67 @@ 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
+ require_local_config!
17
18
  require_project!
19
+ ensure_valid_configuration!
18
20
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
21
+ if path != config.path
22
+ config.path = path
23
+ warn("Configuration updated")
24
24
  end
25
- end
26
-
27
- private
28
25
 
29
- def show_current_configuration
30
- if task_id.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
26
+ print_configuration
35
27
  end
36
28
 
37
- def update_configuration
38
- ensure_project_is_valid!
39
- config.project_id = project_id
29
+ private
40
30
 
31
+ def print_configuration
41
32
  if task_id.nil?
42
33
  print_project(project)
43
- config.task_id = nil
44
34
  else
45
- ensure_task_is_valid!
46
- config.task_id = task_id
47
-
48
35
  print_task(project, task)
49
36
  end
50
37
  end
51
38
 
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?
39
+ def ensure_valid_configuration!
40
+ abort("Invalid project: #{project_id}") if project.nil?
41
+ abort("Invalid task: #{task_id}") if task_id && task.nil?
58
42
  end
59
43
 
60
44
  def project
61
- @project ||= project_assignment['project'].merge('client' => project_assignment['client'])
45
+ return @project if instance_variable_defined?(:@project)
46
+
47
+ @project = if project_assignment
48
+ project_assignment["project"].merge("client" => project_assignment["client"])
49
+ end
62
50
  end
63
51
 
64
52
  def task
65
- @task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
66
- task['id'].to_s == task_id
67
- end
53
+ return @task if instance_variable_defined?(:@task)
54
+
55
+ @task = if project_assignment
56
+ project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
57
+ task["id"].to_s == task_id
58
+ end
59
+ end
68
60
  end
69
61
 
70
62
  def project_assignment
71
63
  @project_assignment ||= begin
72
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
64
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
73
65
  end
74
66
  end
75
67
 
76
68
  def project_assignments
77
- @project_assignments ||= api.get_paged('users/me/project_assignments')
69
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
78
70
  end
79
71
  end
80
72
  end