abt-cli 0.0.18 → 0.0.23

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