abt-cli 0.0.16 → 0.0.21
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 +59 -47
- data/lib/abt/cli/arguments_parser.rb +9 -24
- data/lib/abt/cli/global_commands/commands.rb +23 -0
- data/lib/abt/cli/global_commands/examples.rb +23 -0
- data/lib/abt/cli/global_commands/help.rb +23 -0
- data/lib/abt/cli/global_commands/readme.rb +23 -0
- data/lib/abt/cli/global_commands/share.rb +36 -0
- data/lib/abt/cli/global_commands/version.rb +23 -0
- data/lib/abt/cli/prompt.rb +5 -4
- data/lib/abt/docs.rb +32 -15
- data/lib/abt/docs/cli.rb +5 -5
- data/lib/abt/docs/markdown.rb +8 -7
- data/lib/abt/git_config.rb +20 -36
- data/lib/abt/providers/asana/base_command.rb +15 -35
- 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 +5 -9
- 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 +5 -7
- 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 +20 -26
- 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 +15 -40
- 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 +6 -5
- 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 +22 -16
- data/lib/abt/providers/harvest/base_command.rb +16 -34
- 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 +5 -7
- data/lib/abt/providers/harvest/commands/start.rb +1 -1
- 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 +26 -19
- 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 +14 -3
- data/lib/abt/cli/base_command.rb +0 -61
@@ -14,16 +14,16 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
unless config.
|
18
|
-
|
17
|
+
unless project_gid == config.path.project_gid
|
18
|
+
abort 'This is a no-op for tasks outside the current project'
|
19
19
|
end
|
20
20
|
require_task!
|
21
21
|
print_task(project_gid, task)
|
22
22
|
|
23
23
|
if task_already_in_finalized_section?
|
24
|
-
|
24
|
+
warn "Task already in section: #{current_task_section['name']}"
|
25
25
|
else
|
26
|
-
|
26
|
+
warn "Moving task to section: #{finalized_section['name']}"
|
27
27
|
move_task
|
28
28
|
end
|
29
29
|
end
|
@@ -57,11 +57,7 @@ module Abt
|
|
57
57
|
|
58
58
|
def task
|
59
59
|
@task ||= begin
|
60
|
-
|
61
|
-
nil
|
62
|
-
else
|
63
|
-
api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
|
64
|
-
end
|
60
|
+
api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
|
65
61
|
end
|
66
62
|
end
|
67
63
|
end
|
@@ -26,21 +26,26 @@ module Abt
|
|
26
26
|
}
|
27
27
|
}
|
28
28
|
|
29
|
-
|
29
|
+
puts Oj.dump(body, mode: :json)
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def ensure_current_is_valid!
|
35
|
-
|
35
|
+
abort "Invalid task gid: #{task_gid}" if task.nil?
|
36
36
|
|
37
37
|
return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
|
38
38
|
|
39
|
-
|
39
|
+
abort "Invalid or unmatching project gid: #{project_gid}"
|
40
40
|
end
|
41
41
|
|
42
42
|
def task
|
43
|
-
@task ||=
|
43
|
+
@task ||= begin
|
44
|
+
warn 'Fetching task...'
|
45
|
+
api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
|
46
|
+
rescue Abt::HttpError::NotFoundError
|
47
|
+
nil
|
48
|
+
end
|
44
49
|
end
|
45
50
|
end
|
46
51
|
end
|
@@ -19,12 +19,12 @@ module Abt
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def perform
|
22
|
-
|
22
|
+
abort 'Must be run inside a git repository' unless config.local_available?
|
23
23
|
|
24
24
|
projects # Load projects up front to make it obvious that searches are instant
|
25
25
|
project = find_search_result
|
26
26
|
|
27
|
-
config.
|
27
|
+
config.path = Path.from_ids(project['gid'])
|
28
28
|
|
29
29
|
print_project(project)
|
30
30
|
end
|
@@ -32,16 +32,16 @@ module Abt
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def find_search_result
|
35
|
-
|
35
|
+
warn 'Select a project'
|
36
36
|
|
37
37
|
loop do
|
38
38
|
matches = matches_for_string cli.prompt.text('Enter search')
|
39
39
|
if matches.empty?
|
40
|
-
|
40
|
+
warn 'No matches'
|
41
41
|
next
|
42
42
|
end
|
43
43
|
|
44
|
-
|
44
|
+
warn 'Showing the 10 first matches' if matches.size > 10
|
45
45
|
choice = cli.prompt.choice 'Select a project', matches[0...10], true
|
46
46
|
break choice unless choice.nil?
|
47
47
|
end
|
@@ -61,7 +61,7 @@ module Abt
|
|
61
61
|
|
62
62
|
def projects
|
63
63
|
@projects ||= begin
|
64
|
-
|
64
|
+
warn 'Fetching projects...'
|
65
65
|
api.get_paged('projects',
|
66
66
|
workspace: config.workspace_gid,
|
67
67
|
archived: false,
|
@@ -20,10 +20,10 @@ 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_project!
|
25
25
|
|
26
|
-
|
26
|
+
warn project['name']
|
27
27
|
|
28
28
|
task = select_task
|
29
29
|
|
@@ -31,24 +31,23 @@ module Abt
|
|
31
31
|
|
32
32
|
return if flags[:"dry-run"]
|
33
33
|
|
34
|
-
config.
|
35
|
-
config.task_gid = task['gid']
|
34
|
+
config.path = Path.from_ids(project_gid, task['gid'])
|
36
35
|
end
|
37
36
|
|
38
37
|
private
|
39
38
|
|
40
39
|
def project
|
41
|
-
@project ||= api.get("projects/#{project_gid}")
|
40
|
+
@project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
|
42
41
|
end
|
43
42
|
|
44
43
|
def select_task
|
45
44
|
loop do
|
46
45
|
section = cli.prompt.choice 'Which section?', sections
|
47
|
-
|
46
|
+
warn 'Fetching tasks...'
|
48
47
|
tasks = tasks_in_section(section)
|
49
48
|
|
50
49
|
if tasks.length.zero?
|
51
|
-
|
50
|
+
warn 'Section is empty'
|
52
51
|
next
|
53
52
|
end
|
54
53
|
|
@@ -58,15 +57,21 @@ module Abt
|
|
58
57
|
end
|
59
58
|
|
60
59
|
def tasks_in_section(section)
|
61
|
-
api.get_paged(
|
60
|
+
tasks = api.get_paged(
|
61
|
+
'tasks',
|
62
|
+
section: section['gid'],
|
63
|
+
opt_fields: 'name,completed,permalink_url'
|
64
|
+
)
|
65
|
+
|
66
|
+
# The below filtering is the best we can do with Asanas api, see this:
|
67
|
+
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
68
|
+
tasks.select { |task| !task['completed'] }
|
62
69
|
end
|
63
70
|
|
64
71
|
def sections
|
65
72
|
@sections ||= begin
|
66
|
-
|
73
|
+
warn 'Fetching sections...'
|
67
74
|
api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
|
68
|
-
rescue Abt::HttpError::HttpError
|
69
|
-
[]
|
70
75
|
end
|
71
76
|
end
|
72
77
|
end
|
@@ -10,16 +10,14 @@ module Abt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
'Print project/task
|
13
|
+
'Print project/task ARI'
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
else
|
22
|
-
cli.print_scheme_argument('asana', "#{project_gid}/#{task_gid}")
|
17
|
+
if path != ''
|
18
|
+
cli.print_ari('asana', path)
|
19
|
+
elsif cli.output.isatty
|
20
|
+
warn 'No configuration for project. Did you initialize Asana?'
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
@@ -22,10 +22,11 @@ module Abt
|
|
22
22
|
def perform
|
23
23
|
require_task!
|
24
24
|
|
25
|
-
|
25
|
+
print_task(project_gid, task)
|
26
26
|
|
27
27
|
update_assignee_if_needed
|
28
28
|
move_if_needed
|
29
|
+
maybe_override_current_task
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
@@ -33,36 +34,37 @@ module Abt
|
|
33
34
|
def maybe_override_current_task
|
34
35
|
return unless flags[:set]
|
35
36
|
return if path.nil?
|
36
|
-
return if
|
37
|
+
return if path == config.path
|
37
38
|
return unless config.local_available?
|
38
39
|
|
39
|
-
|
40
|
+
config.path = path
|
41
|
+
warn 'Current task updated'
|
40
42
|
end
|
41
43
|
|
42
44
|
def update_assignee_if_needed
|
43
45
|
current_assignee = task.dig('assignee')
|
44
46
|
|
45
47
|
if current_assignee.nil?
|
46
|
-
|
48
|
+
warn "Assigning task to user: #{current_user['name']}"
|
47
49
|
update_assignee
|
48
50
|
elsif current_assignee['gid'] == current_user['gid']
|
49
|
-
|
51
|
+
warn 'You are already assigned to this task'
|
50
52
|
elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
|
51
|
-
|
53
|
+
warn "Reassigning task to user: #{current_user['name']}"
|
52
54
|
update_assignee
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
56
58
|
def move_if_needed
|
57
|
-
unless project_gid == config.project_gid
|
58
|
-
|
59
|
+
unless project_gid == config.path.project_gid
|
60
|
+
warn 'Task was not moved, this is not implemented for tasks outside current project'
|
59
61
|
return
|
60
62
|
end
|
61
63
|
|
62
64
|
if task_already_in_wip_section?
|
63
|
-
|
65
|
+
warn "Task already in section: #{current_task_section['name']}"
|
64
66
|
else
|
65
|
-
|
67
|
+
warn "Moving task to section: #{wip_section['name']}"
|
66
68
|
move_task
|
67
69
|
end
|
68
70
|
end
|
@@ -82,7 +84,7 @@ module Abt
|
|
82
84
|
end
|
83
85
|
|
84
86
|
def wip_section
|
85
|
-
@wip_section ||= api.get("sections/#{config.wip_section_gid}")
|
87
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: 'name')
|
86
88
|
end
|
87
89
|
|
88
90
|
def move_task
|
@@ -102,7 +104,7 @@ module Abt
|
|
102
104
|
end
|
103
105
|
|
104
106
|
def task
|
105
|
-
@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')
|
106
108
|
end
|
107
109
|
end
|
108
110
|
end
|
@@ -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,29 +45,18 @@ module Abt
|
|
46
45
|
@finalized_section_gid ||= git['finalizedSectionGid'] || prompt_finalized_section['gid']
|
47
46
|
end
|
48
47
|
|
49
|
-
def project_gid=(value)
|
50
|
-
return if project_gid == value
|
51
|
-
|
52
|
-
clear_local(verbose: false)
|
53
|
-
git['projectGid'] = value unless value.nil?
|
54
|
-
end
|
55
|
-
|
56
|
-
def task_gid=(value)
|
57
|
-
git['taskGid'] = value
|
58
|
-
end
|
59
|
-
|
60
48
|
def clear_local(verbose: true)
|
61
49
|
git.clear(output: verbose ? cli.err_output : nil)
|
62
50
|
end
|
63
51
|
|
64
52
|
def clear_global(verbose: true)
|
65
|
-
|
53
|
+
git_global.clear(output: verbose ? cli.err_output : nil)
|
66
54
|
end
|
67
55
|
|
68
56
|
def access_token
|
69
|
-
return
|
57
|
+
return git_global['accessToken'] unless git_global['accessToken'].nil?
|
70
58
|
|
71
|
-
|
59
|
+
git_global['accessToken'] = cli.prompt.text([
|
72
60
|
'Please provide your personal access token for Asana.',
|
73
61
|
'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
|
74
62
|
'',
|
@@ -78,7 +66,13 @@ module Abt
|
|
78
66
|
|
79
67
|
private
|
80
68
|
|
81
|
-
|
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
|
82
76
|
|
83
77
|
def prompt_finalized_section
|
84
78
|
section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
|
@@ -94,23 +88,23 @@ module Abt
|
|
94
88
|
|
95
89
|
def prompt_section(message)
|
96
90
|
cli.warn 'Fetching sections...'
|
97
|
-
sections = api.get_paged("projects/#{project_gid}/sections")
|
91
|
+
sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: 'name')
|
98
92
|
cli.prompt.choice(message, sections)
|
99
93
|
end
|
100
94
|
|
101
95
|
def prompt_workspace
|
102
96
|
cli.warn 'Fetching workspaces...'
|
103
|
-
workspaces = api.get_paged('workspaces')
|
97
|
+
workspaces = api.get_paged('workspaces', opt_fields: 'name')
|
104
98
|
if workspaces.empty?
|
105
99
|
cli.abort 'Your asana access token does not have access to any workspaces'
|
106
100
|
elsif workspaces.one?
|
107
101
|
workspace = workspaces.first
|
108
|
-
cli.warn "Selected Asana workspace #{workspace['name']}"
|
102
|
+
cli.warn "Selected Asana workspace: #{workspace['name']}"
|
109
103
|
else
|
110
104
|
workspace = cli.prompt.choice('Select Asana workspace', workspaces)
|
111
105
|
end
|
112
106
|
|
113
|
-
|
107
|
+
git_global['workspaceGid'] = workspace['gid']
|
114
108
|
workspace
|
115
109
|
end
|
116
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
|