abt-cli 0.0.18 → 0.0.23
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 +3 -3
- data/lib/abt.rb +6 -6
- 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 +51 -52
- data/lib/abt/cli/arguments_parser.rb +7 -26
- data/lib/abt/cli/global_commands.rb +23 -0
- 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 +64 -51
- data/lib/abt/docs.rb +48 -25
- data/lib/abt/docs/cli.rb +3 -3
- data/lib/abt/docs/markdown.rb +11 -8
- data/lib/abt/git_config.rb +21 -39
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana/api.rb +9 -9
- data/lib/abt/providers/asana/base_command.rb +20 -38
- data/lib/abt/providers/asana/commands/add.rb +18 -15
- data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
- data/lib/abt/providers/asana/commands/clear.rb +8 -7
- data/lib/abt/providers/asana/commands/current.rb +22 -38
- data/lib/abt/providers/asana/commands/finalize.rb +17 -18
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +20 -13
- data/lib/abt/providers/asana/commands/init.rb +8 -41
- data/lib/abt/providers/asana/commands/pick.rb +27 -26
- data/lib/abt/providers/asana/commands/projects.rb +5 -5
- data/lib/abt/providers/asana/commands/share.rb +6 -8
- data/lib/abt/providers/asana/commands/start.rb +33 -24
- data/lib/abt/providers/asana/commands/tasks.rb +6 -5
- data/lib/abt/providers/asana/configuration.rb +46 -44
- data/lib/abt/providers/asana/path.rb +36 -0
- data/lib/abt/providers/devops/api.rb +23 -11
- data/lib/abt/providers/devops/base_command.rb +22 -43
- data/lib/abt/providers/devops/commands/boards.rb +5 -7
- data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
- data/lib/abt/providers/devops/commands/clear.rb +8 -7
- data/lib/abt/providers/devops/commands/current.rb +24 -49
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +26 -16
- data/lib/abt/providers/devops/commands/init.rb +33 -26
- data/lib/abt/providers/devops/commands/pick.rb +23 -24
- data/lib/abt/providers/devops/commands/share.rb +7 -6
- data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
- data/lib/abt/providers/devops/configuration.rb +27 -56
- data/lib/abt/providers/devops/path.rb +51 -0
- data/lib/abt/providers/git/commands/branch.rb +25 -19
- data/lib/abt/providers/harvest/api.rb +8 -8
- data/lib/abt/providers/harvest/base_command.rb +20 -36
- data/lib/abt/providers/harvest/commands/clear.rb +8 -7
- data/lib/abt/providers/harvest/commands/current.rb +27 -35
- data/lib/abt/providers/harvest/commands/init.rb +10 -40
- data/lib/abt/providers/harvest/commands/pick.rb +15 -12
- data/lib/abt/providers/harvest/commands/projects.rb +5 -5
- data/lib/abt/providers/harvest/commands/share.rb +6 -8
- data/lib/abt/providers/harvest/commands/start.rb +5 -3
- data/lib/abt/providers/harvest/commands/stop.rb +13 -13
- data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
- data/lib/abt/providers/harvest/commands/track.rb +60 -38
- data/lib/abt/providers/harvest/configuration.rb +28 -37
- data/lib/abt/providers/harvest/path.rb +36 -0
- data/lib/abt/version.rb +1 -1
- metadata +18 -6
- data/lib/abt/cli/base_command.rb +0 -61
@@ -6,47 +6,49 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Finalize < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
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
16
|
def perform
|
17
|
-
unless config.
|
18
|
-
cli.abort 'This is a no-op for tasks outside the current project'
|
19
|
-
end
|
17
|
+
abort("This is a no-op for tasks outside the current project") unless project_gid == config.path.project_gid
|
20
18
|
require_task!
|
21
19
|
print_task(project_gid, task)
|
22
20
|
|
21
|
+
maybe_move_task
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def maybe_move_task
|
23
27
|
if task_already_in_finalized_section?
|
24
|
-
|
28
|
+
warn("Task already in section: #{current_task_section['name']}")
|
25
29
|
else
|
26
|
-
|
30
|
+
warn("Moving task to section: #{finalized_section['name']}")
|
27
31
|
move_task
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
31
|
-
private
|
32
|
-
|
33
35
|
def task_already_in_finalized_section?
|
34
36
|
!task_section_membership.nil?
|
35
37
|
end
|
36
38
|
|
37
39
|
def current_task_section
|
38
|
-
task_section_membership&.dig(
|
40
|
+
task_section_membership&.dig("section")
|
39
41
|
end
|
40
42
|
|
41
43
|
def task_section_membership
|
42
|
-
task[
|
43
|
-
membership.dig(
|
44
|
+
task["memberships"].find do |membership|
|
45
|
+
membership.dig("section", "gid") == config.finalized_section_gid
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
47
49
|
def finalized_section
|
48
50
|
@finalized_section ||= api.get("sections/#{config.finalized_section_gid}",
|
49
|
-
opt_fields:
|
51
|
+
opt_fields: "name")
|
50
52
|
end
|
51
53
|
|
52
54
|
def move_task
|
@@ -57,11 +59,8 @@ module Abt
|
|
57
59
|
|
58
60
|
def task
|
59
61
|
@task ||= begin
|
60
|
-
|
61
|
-
|
62
|
-
else
|
63
|
-
api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
|
64
|
-
end
|
62
|
+
api.get("tasks/#{task_gid}",
|
63
|
+
opt_fields: "name,memberships.section.name,permalink_url")
|
65
64
|
end
|
66
65
|
end
|
67
66
|
end
|
@@ -6,41 +6,48 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class HarvestTimeEntryData < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
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
16
|
def perform
|
17
17
|
require_task!
|
18
18
|
ensure_current_is_valid!
|
19
19
|
|
20
|
-
body
|
21
|
-
|
20
|
+
puts Oj.dump(body, mode: :json)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def body
|
26
|
+
{
|
27
|
+
notes: task["name"],
|
22
28
|
external_reference: {
|
23
29
|
id: task_gid.to_i,
|
24
30
|
group_id: project_gid.to_i,
|
25
|
-
permalink: task[
|
31
|
+
permalink: task["permalink_url"]
|
26
32
|
}
|
27
33
|
}
|
28
|
-
|
29
|
-
cli.puts Oj.dump(body, mode: :json)
|
30
34
|
end
|
31
35
|
|
32
|
-
private
|
33
|
-
|
34
36
|
def ensure_current_is_valid!
|
35
|
-
|
37
|
+
abort("Invalid task gid: #{task_gid}") if task.nil?
|
36
38
|
|
37
|
-
return if task[
|
39
|
+
return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
|
38
40
|
|
39
|
-
|
41
|
+
abort("Invalid or unmatching project gid: #{project_gid}")
|
40
42
|
end
|
41
43
|
|
42
44
|
def task
|
43
|
-
@task ||=
|
45
|
+
@task ||= begin
|
46
|
+
warn("Fetching task...")
|
47
|
+
api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url,memberships.project")
|
48
|
+
rescue Abt::HttpError::NotFoundError
|
49
|
+
nil
|
50
|
+
end
|
44
51
|
end
|
45
52
|
end
|
46
53
|
end
|
@@ -6,66 +6,33 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Init < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt init asana"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(cli:, **)
|
17
|
-
@config = Configuration.new(cli: cli)
|
18
|
-
@cli = cli
|
13
|
+
"Pick Asana project for current git repository"
|
19
14
|
end
|
20
15
|
|
21
16
|
def perform
|
22
|
-
|
17
|
+
require_local_config!
|
23
18
|
|
24
19
|
projects # Load projects up front to make it obvious that searches are instant
|
25
|
-
project =
|
20
|
+
project = cli.prompt.search("Select a project", projects)
|
26
21
|
|
27
|
-
config.
|
22
|
+
config.path = Path.from_ids(project_gid: project["gid"])
|
28
23
|
|
29
24
|
print_project(project)
|
30
25
|
end
|
31
26
|
|
32
27
|
private
|
33
28
|
|
34
|
-
def find_search_result
|
35
|
-
cli.warn 'Select a project'
|
36
|
-
|
37
|
-
loop do
|
38
|
-
matches = matches_for_string cli.prompt.text('Enter search')
|
39
|
-
if matches.empty?
|
40
|
-
cli.warn 'No matches'
|
41
|
-
next
|
42
|
-
end
|
43
|
-
|
44
|
-
cli.warn 'Showing the 10 first matches' if matches.size > 10
|
45
|
-
choice = cli.prompt.choice 'Select a project', matches[0...10], true
|
46
|
-
break choice unless choice.nil?
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def matches_for_string(string)
|
51
|
-
search_string = sanitize_string(string)
|
52
|
-
|
53
|
-
projects.select do |project|
|
54
|
-
sanitize_string(project['name']).include?(search_string)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def sanitize_string(string)
|
59
|
-
string.downcase.gsub(/[^\w]/, '')
|
60
|
-
end
|
61
|
-
|
62
29
|
def projects
|
63
30
|
@projects ||= begin
|
64
|
-
|
65
|
-
api.get_paged(
|
31
|
+
warn("Fetching projects...")
|
32
|
+
api.get_paged("projects",
|
66
33
|
workspace: config.workspace_gid,
|
67
34
|
archived: false,
|
68
|
-
opt_fields:
|
35
|
+
opt_fields: "name,permalink_url")
|
69
36
|
end
|
70
37
|
end
|
71
38
|
end
|
@@ -6,67 +6,68 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Pick < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
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
16
|
def self.flags
|
17
17
|
[
|
18
|
-
[
|
18
|
+
["-d", "--dry-run", "Keep existing configuration"]
|
19
19
|
]
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
23
|
+
require_local_config!
|
24
24
|
require_project!
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
warn(project["name"])
|
28
27
|
task = select_task
|
29
28
|
|
30
29
|
print_task(project, task)
|
31
30
|
|
32
31
|
return if flags[:"dry-run"]
|
33
32
|
|
34
|
-
config.
|
35
|
-
config.task_gid = task['gid']
|
33
|
+
config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
|
36
34
|
end
|
37
35
|
|
38
36
|
private
|
39
37
|
|
40
38
|
def project
|
41
|
-
@project ||= api.get("projects/#{project_gid}")
|
39
|
+
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
42
40
|
end
|
43
41
|
|
44
42
|
def select_task
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
task = cli.prompt.choice 'Select a task', tasks, true
|
56
|
-
return task if task
|
43
|
+
section = cli.prompt.choice("Which section?", sections)
|
44
|
+
warn("Fetching tasks...")
|
45
|
+
tasks = tasks_in_section(section)
|
46
|
+
|
47
|
+
if tasks.length.zero?
|
48
|
+
warn("Section is empty")
|
49
|
+
select_task
|
50
|
+
else
|
51
|
+
cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
|
57
52
|
end
|
58
53
|
end
|
59
54
|
|
60
55
|
def tasks_in_section(section)
|
61
|
-
api.get_paged(
|
56
|
+
tasks = api.get_paged(
|
57
|
+
"tasks",
|
58
|
+
section: section["gid"],
|
59
|
+
opt_fields: "name,completed,permalink_url"
|
60
|
+
)
|
61
|
+
|
62
|
+
# The below filtering is the best we can do with Asanas api, see this:
|
63
|
+
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
64
|
+
tasks.reject { |task| task["completed"] }
|
62
65
|
end
|
63
66
|
|
64
67
|
def sections
|
65
68
|
@sections ||= begin
|
66
|
-
|
67
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields:
|
68
|
-
rescue Abt::HttpError::HttpError
|
69
|
-
[]
|
69
|
+
warn("Fetching sections...")
|
70
|
+
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
70
71
|
end
|
71
72
|
end
|
72
73
|
end
|
@@ -6,11 +6,11 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Projects < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
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
16
|
def perform
|
@@ -23,12 +23,12 @@ 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,
|
30
30
|
archived: false,
|
31
|
-
opt_fields:
|
31
|
+
opt_fields: "name"
|
32
32
|
)
|
33
33
|
end
|
34
34
|
end
|
@@ -6,20 +6,18 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Share < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt share asana[:<project-gid>[/<task-gid>]]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Print project/task ARI"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
else
|
22
|
-
cli.print_ari('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
|
@@ -6,26 +6,27 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Start < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
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
16
|
def self.flags
|
17
17
|
[
|
18
|
-
[
|
18
|
+
["-s", "--set", "Set specified task as current"]
|
19
19
|
]
|
20
20
|
end
|
21
21
|
|
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,43 @@ 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
|
-
current_assignee = task.dig('assignee')
|
44
|
-
|
45
45
|
if current_assignee.nil?
|
46
|
-
|
46
|
+
warn("Assigning task to user: #{current_user['name']}")
|
47
47
|
update_assignee
|
48
|
-
elsif current_assignee[
|
49
|
-
|
50
|
-
elsif
|
51
|
-
|
48
|
+
elsif current_assignee["gid"] == current_user["gid"]
|
49
|
+
warn("You are already assigned to this task")
|
50
|
+
elsif should_reassign?
|
51
|
+
warn("Reassigning task to user: #{current_user['name']}")
|
52
52
|
update_assignee
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
def current_assignee
|
57
|
+
task["assignee"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def should_reassign?
|
61
|
+
cli.prompt.boolean("Task is assigned to: #{current_assignee['name']}, take over?")
|
62
|
+
end
|
63
|
+
|
56
64
|
def move_if_needed
|
57
|
-
unless project_gid == config.project_gid
|
58
|
-
|
65
|
+
unless project_gid == config.path.project_gid
|
66
|
+
warn("Task was not moved, this is not implemented for tasks outside current project")
|
59
67
|
return
|
60
68
|
end
|
61
69
|
|
62
70
|
if task_already_in_wip_section?
|
63
|
-
|
71
|
+
warn("Task already in section: #{current_task_section['name']}")
|
64
72
|
else
|
65
|
-
|
73
|
+
warn("Moving task to section: #{wip_section['name']}")
|
66
74
|
move_task
|
67
75
|
end
|
68
76
|
end
|
@@ -72,17 +80,17 @@ module Abt
|
|
72
80
|
end
|
73
81
|
|
74
82
|
def current_task_section
|
75
|
-
task_section_membership&.dig(
|
83
|
+
task_section_membership&.dig("section")
|
76
84
|
end
|
77
85
|
|
78
86
|
def task_section_membership
|
79
|
-
task[
|
80
|
-
membership.dig(
|
87
|
+
task["memberships"].find do |membership|
|
88
|
+
membership.dig("section", "gid") == config.wip_section_gid
|
81
89
|
end
|
82
90
|
end
|
83
91
|
|
84
92
|
def wip_section
|
85
|
-
@wip_section ||= api.get("sections/#{config.wip_section_gid}")
|
93
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: "name")
|
86
94
|
end
|
87
95
|
|
88
96
|
def move_task
|
@@ -92,17 +100,18 @@ module Abt
|
|
92
100
|
end
|
93
101
|
|
94
102
|
def update_assignee
|
95
|
-
body = { data: { assignee: current_user[
|
103
|
+
body = { data: { assignee: current_user["gid"] } }
|
96
104
|
body_json = Oj.dump(body, mode: :json)
|
97
105
|
api.put("tasks/#{task_gid}", body_json)
|
98
106
|
end
|
99
107
|
|
100
108
|
def current_user
|
101
|
-
@current_user ||= api.get(
|
109
|
+
@current_user ||= api.get("users/me", opt_fields: "name")
|
102
110
|
end
|
103
111
|
|
104
112
|
def task
|
105
|
-
@task ||= api.get("tasks/#{task_gid}",
|
113
|
+
@task ||= api.get("tasks/#{task_gid}",
|
114
|
+
opt_fields: "name,memberships.section.name,assignee.name,permalink_url")
|
106
115
|
end
|
107
116
|
end
|
108
117
|
end
|