abt-cli 0.0.21 → 0.0.22
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 +1 -1
- data/lib/abt/ari_list.rb +1 -1
- data/lib/abt/base_command.rb +7 -7
- data/lib/abt/cli.rb +27 -40
- data/lib/abt/cli/arguments_parser.rb +5 -9
- data/lib/abt/cli/global_commands.rb +23 -0
- data/lib/abt/cli/global_commands/commands.rb +2 -2
- data/lib/abt/cli/global_commands/examples.rb +2 -2
- data/lib/abt/cli/global_commands/help.rb +2 -2
- data/lib/abt/cli/global_commands/readme.rb +2 -2
- data/lib/abt/cli/global_commands/share.rb +6 -6
- data/lib/abt/cli/global_commands/version.rb +2 -2
- data/lib/abt/cli/prompt.rb +51 -20
- data/lib/abt/docs.rb +39 -33
- data/lib/abt/docs/cli.rb +3 -3
- data/lib/abt/docs/markdown.rb +5 -5
- data/lib/abt/git_config.rb +4 -6
- data/lib/abt/providers/asana/api.rb +9 -9
- data/lib/abt/providers/asana/base_command.rb +8 -10
- data/lib/abt/providers/asana/commands/add.rb +13 -12
- data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
- data/lib/abt/providers/asana/commands/clear.rb +7 -8
- data/lib/abt/providers/asana/commands/current.rb +14 -14
- data/lib/abt/providers/asana/commands/finalize.rb +11 -12
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +11 -11
- data/lib/abt/providers/asana/commands/init.rb +8 -41
- data/lib/abt/providers/asana/commands/pick.rb +17 -17
- data/lib/abt/providers/asana/commands/projects.rb +5 -5
- data/lib/abt/providers/asana/commands/share.rb +5 -5
- data/lib/abt/providers/asana/commands/start.rb +21 -20
- data/lib/abt/providers/asana/commands/tasks.rb +6 -6
- data/lib/abt/providers/asana/configuration.rb +25 -25
- data/lib/abt/providers/asana/path.rb +5 -5
- data/lib/abt/providers/devops/api.rb +12 -12
- data/lib/abt/providers/devops/base_command.rb +10 -10
- data/lib/abt/providers/devops/commands/boards.rb +5 -7
- data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
- data/lib/abt/providers/devops/commands/clear.rb +7 -8
- data/lib/abt/providers/devops/commands/current.rb +17 -17
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +13 -13
- data/lib/abt/providers/devops/commands/init.rb +17 -13
- data/lib/abt/providers/devops/commands/pick.rb +11 -11
- data/lib/abt/providers/devops/commands/share.rb +5 -5
- data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
- data/lib/abt/providers/devops/configuration.rb +19 -15
- data/lib/abt/providers/devops/path.rb +5 -4
- data/lib/abt/providers/git/commands/branch.rb +17 -19
- data/lib/abt/providers/harvest/api.rb +8 -8
- data/lib/abt/providers/harvest/base_command.rb +6 -8
- data/lib/abt/providers/harvest/commands/clear.rb +7 -8
- data/lib/abt/providers/harvest/commands/current.rb +13 -13
- data/lib/abt/providers/harvest/commands/init.rb +10 -38
- data/lib/abt/providers/harvest/commands/pick.rb +11 -11
- data/lib/abt/providers/harvest/commands/projects.rb +5 -5
- data/lib/abt/providers/harvest/commands/share.rb +5 -5
- data/lib/abt/providers/harvest/commands/start.rb +5 -3
- data/lib/abt/providers/harvest/commands/stop.rb +12 -12
- data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
- data/lib/abt/providers/harvest/commands/track.rb +21 -20
- data/lib/abt/providers/harvest/configuration.rb +18 -18
- data/lib/abt/providers/harvest/path.rb +5 -5
- data/lib/abt/version.rb +1 -1
- metadata +6 -5
@@ -6,24 +6,22 @@ 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 project_gid == config.path.project_gid
|
18
|
-
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
|
|
23
21
|
if task_already_in_finalized_section?
|
24
|
-
warn
|
22
|
+
warn("Task already in section: #{current_task_section['name']}")
|
25
23
|
else
|
26
|
-
warn
|
24
|
+
warn("Moving task to section: #{finalized_section['name']}")
|
27
25
|
move_task
|
28
26
|
end
|
29
27
|
end
|
@@ -35,18 +33,18 @@ module Abt
|
|
35
33
|
end
|
36
34
|
|
37
35
|
def current_task_section
|
38
|
-
task_section_membership&.dig(
|
36
|
+
task_section_membership&.dig("section")
|
39
37
|
end
|
40
38
|
|
41
39
|
def task_section_membership
|
42
|
-
task[
|
43
|
-
membership.dig(
|
40
|
+
task["memberships"].find do |membership|
|
41
|
+
membership.dig("section", "gid") == config.finalized_section_gid
|
44
42
|
end
|
45
43
|
end
|
46
44
|
|
47
45
|
def finalized_section
|
48
46
|
@finalized_section ||= api.get("sections/#{config.finalized_section_gid}",
|
49
|
-
opt_fields:
|
47
|
+
opt_fields: "name")
|
50
48
|
end
|
51
49
|
|
52
50
|
def move_task
|
@@ -57,7 +55,8 @@ module Abt
|
|
57
55
|
|
58
56
|
def task
|
59
57
|
@task ||= begin
|
60
|
-
api.get("tasks/#{task_gid}",
|
58
|
+
api.get("tasks/#{task_gid}",
|
59
|
+
opt_fields: "name,memberships.section.name,permalink_url")
|
61
60
|
end
|
62
61
|
end
|
63
62
|
end
|
@@ -6,11 +6,11 @@ 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
|
@@ -18,11 +18,11 @@ module Abt
|
|
18
18
|
ensure_current_is_valid!
|
19
19
|
|
20
20
|
body = {
|
21
|
-
notes: task[
|
21
|
+
notes: task["name"],
|
22
22
|
external_reference: {
|
23
23
|
id: task_gid.to_i,
|
24
24
|
group_id: project_gid.to_i,
|
25
|
-
permalink: task[
|
25
|
+
permalink: task["permalink_url"]
|
26
26
|
}
|
27
27
|
}
|
28
28
|
|
@@ -32,19 +32,19 @@ module Abt
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def ensure_current_is_valid!
|
35
|
-
abort
|
35
|
+
abort("Invalid task gid: #{task_gid}") if task.nil?
|
36
36
|
|
37
|
-
return if task[
|
37
|
+
return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
|
38
38
|
|
39
|
-
abort
|
39
|
+
abort("Invalid or unmatching project gid: #{project_gid}")
|
40
40
|
end
|
41
41
|
|
42
42
|
def task
|
43
43
|
@task ||= begin
|
44
|
-
warn
|
45
|
-
api.get("tasks/#{task_gid}", opt_fields:
|
46
|
-
|
47
|
-
|
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
48
|
end
|
49
49
|
end
|
50
50
|
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
|
-
abort
|
17
|
+
abort("Must be run inside a git repository") unless config.local_available?
|
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.path = Path.from_ids(project[
|
22
|
+
config.path = Path.from_ids(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
|
-
warn 'Select a project'
|
36
|
-
|
37
|
-
loop do
|
38
|
-
matches = matches_for_string cli.prompt.text('Enter search')
|
39
|
-
if matches.empty?
|
40
|
-
warn 'No matches'
|
41
|
-
next
|
42
|
-
end
|
43
|
-
|
44
|
-
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
|
-
warn
|
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,24 +6,24 @@ 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
|
-
abort
|
23
|
+
abort("Must be run inside a git repository") unless config.local_available?
|
24
24
|
require_project!
|
25
25
|
|
26
|
-
warn
|
26
|
+
warn(project["name"])
|
27
27
|
|
28
28
|
task = select_task
|
29
29
|
|
@@ -31,47 +31,47 @@ module Abt
|
|
31
31
|
|
32
32
|
return if flags[:"dry-run"]
|
33
33
|
|
34
|
-
config.path = Path.from_ids(project_gid, task[
|
34
|
+
config.path = Path.from_ids(project_gid, task["gid"])
|
35
35
|
end
|
36
36
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def project
|
40
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields:
|
40
|
+
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
41
41
|
end
|
42
42
|
|
43
43
|
def select_task
|
44
44
|
loop do
|
45
|
-
section = cli.prompt.choice
|
46
|
-
warn
|
45
|
+
section = cli.prompt.choice("Which section?", sections)
|
46
|
+
warn("Fetching tasks...")
|
47
47
|
tasks = tasks_in_section(section)
|
48
48
|
|
49
49
|
if tasks.length.zero?
|
50
|
-
warn
|
50
|
+
warn("Section is empty")
|
51
51
|
next
|
52
52
|
end
|
53
53
|
|
54
|
-
task = cli.prompt.choice
|
54
|
+
task = cli.prompt.choice("Select a task", tasks, nil_option: true)
|
55
55
|
return task if task
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
59
|
def tasks_in_section(section)
|
60
60
|
tasks = api.get_paged(
|
61
|
-
|
62
|
-
section: section[
|
63
|
-
opt_fields:
|
61
|
+
"tasks",
|
62
|
+
section: section["gid"],
|
63
|
+
opt_fields: "name,completed,permalink_url"
|
64
64
|
)
|
65
65
|
|
66
66
|
# The below filtering is the best we can do with Asanas api, see this:
|
67
67
|
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
68
|
-
tasks.
|
68
|
+
tasks.reject { |task| task["completed"] }
|
69
69
|
end
|
70
70
|
|
71
71
|
def sections
|
72
72
|
@sections ||= begin
|
73
|
-
warn
|
74
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields:
|
73
|
+
warn("Fetching sections...")
|
74
|
+
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
75
75
|
end
|
76
76
|
end
|
77
77
|
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
|
-
warn
|
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,18 +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
|
-
if path !=
|
18
|
-
cli.print_ari(
|
17
|
+
if path != ""
|
18
|
+
cli.print_ari("asana", path)
|
19
19
|
elsif cli.output.isatty
|
20
|
-
warn
|
20
|
+
warn("No configuration for project. Did you initialize Asana?")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -6,16 +6,16 @@ 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
|
|
@@ -38,33 +38,33 @@ module Abt
|
|
38
38
|
return unless config.local_available?
|
39
39
|
|
40
40
|
config.path = path
|
41
|
-
warn
|
41
|
+
warn("Current task updated")
|
42
42
|
end
|
43
43
|
|
44
44
|
def update_assignee_if_needed
|
45
|
-
current_assignee = task
|
45
|
+
current_assignee = task["assignee"]
|
46
46
|
|
47
47
|
if current_assignee.nil?
|
48
|
-
warn
|
48
|
+
warn("Assigning task to user: #{current_user['name']}")
|
49
49
|
update_assignee
|
50
|
-
elsif current_assignee[
|
51
|
-
warn
|
52
|
-
elsif cli.prompt.boolean
|
53
|
-
warn
|
50
|
+
elsif current_assignee["gid"] == current_user["gid"]
|
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']}")
|
54
54
|
update_assignee
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
def move_if_needed
|
59
59
|
unless project_gid == config.path.project_gid
|
60
|
-
warn
|
60
|
+
warn("Task was not moved, this is not implemented for tasks outside current project")
|
61
61
|
return
|
62
62
|
end
|
63
63
|
|
64
64
|
if task_already_in_wip_section?
|
65
|
-
warn
|
65
|
+
warn("Task already in section: #{current_task_section['name']}")
|
66
66
|
else
|
67
|
-
warn
|
67
|
+
warn("Moving task to section: #{wip_section['name']}")
|
68
68
|
move_task
|
69
69
|
end
|
70
70
|
end
|
@@ -74,17 +74,17 @@ module Abt
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def current_task_section
|
77
|
-
task_section_membership&.dig(
|
77
|
+
task_section_membership&.dig("section")
|
78
78
|
end
|
79
79
|
|
80
80
|
def task_section_membership
|
81
|
-
task[
|
82
|
-
membership.dig(
|
81
|
+
task["memberships"].find do |membership|
|
82
|
+
membership.dig("section", "gid") == config.wip_section_gid
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
86
86
|
def wip_section
|
87
|
-
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields:
|
87
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: "name")
|
88
88
|
end
|
89
89
|
|
90
90
|
def move_task
|
@@ -94,17 +94,18 @@ module Abt
|
|
94
94
|
end
|
95
95
|
|
96
96
|
def update_assignee
|
97
|
-
body = { data: { assignee: current_user[
|
97
|
+
body = { data: { assignee: current_user["gid"] } }
|
98
98
|
body_json = Oj.dump(body, mode: :json)
|
99
99
|
api.put("tasks/#{task_gid}", body_json)
|
100
100
|
end
|
101
101
|
|
102
102
|
def current_user
|
103
|
-
@current_user ||= api.get(
|
103
|
+
@current_user ||= api.get("users/me", opt_fields: "name")
|
104
104
|
end
|
105
105
|
|
106
106
|
def task
|
107
|
-
@task ||= api.get("tasks/#{task_gid}",
|
107
|
+
@task ||= api.get("tasks/#{task_gid}",
|
108
|
+
opt_fields: "name,memberships.section.name,assignee.name,permalink_url")
|
108
109
|
end
|
109
110
|
end
|
110
111
|
end
|
@@ -6,11 +6,11 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Tasks < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
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
16
|
def perform
|
@@ -25,15 +25,15 @@ module Abt
|
|
25
25
|
|
26
26
|
def project
|
27
27
|
@project ||= begin
|
28
|
-
api.get("projects/#{project_gid}", opt_fields:
|
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
|
-
warn
|
35
|
-
tasks = api.get_paged(
|
36
|
-
tasks.
|
34
|
+
warn("Fetching tasks...")
|
35
|
+
tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
|
36
|
+
tasks.reject { |task| task["completed"] }
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|