abt-cli 0.0.18 → 0.0.19
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/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 +6 -9
- data/lib/abt/cli/arguments_parser.rb +1 -23
- data/lib/abt/cli/prompt.rb +5 -4
- data/lib/abt/docs.rb +6 -5
- data/lib/abt/docs/markdown.rb +1 -1
- data/lib/abt/git_config.rb +20 -36
- data/lib/abt/providers/asana/base_command.rb +13 -33
- data/lib/abt/providers/asana/commands/add.rb +9 -7
- data/lib/abt/providers/asana/commands/branch_name.rb +9 -4
- data/lib/abt/providers/asana/commands/clear.rb +2 -0
- data/lib/abt/providers/asana/commands/current.rb +19 -34
- data/lib/abt/providers/asana/commands/finalize.rb +3 -3
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +9 -4
- data/lib/abt/providers/asana/commands/init.rb +6 -6
- data/lib/abt/providers/asana/commands/pick.rb +16 -11
- data/lib/abt/providers/asana/commands/projects.rb +1 -1
- data/lib/abt/providers/asana/commands/share.rb +2 -6
- data/lib/abt/providers/asana/commands/start.rb +14 -12
- data/lib/abt/providers/asana/commands/tasks.rb +4 -3
- data/lib/abt/providers/asana/configuration.rb +18 -24
- 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 +13 -38
- data/lib/abt/providers/devops/commands/boards.rb +2 -2
- data/lib/abt/providers/devops/commands/branch_name.rb +7 -3
- data/lib/abt/providers/devops/commands/clear.rb +2 -0
- data/lib/abt/providers/devops/commands/current.rb +14 -38
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +9 -1
- data/lib/abt/providers/devops/commands/init.rb +15 -15
- data/lib/abt/providers/devops/commands/pick.rb +5 -12
- data/lib/abt/providers/devops/commands/share.rb +3 -4
- data/lib/abt/providers/devops/commands/work-items.rb +1 -1
- data/lib/abt/providers/devops/configuration.rb +17 -46
- data/lib/abt/providers/devops/path.rb +50 -0
- data/lib/abt/providers/git/commands/branch.rb +14 -8
- data/lib/abt/providers/harvest/base_command.rb +14 -32
- data/lib/abt/providers/harvest/commands/clear.rb +2 -0
- data/lib/abt/providers/harvest/commands/current.rb +24 -31
- data/lib/abt/providers/harvest/commands/init.rb +5 -6
- data/lib/abt/providers/harvest/commands/pick.rb +3 -4
- data/lib/abt/providers/harvest/commands/projects.rb +1 -1
- data/lib/abt/providers/harvest/commands/share.rb +4 -8
- data/lib/abt/providers/harvest/commands/stop.rb +7 -7
- data/lib/abt/providers/harvest/commands/tasks.rb +4 -1
- data/lib/abt/providers/harvest/commands/track.rb +25 -18
- data/lib/abt/providers/harvest/configuration.rb +20 -29
- data/lib/abt/providers/harvest/path.rb +36 -0
- data/lib/abt/version.rb +1 -1
- metadata +8 -3
- data/lib/abt/cli/base_command.rb +0 -61
@@ -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,19 +3,18 @@
|
|
3
3
|
module Abt
|
4
4
|
module Providers
|
5
5
|
module Devops
|
6
|
-
class BaseCommand < Abt::
|
7
|
-
|
6
|
+
class BaseCommand < Abt::BaseCommand
|
7
|
+
extend Forwardable
|
8
8
|
|
9
|
-
|
9
|
+
attr_reader :config, :path
|
10
|
+
|
11
|
+
def_delegators(:@path, :organization_name, :project_name, :board_id, :work_item_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
|
@@ -23,17 +22,17 @@ module Abt
|
|
23
22
|
def require_board!
|
24
23
|
return if organization_name && project_name && board_id
|
25
24
|
|
26
|
-
|
25
|
+
abort 'No current/specified board. Did you initialize DevOps?'
|
27
26
|
end
|
28
27
|
|
29
28
|
def require_work_item!
|
30
29
|
unless organization_name && project_name && board_id
|
31
|
-
|
30
|
+
abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
|
32
31
|
end
|
33
32
|
|
34
33
|
return if work_item_id
|
35
34
|
|
36
|
-
|
35
|
+
abort 'No current/specified work item. Did you pick a DevOps work item?'
|
37
36
|
end
|
38
37
|
|
39
38
|
def sanitize_work_item(work_item)
|
@@ -46,42 +45,18 @@ module Abt
|
|
46
45
|
)
|
47
46
|
end
|
48
47
|
|
49
|
-
def same_args_as_config?
|
50
|
-
organization_name == config.organization_name &&
|
51
|
-
project_name == config.project_name &&
|
52
|
-
board_id == config.board_id &&
|
53
|
-
work_item_id == config.work_item_id
|
54
|
-
end
|
55
|
-
|
56
48
|
def print_board(organization_name, project_name, board)
|
57
49
|
path = "#{organization_name}/#{project_name}/#{board['id']}"
|
58
50
|
|
59
51
|
cli.print_ari('devops', path, board['name'])
|
60
|
-
|
52
|
+
warn api.url_for_board(board) if cli.output.isatty
|
61
53
|
end
|
62
54
|
|
63
55
|
def print_work_item(organization, project, board, work_item)
|
64
56
|
path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
|
65
57
|
|
66
58
|
cli.print_ari('devops', path, work_item['name'])
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
def use_current_path
|
71
|
-
@organization_name = config.organization_name
|
72
|
-
@project_name = config.project_name
|
73
|
-
@board_id = config.board_id
|
74
|
-
@work_item_id = config.work_item_id
|
75
|
-
end
|
76
|
-
|
77
|
-
def use_path(path)
|
78
|
-
args = path.to_s.split('/')
|
79
|
-
|
80
|
-
if args.length < 3
|
81
|
-
cli.abort 'Argument format is <organization>/<project>/<board-id>[/<work-item-id>]'
|
82
|
-
end
|
83
|
-
|
84
|
-
(@organization_name, @project_name, @board_id, @work_item_id) = args
|
59
|
+
warn work_item['url'] if work_item.key?('url') && cli.output.isatty
|
85
60
|
end
|
86
61
|
|
87
62
|
def api
|
@@ -15,9 +15,9 @@ module Abt
|
|
15
15
|
|
16
16
|
def perform
|
17
17
|
if organization_name.nil?
|
18
|
-
|
18
|
+
abort 'No organization selected. Did you initialize DevOps?'
|
19
19
|
end
|
20
|
-
|
20
|
+
abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
|
21
21
|
|
22
22
|
boards.map do |board|
|
23
23
|
print_board(organization_name, project_name, board)
|
@@ -16,11 +16,15 @@ module Abt
|
|
16
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
|
@@ -14,63 +14,39 @@ module Abt
|
|
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_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
|
@@ -25,7 +25,15 @@ module Abt
|
|
25
25
|
}
|
26
26
|
}
|
27
27
|
|
28
|
-
|
28
|
+
puts Oj.dump(body, mode: :json)
|
29
|
+
rescue HttpError::NotFoundError
|
30
|
+
args = [organization_name, project_name, board_id, work_item_id].compact
|
31
|
+
|
32
|
+
error_message = [
|
33
|
+
'Unable to find work item for configuration:',
|
34
|
+
"devops:#{args.join('/')}"
|
35
|
+
].join("\n")
|
36
|
+
abort error_message
|
29
37
|
end
|
30
38
|
|
31
39
|
private
|
@@ -17,15 +17,11 @@ module Abt
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def perform
|
20
|
-
|
21
|
-
|
22
|
-
@organization_name = config.organization_name = organization_name_from_url
|
23
|
-
@project_name = config.project_name = project_name_from_url
|
20
|
+
abort 'Must be run inside a git repository' unless config.local_available?
|
24
21
|
|
25
22
|
board = cli.prompt.choice 'Select a project work board', boards
|
26
23
|
|
27
|
-
config.
|
28
|
-
|
24
|
+
config.path = Path.from_ids(organization_name, project_name, board['id'])
|
29
25
|
print_board(organization_name, project_name, board)
|
30
26
|
end
|
31
27
|
|
@@ -35,17 +31,21 @@ module Abt
|
|
35
31
|
@boards ||= api.get_paged('work/boards')
|
36
32
|
end
|
37
33
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
34
|
+
def project_name
|
35
|
+
@project_name ||= begin
|
36
|
+
if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
|
37
|
+
(match = VS_URL_REGEX.match(project_url))
|
38
|
+
match[:project]
|
39
|
+
end
|
42
40
|
end
|
43
41
|
end
|
44
42
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
def organization_name
|
44
|
+
@organization_name ||= begin
|
45
|
+
if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
|
46
|
+
(match = VS_URL_REGEX.match(project_url))
|
47
|
+
match[:organization]
|
48
|
+
end
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -61,7 +61,7 @@ module Abt
|
|
61
61
|
|
62
62
|
break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
|
63
63
|
|
64
|
-
|
64
|
+
warn 'Invalid URL'
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
@@ -20,36 +20,29 @@ module Abt
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
23
|
+
abort 'Must be run inside a git repository' unless config.local_available?
|
24
24
|
require_board!
|
25
25
|
|
26
|
-
|
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
|
-
|
33
|
+
config.path = Path.from_ids(organization_name, project_name, board_id, work_item['id'])
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
|
-
def update_config!(work_item)
|
39
|
-
config.organization_name = organization_name
|
40
|
-
config.project_name = project_name
|
41
|
-
config.board_id = board_id
|
42
|
-
config.work_item_id = work_item['id']
|
43
|
-
end
|
44
|
-
|
45
38
|
def select_work_item
|
46
39
|
loop do
|
47
40
|
column = cli.prompt.choice 'Which column?', columns
|
48
|
-
|
41
|
+
warn 'Fetching work items...'
|
49
42
|
work_items = work_items_in_column(column)
|
50
43
|
|
51
44
|
if work_items.length.zero?
|
52
|
-
|
45
|
+
warn 'Section is empty'
|
53
46
|
next
|
54
47
|
end
|
55
48
|
|
@@ -10,14 +10,13 @@ module Abt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
'Print DevOps
|
13
|
+
'Print DevOps ARI'
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
require_board!
|
18
18
|
|
19
|
-
|
20
|
-
cli.print_ari('devops', args.join('/'))
|
19
|
+
cli.print_ari('devops', path)
|
21
20
|
end
|
22
21
|
end
|
23
22
|
end
|
@@ -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
|
-
|
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
|
-
|
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,15 +27,15 @@ module Abt
|
|
62
27
|
end
|
63
28
|
|
64
29
|
def clear_global(verbose: true)
|
65
|
-
|
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
|
36
|
+
return git_global[username_key] unless git_global[username_key].nil?
|
72
37
|
|
73
|
-
|
38
|
+
git_global[username_key] = cli.prompt.text([
|
74
39
|
"Please provide your username for the DevOps organization (#{organization_name}).",
|
75
40
|
'',
|
76
41
|
'Enter username'
|
@@ -80,9 +45,9 @@ module Abt
|
|
80
45
|
def access_token_for_organization(organization_name)
|
81
46
|
access_token_key = "organizations.#{organization_name}.accessToken"
|
82
47
|
|
83
|
-
return
|
48
|
+
return git_global[access_token_key] unless git_global[access_token_key].nil?
|
84
49
|
|
85
|
-
|
50
|
+
git_global[access_token_key] = cli.prompt.text([
|
86
51
|
"Please provide your personal access token for the DevOps organization (#{organization_name}).",
|
87
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',
|
88
53
|
'',
|
@@ -95,7 +60,13 @@ module Abt
|
|
95
60
|
|
96
61
|
private
|
97
62
|
|
98
|
-
|
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
|