abt-cli 0.0.15 → 0.0.20
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.
- checksums.yaml +4 -4
- data/bin/abt +1 -1
- data/lib/abt.rb +4 -3
- data/lib/abt/ari.rb +20 -0
- data/lib/abt/ari_list.rb +13 -0
- data/lib/abt/base_command.rb +63 -0
- data/lib/abt/cli.rb +68 -49
- data/lib/abt/cli/arguments_parser.rb +48 -0
- data/lib/abt/cli/prompt.rb +7 -6
- data/lib/abt/docs.rb +35 -28
- data/lib/abt/docs/cli.rb +42 -11
- data/lib/abt/docs/markdown.rb +38 -11
- data/lib/abt/git_config.rb +26 -31
- data/lib/abt/providers/asana/base_command.rb +17 -37
- data/lib/abt/providers/asana/commands/add.rb +12 -10
- data/lib/abt/providers/asana/commands/{branch-name.rb → branch_name.rb} +12 -7
- data/lib/abt/providers/asana/commands/clear.rb +19 -6
- data/lib/abt/providers/asana/commands/current.rb +22 -37
- data/lib/abt/providers/asana/commands/finalize.rb +8 -12
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
- data/lib/abt/providers/asana/commands/init.rb +9 -9
- data/lib/abt/providers/asana/commands/pick.rb +28 -15
- data/lib/abt/providers/asana/commands/projects.rb +4 -4
- data/lib/abt/providers/asana/commands/share.rb +5 -9
- data/lib/abt/providers/asana/commands/start.rb +26 -18
- data/lib/abt/providers/asana/commands/tasks.rb +7 -6
- data/lib/abt/providers/asana/configuration.rb +23 -37
- data/lib/abt/providers/asana/path.rb +36 -0
- data/lib/abt/providers/devops/api.rb +12 -0
- data/lib/abt/providers/devops/base_command.rb +18 -44
- data/lib/abt/providers/devops/commands/boards.rb +7 -5
- data/lib/abt/providers/devops/commands/{branch-name.rb → branch_name.rb} +10 -6
- data/lib/abt/providers/devops/commands/clear.rb +19 -6
- data/lib/abt/providers/devops/commands/current.rb +17 -41
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +12 -4
- data/lib/abt/providers/devops/commands/init.rb +18 -18
- data/lib/abt/providers/devops/commands/pick.rb +16 -16
- data/lib/abt/providers/devops/commands/share.rb +6 -7
- data/lib/abt/providers/devops/commands/work-items.rb +4 -4
- data/lib/abt/providers/devops/configuration.rb +20 -57
- data/lib/abt/providers/devops/path.rb +50 -0
- data/lib/abt/providers/git/commands/branch.rb +28 -28
- data/lib/abt/providers/harvest/base_command.rb +18 -36
- data/lib/abt/providers/harvest/commands/clear.rb +19 -6
- data/lib/abt/providers/harvest/commands/current.rb +27 -34
- data/lib/abt/providers/harvest/commands/init.rb +8 -9
- data/lib/abt/providers/harvest/commands/pick.rb +15 -8
- data/lib/abt/providers/harvest/commands/projects.rb +4 -4
- data/lib/abt/providers/harvest/commands/share.rb +7 -11
- data/lib/abt/providers/harvest/commands/start.rb +6 -42
- data/lib/abt/providers/harvest/commands/stop.rb +10 -10
- data/lib/abt/providers/harvest/commands/tasks.rb +7 -4
- data/lib/abt/providers/harvest/commands/track.rb +66 -21
- data/lib/abt/providers/harvest/configuration.rb +23 -38
- data/lib/abt/providers/harvest/path.rb +36 -0
- data/lib/abt/version.rb +1 -1
- metadata +11 -7
- data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
- data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
- data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
@@ -5,15 +5,15 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Tasks < BaseCommand
|
8
|
-
def self.
|
9
|
-
'tasks asana'
|
8
|
+
def self.usage
|
9
|
+
'abt tasks asana'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'List available tasks on project - useful for piping into grep etc.'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def perform
|
17
17
|
require_project!
|
18
18
|
|
19
19
|
tasks.each do |task|
|
@@ -25,14 +25,15 @@ module Abt
|
|
25
25
|
|
26
26
|
def project
|
27
27
|
@project ||= begin
|
28
|
-
api.get("projects/#{project_gid}")
|
28
|
+
api.get("projects/#{project_gid}", opt_fields: 'name')
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
def tasks
|
33
33
|
@tasks ||= begin
|
34
|
-
|
35
|
-
api.get_paged('tasks', project: project['gid'], opt_fields: 'name')
|
34
|
+
warn 'Fetching tasks...'
|
35
|
+
tasks = api.get_paged('tasks', project: project['gid'], opt_fields: 'name,completed')
|
36
|
+
tasks.select { |task| !task['completed'] }
|
36
37
|
end
|
37
38
|
end
|
38
39
|
end
|
@@ -8,24 +8,23 @@ module Abt
|
|
8
8
|
|
9
9
|
def initialize(cli:)
|
10
10
|
@cli = cli
|
11
|
-
@git = GitConfig.new(namespace: 'abt.asana')
|
12
11
|
end
|
13
12
|
|
14
13
|
def local_available?
|
15
|
-
|
14
|
+
git.available?
|
16
15
|
end
|
17
16
|
|
18
|
-
def
|
19
|
-
local_available?
|
17
|
+
def path
|
18
|
+
Path.new(local_available? && git['path'] || '')
|
20
19
|
end
|
21
20
|
|
22
|
-
def
|
23
|
-
|
21
|
+
def path=(new_path)
|
22
|
+
git['path'] = new_path
|
24
23
|
end
|
25
24
|
|
26
25
|
def workspace_gid
|
27
26
|
@workspace_gid ||= begin
|
28
|
-
current =
|
27
|
+
current = git_global['workspaceGid']
|
29
28
|
if current.nil?
|
30
29
|
prompt_workspace['gid']
|
31
30
|
else
|
@@ -46,37 +45,18 @@ module Abt
|
|
46
45
|
@finalized_section_gid ||= git['finalizedSectionGid'] || prompt_finalized_section['gid']
|
47
46
|
end
|
48
47
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
clear_local
|
53
|
-
git['projectGid'] = value unless value.nil?
|
54
|
-
end
|
55
|
-
|
56
|
-
def task_gid=(value)
|
57
|
-
git['taskGid'] = value
|
58
|
-
end
|
59
|
-
|
60
|
-
def clear_local
|
61
|
-
cli.abort 'No local configuration was found' unless local_available?
|
62
|
-
|
63
|
-
git['projectGid'] = nil
|
64
|
-
git['taskGid'] = nil
|
65
|
-
git['wipSectionGid'] = nil
|
66
|
-
git['finalizedSectionGid'] = nil
|
48
|
+
def clear_local(verbose: true)
|
49
|
+
git.clear(output: verbose ? cli.err_output : nil)
|
67
50
|
end
|
68
51
|
|
69
|
-
def clear_global
|
70
|
-
|
71
|
-
cli.puts 'Deleting configuration: ' + key
|
72
|
-
git.global[key] = nil
|
73
|
-
end
|
52
|
+
def clear_global(verbose: true)
|
53
|
+
git_global.clear(output: verbose ? cli.err_output : nil)
|
74
54
|
end
|
75
55
|
|
76
56
|
def access_token
|
77
|
-
return
|
57
|
+
return git_global['accessToken'] unless git_global['accessToken'].nil?
|
78
58
|
|
79
|
-
|
59
|
+
git_global['accessToken'] = cli.prompt.text([
|
80
60
|
'Please provide your personal access token for Asana.',
|
81
61
|
'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
|
82
62
|
'',
|
@@ -86,7 +66,13 @@ module Abt
|
|
86
66
|
|
87
67
|
private
|
88
68
|
|
89
|
-
|
69
|
+
def git
|
70
|
+
@git ||= GitConfig.new('local', 'abt.asana')
|
71
|
+
end
|
72
|
+
|
73
|
+
def git_global
|
74
|
+
@git_global ||= GitConfig.new('global', 'abt.asana')
|
75
|
+
end
|
90
76
|
|
91
77
|
def prompt_finalized_section
|
92
78
|
section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
|
@@ -102,23 +88,23 @@ module Abt
|
|
102
88
|
|
103
89
|
def prompt_section(message)
|
104
90
|
cli.warn 'Fetching sections...'
|
105
|
-
sections = api.get_paged("projects/#{project_gid}/sections")
|
91
|
+
sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: 'name')
|
106
92
|
cli.prompt.choice(message, sections)
|
107
93
|
end
|
108
94
|
|
109
95
|
def prompt_workspace
|
110
96
|
cli.warn 'Fetching workspaces...'
|
111
|
-
workspaces = api.get_paged('workspaces')
|
97
|
+
workspaces = api.get_paged('workspaces', opt_fields: 'name')
|
112
98
|
if workspaces.empty?
|
113
99
|
cli.abort 'Your asana access token does not have access to any workspaces'
|
114
100
|
elsif workspaces.one?
|
115
101
|
workspace = workspaces.first
|
116
|
-
cli.warn "Selected Asana workspace #{workspace['name']}"
|
102
|
+
cli.warn "Selected Asana workspace: #{workspace['name']}"
|
117
103
|
else
|
118
104
|
workspace = cli.prompt.choice('Select Asana workspace', workspaces)
|
119
105
|
end
|
120
106
|
|
121
|
-
|
107
|
+
git_global['workspaceGid'] = workspace['gid']
|
122
108
|
workspace
|
123
109
|
end
|
124
110
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
class Path < String
|
7
|
+
PATH_REGEX = %r{^(?<project_gid>\d+)?(/(?<task_gid>\d+))?$}.freeze
|
8
|
+
|
9
|
+
def self.from_ids(project_gid = nil, task_gid = nil)
|
10
|
+
path = project_gid ? [project_gid, *task_gid].join('/') : ''
|
11
|
+
new path
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(path = '')
|
15
|
+
raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def project_gid
|
21
|
+
match[:project_gid]
|
22
|
+
end
|
23
|
+
|
24
|
+
def task_gid
|
25
|
+
match[:task_gid]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def match
|
31
|
+
@match ||= PATH_REGEX.match(self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -69,6 +69,10 @@ module Abt
|
|
69
69
|
"#{base_url}/_workitems/edit/#{work_item['id']}"
|
70
70
|
end
|
71
71
|
|
72
|
+
def url_for_board(board)
|
73
|
+
"#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
|
74
|
+
end
|
75
|
+
|
72
76
|
def connection
|
73
77
|
@connection ||= Faraday.new(api_endpoint) do |connection|
|
74
78
|
connection.basic_auth username, access_token
|
@@ -79,6 +83,14 @@ module Abt
|
|
79
83
|
|
80
84
|
private
|
81
85
|
|
86
|
+
# Shamelessly copied from ERB::Util.url_encode
|
87
|
+
# https://apidock.com/ruby/ERB/Util/url_encode
|
88
|
+
def rfc_3986_encode_path_segment(string)
|
89
|
+
string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
|
90
|
+
format('%%%02X', match.unpack1('C')) # rubocop:disable Style/FormatStringToken
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
82
94
|
def handle_denied_by_conditional_access_policy!(exception)
|
83
95
|
raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
|
84
96
|
|
@@ -3,20 +3,18 @@
|
|
3
3
|
module Abt
|
4
4
|
module Providers
|
5
5
|
module Devops
|
6
|
-
class BaseCommand
|
7
|
-
|
6
|
+
class BaseCommand < Abt::BaseCommand
|
7
|
+
extend Forwardable
|
8
8
|
|
9
|
-
|
10
|
-
@arg_str = arg_str
|
9
|
+
attr_reader :config, :path
|
11
10
|
|
12
|
-
|
13
|
-
@cli = cli
|
11
|
+
def_delegators(:@path, :organization_name, :project_name, :board_id, :work_item_id)
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
def initialize(ari:, cli:)
|
14
|
+
super
|
15
|
+
|
16
|
+
@config = Configuration.new(cli: cli)
|
17
|
+
@path = ari.path ? Path.new(ari.path) : config.path
|
20
18
|
end
|
21
19
|
|
22
20
|
private
|
@@ -24,17 +22,17 @@ module Abt
|
|
24
22
|
def require_board!
|
25
23
|
return if organization_name && project_name && board_id
|
26
24
|
|
27
|
-
|
25
|
+
abort 'No current/specified board. Did you initialize DevOps?'
|
28
26
|
end
|
29
27
|
|
30
28
|
def require_work_item!
|
31
29
|
unless organization_name && project_name && board_id
|
32
|
-
|
30
|
+
abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
|
33
31
|
end
|
34
32
|
|
35
33
|
return if work_item_id
|
36
34
|
|
37
|
-
|
35
|
+
abort 'No current/specified work item. Did you pick a DevOps work item?'
|
38
36
|
end
|
39
37
|
|
40
38
|
def sanitize_work_item(work_item)
|
@@ -47,42 +45,18 @@ module Abt
|
|
47
45
|
)
|
48
46
|
end
|
49
47
|
|
50
|
-
def same_args_as_config?
|
51
|
-
organization_name == config.organization_name &&
|
52
|
-
project_name == config.project_name &&
|
53
|
-
board_id == config.board_id &&
|
54
|
-
work_item_id == config.work_item_id
|
55
|
-
end
|
56
|
-
|
57
48
|
def print_board(organization_name, project_name, board)
|
58
|
-
|
49
|
+
path = "#{organization_name}/#{project_name}/#{board['id']}"
|
59
50
|
|
60
|
-
cli.
|
61
|
-
|
51
|
+
cli.print_ari('devops', path, board['name'])
|
52
|
+
warn api.url_for_board(board) if cli.output.isatty
|
62
53
|
end
|
63
54
|
|
64
55
|
def print_work_item(organization, project, board, work_item)
|
65
|
-
|
66
|
-
|
67
|
-
cli.print_provider_command('devops', arg_str, work_item['name'])
|
68
|
-
cli.warn work_item['url'] if work_item.key?('url') && cli.output.isatty
|
69
|
-
end
|
70
|
-
|
71
|
-
def use_current_args
|
72
|
-
@organization_name = config.organization_name
|
73
|
-
@project_name = config.project_name
|
74
|
-
@board_id = config.board_id
|
75
|
-
@work_item_id = config.work_item_id
|
76
|
-
end
|
77
|
-
|
78
|
-
def use_arg_str(arg_str)
|
79
|
-
args = arg_str.to_s.split('/')
|
80
|
-
|
81
|
-
if args.length < 3
|
82
|
-
cli.abort 'Argument format is <organization>/<project>/<board-id>[/<work-item-id>]'
|
83
|
-
end
|
56
|
+
path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
|
84
57
|
|
85
|
-
(
|
58
|
+
cli.print_ari('devops', path, work_item['name'])
|
59
|
+
warn work_item['url'] if work_item.key?('url') && cli.output.isatty
|
86
60
|
end
|
87
61
|
|
88
62
|
def api
|
@@ -5,17 +5,19 @@ module Abt
|
|
5
5
|
module Devops
|
6
6
|
module Commands
|
7
7
|
class Boards < BaseCommand
|
8
|
-
def self.
|
9
|
-
'boards devops'
|
8
|
+
def self.usage
|
9
|
+
'abt boards devops'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'List all boards - useful for piping into grep etc'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
16
|
+
def perform
|
17
|
+
if organization_name.nil?
|
18
|
+
abort 'No organization selected. Did you initialize DevOps?'
|
19
|
+
end
|
20
|
+
abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
|
19
21
|
|
20
22
|
boards.map do |board|
|
21
23
|
print_board(organization_name, project_name, board)
|
@@ -5,22 +5,26 @@ module Abt
|
|
5
5
|
module Devops
|
6
6
|
module Commands
|
7
7
|
class BranchName < BaseCommand
|
8
|
-
def self.
|
9
|
-
'branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
|
8
|
+
def self.usage
|
9
|
+
'abt branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'Suggest a git branch name for the current/specified work-item.'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def perform
|
17
17
|
require_work_item!
|
18
18
|
|
19
|
-
|
19
|
+
puts name
|
20
20
|
rescue HttpError::NotFoundError
|
21
21
|
args = [organization_name, project_name, board_id, work_item_id].compact
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
error_message = [
|
24
|
+
'Unable to find work item for configuration:',
|
25
|
+
"devops:#{args.join('/')}"
|
26
|
+
].join("\n")
|
27
|
+
abort error_message
|
24
28
|
end
|
25
29
|
|
26
30
|
private
|
@@ -5,17 +5,30 @@ module Abt
|
|
5
5
|
module Devops
|
6
6
|
module Commands
|
7
7
|
class Clear < BaseCommand
|
8
|
-
def self.
|
9
|
-
'clear devops'
|
8
|
+
def self.usage
|
9
|
+
'abt clear devops'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
'Clear DevOps
|
13
|
+
'Clear DevOps configuration'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
16
|
+
def self.flags
|
17
|
+
[
|
18
|
+
['-g', '--global', 'Clear global instead of local DevOp configuration (credentials etc.)'],
|
19
|
+
['-a', '--all', 'Clear all DevOp configuration']
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
def perform
|
24
|
+
if flags[:global] && flags[:all]
|
25
|
+
abort('Flags --global and --all cannot be used at the same time')
|
26
|
+
end
|
27
|
+
|
28
|
+
config.clear_local unless flags[:global]
|
29
|
+
config.clear_global if flags[:global] || flags[:all]
|
30
|
+
|
31
|
+
warn 'Configuration cleared'
|
19
32
|
end
|
20
33
|
end
|
21
34
|
end
|
@@ -5,72 +5,48 @@ module Abt
|
|
5
5
|
module Devops
|
6
6
|
module Commands
|
7
7
|
class Current < BaseCommand
|
8
|
-
def self.
|
9
|
-
'current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
|
8
|
+
def self.usage
|
9
|
+
'abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'Get or set DevOps configuration for current git repository'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def perform
|
17
|
+
abort 'Must be run inside a git repository' unless config.local_available?
|
18
|
+
|
17
19
|
require_board!
|
20
|
+
ensure_valid_configuration!
|
18
21
|
|
19
|
-
if
|
20
|
-
|
21
|
-
|
22
|
-
cli.warn 'Updating configuration'
|
23
|
-
update_configuration
|
22
|
+
if path != config.path && config.local_available?
|
23
|
+
config.path = path
|
24
|
+
warn 'Configuration updated'
|
24
25
|
end
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
26
|
|
29
|
-
|
30
|
-
if work_item_id.nil?
|
31
|
-
print_board(organization_name, project_name, board)
|
32
|
-
else
|
33
|
-
print_work_item(organization_name, project_name, board, work_item)
|
34
|
-
end
|
27
|
+
print_configuration
|
35
28
|
end
|
36
29
|
|
37
|
-
|
38
|
-
ensure_board_is_valid!
|
30
|
+
private
|
39
31
|
|
32
|
+
def print_configuration
|
40
33
|
if work_item_id.nil?
|
41
|
-
update_board_config
|
42
|
-
config.work_item_id = nil
|
43
|
-
|
44
34
|
print_board(organization_name, project_name, board)
|
45
35
|
else
|
46
|
-
ensure_work_item_is_valid!
|
47
|
-
|
48
|
-
update_board_config
|
49
|
-
config.work_item_id = work_item_id
|
50
|
-
|
51
36
|
print_work_item(organization_name, project_name, board, work_item)
|
52
37
|
end
|
53
38
|
end
|
54
39
|
|
55
|
-
def
|
56
|
-
config.organization_name = organization_name
|
57
|
-
config.project_name = project_name
|
58
|
-
config.board_id = board_id
|
59
|
-
end
|
60
|
-
|
61
|
-
def ensure_board_is_valid!
|
40
|
+
def ensure_valid_configuration!
|
62
41
|
if board.nil?
|
63
|
-
|
42
|
+
abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
|
64
43
|
end
|
65
|
-
|
66
|
-
|
67
|
-
def ensure_work_item_is_valid!
|
68
|
-
cli.abort "No such work item: ##{work_item_id}" if work_item.nil?
|
44
|
+
abort "No such work item: ##{work_item_id}" if work_item_id && work_item.nil?
|
69
45
|
end
|
70
46
|
|
71
47
|
def board
|
72
48
|
@board ||= begin
|
73
|
-
|
49
|
+
warn 'Fetching board...'
|
74
50
|
api.get("work/boards/#{board_id}")
|
75
51
|
rescue HttpError::NotFoundError
|
76
52
|
nil
|
@@ -79,7 +55,7 @@ module Abt
|
|
79
55
|
|
80
56
|
def work_item
|
81
57
|
@work_item ||= begin
|
82
|
-
|
58
|
+
warn 'Fetching work item...'
|
83
59
|
work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
|
84
60
|
sanitize_work_item(work_item)
|
85
61
|
rescue HttpError::NotFoundError
|