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,69 +5,54 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Current < BaseCommand
|
8
|
-
def self.
|
9
|
-
'current asana[:<project-gid>[/<task-gid>]]'
|
8
|
+
def self.usage
|
9
|
+
'abt current asana[:<project-gid>[/<task-gid>]]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'Get or set project and or task 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_project!
|
20
|
+
ensure_valid_configuration!
|
18
21
|
|
19
|
-
if
|
20
|
-
|
21
|
-
|
22
|
-
cli.warn 'Updating configuration'
|
23
|
-
update_configuration
|
22
|
+
if path != config.path
|
23
|
+
config.path = path
|
24
|
+
warn 'Configuration updated'
|
24
25
|
end
|
25
|
-
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
def show_current_configuration
|
30
|
-
if task_gid.nil?
|
31
|
-
print_project(project)
|
32
|
-
else
|
33
|
-
print_task(project, task)
|
34
|
-
end
|
27
|
+
print_configuration
|
35
28
|
end
|
36
29
|
|
37
|
-
|
38
|
-
ensure_project_is_valid!
|
39
|
-
config.project_gid = project_gid
|
40
|
-
|
41
|
-
if task_gid.nil?
|
42
|
-
print_project(project)
|
43
|
-
config.task_gid = nil
|
44
|
-
else
|
45
|
-
ensure_task_is_valid!
|
46
|
-
config.task_gid = task_gid
|
47
|
-
|
48
|
-
print_task(project, task)
|
49
|
-
end
|
50
|
-
end
|
30
|
+
private
|
51
31
|
|
52
|
-
def
|
53
|
-
|
32
|
+
def print_configuration
|
33
|
+
task_gid.nil? ? print_project(project) : print_task(project, task)
|
54
34
|
end
|
55
35
|
|
56
|
-
def
|
57
|
-
|
36
|
+
def ensure_valid_configuration!
|
37
|
+
abort "Invalid project: #{project_gid}" if project.nil?
|
38
|
+
abort "Invalid task: #{task_gid}" if task_gid && task.nil?
|
58
39
|
end
|
59
40
|
|
60
41
|
def project
|
61
42
|
@project ||= begin
|
62
|
-
|
43
|
+
warn 'Fetching project...'
|
63
44
|
api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
|
45
|
+
rescue Abt::HttpError::NotFoundError
|
46
|
+
nil
|
64
47
|
end
|
65
48
|
end
|
66
49
|
|
67
50
|
def task
|
68
51
|
@task ||= begin
|
69
|
-
|
52
|
+
warn 'Fetching task...'
|
70
53
|
api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
|
54
|
+
rescue Abt::HttpError::NotFoundError
|
55
|
+
nil
|
71
56
|
end
|
72
57
|
end
|
73
58
|
end
|
@@ -5,25 +5,25 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Finalize < BaseCommand
|
8
|
-
def self.
|
9
|
-
'finalize asana[:<project-gid>/<task-gid>]'
|
8
|
+
def self.usage
|
9
|
+
'abt finalize asana[:<project-gid>/<task-gid>]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'Move current/specified task to section (column) for finalized tasks'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
unless config.
|
18
|
-
|
16
|
+
def perform
|
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
|
@@ -5,15 +5,15 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class HarvestTimeEntryData < BaseCommand
|
8
|
-
def self.
|
9
|
-
'harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
|
8
|
+
def self.usage
|
9
|
+
'abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def perform
|
17
17
|
require_task!
|
18
18
|
ensure_current_is_valid!
|
19
19
|
|
@@ -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
|
@@ -5,8 +5,8 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Init < BaseCommand
|
8
|
-
def self.
|
9
|
-
'init asana'
|
8
|
+
def self.usage
|
9
|
+
'abt init asana'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
@@ -18,13 +18,13 @@ module Abt
|
|
18
18
|
@cli = cli
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
|
21
|
+
def perform
|
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,
|
@@ -5,42 +5,49 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Pick < BaseCommand
|
8
|
-
def self.
|
9
|
-
'pick asana[:<project-gid>]'
|
8
|
+
def self.usage
|
9
|
+
'abt pick asana[:<project-gid>]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'Pick task for current git repository'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
16
|
+
def self.flags
|
17
|
+
[
|
18
|
+
['-d', '--dry-run', 'Keep existing configuration']
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
abort 'Must be run inside a git repository' unless config.local_available?
|
18
24
|
require_project!
|
19
25
|
|
20
|
-
|
26
|
+
warn project['name']
|
21
27
|
|
22
28
|
task = select_task
|
23
29
|
|
24
|
-
config.project_gid = project_gid # We might have gotten the project ID as an argument
|
25
|
-
config.task_gid = task['gid']
|
26
|
-
|
27
30
|
print_task(project, task)
|
31
|
+
|
32
|
+
return if flags[:"dry-run"]
|
33
|
+
|
34
|
+
config.path = Path.from_ids(project_gid, task['gid'])
|
28
35
|
end
|
29
36
|
|
30
37
|
private
|
31
38
|
|
32
39
|
def project
|
33
|
-
@project ||= api.get("projects/#{project_gid}")
|
40
|
+
@project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
|
34
41
|
end
|
35
42
|
|
36
43
|
def select_task
|
37
44
|
loop do
|
38
45
|
section = cli.prompt.choice 'Which section?', sections
|
39
|
-
|
46
|
+
warn 'Fetching tasks...'
|
40
47
|
tasks = tasks_in_section(section)
|
41
48
|
|
42
49
|
if tasks.length.zero?
|
43
|
-
|
50
|
+
warn 'Section is empty'
|
44
51
|
next
|
45
52
|
end
|
46
53
|
|
@@ -50,15 +57,21 @@ module Abt
|
|
50
57
|
end
|
51
58
|
|
52
59
|
def tasks_in_section(section)
|
53
|
-
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'] }
|
54
69
|
end
|
55
70
|
|
56
71
|
def sections
|
57
72
|
@sections ||= begin
|
58
|
-
|
73
|
+
warn 'Fetching sections...'
|
59
74
|
api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
|
60
|
-
rescue Abt::HttpError::HttpError
|
61
|
-
[]
|
62
75
|
end
|
63
76
|
end
|
64
77
|
end
|
@@ -5,15 +5,15 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Projects < BaseCommand
|
8
|
-
def self.
|
9
|
-
'projects asana'
|
8
|
+
def self.usage
|
9
|
+
'abt projects asana'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
13
|
'List all available projects - useful for piping into grep etc.'
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def perform
|
17
17
|
projects.map do |project|
|
18
18
|
print_project(project)
|
19
19
|
end
|
@@ -23,7 +23,7 @@ module Abt
|
|
23
23
|
|
24
24
|
def projects
|
25
25
|
@projects ||= begin
|
26
|
-
|
26
|
+
warn 'Fetching projects...'
|
27
27
|
api.get_paged(
|
28
28
|
'projects',
|
29
29
|
workspace: config.workspace_gid,
|
@@ -5,22 +5,18 @@ module Abt
|
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
7
|
class Share < BaseCommand
|
8
|
-
def self.
|
9
|
-
'share asana[:<project-gid>[/<task-gid>]]'
|
8
|
+
def self.usage
|
9
|
+
'abt share asana[:<project-gid>[/<task-gid>]]'
|
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
|
-
def
|
16
|
+
def perform
|
17
17
|
require_project!
|
18
18
|
|
19
|
-
|
20
|
-
cli.print_provider_command('asana', project_gid)
|
21
|
-
else
|
22
|
-
cli.print_provider_command('asana', "#{project_gid}/#{task_gid}")
|
23
|
-
end
|
19
|
+
cli.print_ari('asana', path)
|
24
20
|
end
|
25
21
|
end
|
26
22
|
end
|
@@ -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
|
-
|
51
|
+
warn 'You are already assigned to this task'
|
44
52
|
elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
|
45
|
-
|
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
|