abt-cli 0.0.14 → 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/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 +89 -54
- data/lib/abt/cli/arguments_parser.rb +48 -0
- data/lib/abt/cli/{dialogs.rb → prompt.rb} +38 -18
- 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 +15 -13
- 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 +6 -6
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
- data/lib/abt/providers/asana/commands/init.rb +11 -11
- data/lib/abt/providers/asana/commands/pick.rb +30 -17
- 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 +27 -19
- 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 +20 -20
- data/lib/abt/providers/devops/commands/pick.rb +18 -18
- 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 +10 -11
- data/lib/abt/providers/harvest/commands/pick.rb +16 -9
- 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 +12 -9
- data/lib/abt/cli/io.rb +0 -23
- 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,58 +5,66 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Start < BaseCommand
|
8
|
-
def self.
|
9
|
-
'start asana[:<project-gid>/<task-gid>]'
|
8
|
+
def self.usage
|
9
|
+
'abt start asana[:<project-gid>/<task-gid>]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
'
|
13
|
+
'Move current or specified task to WIP section (column) and assign it to you'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def self.flags
|
17
|
+
[
|
18
|
+
['-s', '--set', 'Set specified task as current']
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
17
23
|
require_task!
|
18
24
|
|
19
|
-
|
25
|
+
print_task(project_gid, task)
|
20
26
|
|
21
27
|
update_assignee_if_needed
|
22
28
|
move_if_needed
|
29
|
+
maybe_override_current_task
|
23
30
|
end
|
24
31
|
|
25
32
|
private
|
26
33
|
|
27
34
|
def maybe_override_current_task
|
28
|
-
return
|
29
|
-
return if
|
35
|
+
return unless flags[:set]
|
36
|
+
return if path.nil?
|
37
|
+
return if path == config.path
|
30
38
|
return unless config.local_available?
|
31
39
|
|
32
|
-
|
33
|
-
Current
|
40
|
+
config.path = path
|
41
|
+
warn 'Current task updated'
|
34
42
|
end
|
35
43
|
|
36
44
|
def update_assignee_if_needed
|
37
45
|
current_assignee = task.dig('assignee')
|
38
46
|
|
39
47
|
if current_assignee.nil?
|
40
|
-
|
48
|
+
warn "Assigning task to user: #{current_user['name']}"
|
41
49
|
update_assignee
|
42
50
|
elsif current_assignee['gid'] == current_user['gid']
|
43
|
-
|
44
|
-
elsif cli.
|
45
|
-
|
51
|
+
warn 'You are already assigned to this task'
|
52
|
+
elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
|
53
|
+
warn "Reassigning task to user: #{current_user['name']}"
|
46
54
|
update_assignee
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
50
58
|
def move_if_needed
|
51
|
-
unless project_gid == config.project_gid
|
52
|
-
|
59
|
+
unless project_gid == config.path.project_gid
|
60
|
+
warn 'Task was not moved, this is not implemented for tasks outside current project'
|
53
61
|
return
|
54
62
|
end
|
55
63
|
|
56
64
|
if task_already_in_wip_section?
|
57
|
-
|
65
|
+
warn "Task already in section: #{current_task_section['name']}"
|
58
66
|
else
|
59
|
-
|
67
|
+
warn "Moving task to section: #{wip_section['name']}"
|
60
68
|
move_task
|
61
69
|
end
|
62
70
|
end
|
@@ -76,7 +84,7 @@ module Abt
|
|
76
84
|
end
|
77
85
|
|
78
86
|
def wip_section
|
79
|
-
@wip_section ||= api.get("sections/#{config.wip_section_gid}")
|
87
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: 'name')
|
80
88
|
end
|
81
89
|
|
82
90
|
def move_task
|
@@ -96,7 +104,7 @@ module Abt
|
|
96
104
|
end
|
97
105
|
|
98
106
|
def task
|
99
|
-
@task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name')
|
107
|
+
@task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name,permalink_url')
|
100
108
|
end
|
101
109
|
end
|
102
110
|
end
|
@@ -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.filter { |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,8 +88,8 @@ 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")
|
106
|
-
cli.
|
91
|
+
sections = api.get_paged("projects/#{path.project_gid}/sections")
|
92
|
+
cli.prompt.choice(message, sections)
|
107
93
|
end
|
108
94
|
|
109
95
|
def prompt_workspace
|
@@ -115,10 +101,10 @@ module Abt
|
|
115
101
|
workspace = workspaces.first
|
116
102
|
cli.warn "Selected Asana workspace #{workspace['name']}"
|
117
103
|
else
|
118
|
-
workspace = cli.
|
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
|