abt-cli 0.0.20 → 0.0.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +3 -3
- data/lib/abt.rb +6 -6
- data/lib/abt/ari.rb +2 -2
- data/lib/abt/ari_list.rb +1 -1
- data/lib/abt/base_command.rb +7 -7
- data/lib/abt/cli.rb +49 -47
- data/lib/abt/cli/arguments_parser.rb +6 -3
- 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 +71 -56
- data/lib/abt/docs.rb +48 -26
- data/lib/abt/docs/cli.rb +3 -3
- data/lib/abt/docs/markdown.rb +10 -7
- data/lib/abt/git_config.rb +4 -6
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana/api.rb +9 -9
- data/lib/abt/providers/asana/base_command.rb +12 -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 -15
- data/lib/abt/providers/asana/commands/finalize.rb +17 -14
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
- data/lib/abt/providers/asana/commands/init.rb +8 -41
- data/lib/abt/providers/asana/commands/pick.rb +22 -26
- data/lib/abt/providers/asana/commands/projects.rb +5 -5
- data/lib/abt/providers/asana/commands/share.rb +7 -5
- data/lib/abt/providers/asana/commands/start.rb +28 -21
- data/lib/abt/providers/asana/commands/tasks.rb +6 -6
- data/lib/abt/providers/asana/configuration.rb +37 -29
- data/lib/abt/providers/asana/path.rb +6 -6
- data/lib/abt/providers/devops/api.rb +12 -12
- data/lib/abt/providers/devops/base_command.rb +14 -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 -18
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
- data/lib/abt/providers/devops/commands/init.rb +21 -14
- data/lib/abt/providers/devops/commands/pick.rb +37 -19
- data/lib/abt/providers/devops/commands/share.rb +7 -5
- data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
- data/lib/abt/providers/devops/configuration.rb +15 -15
- data/lib/abt/providers/devops/path.rb +7 -6
- data/lib/abt/providers/git/commands/branch.rb +23 -21
- data/lib/abt/providers/harvest/api.rb +8 -8
- data/lib/abt/providers/harvest/base_command.rb +10 -8
- data/lib/abt/providers/harvest/commands/clear.rb +7 -8
- data/lib/abt/providers/harvest/commands/current.rb +13 -14
- data/lib/abt/providers/harvest/commands/init.rb +10 -39
- data/lib/abt/providers/harvest/commands/pick.rb +15 -11
- data/lib/abt/providers/harvest/commands/projects.rb +5 -5
- data/lib/abt/providers/harvest/commands/share.rb +7 -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 +52 -37
- data/lib/abt/providers/harvest/configuration.rb +18 -18
- data/lib/abt/providers/harvest/path.rb +6 -6
- data/lib/abt/version.rb +1 -1
- metadata +12 -5
@@ -6,72 +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
|
-
warn
|
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.path = Path.from_ids(project_gid, task[
|
33
|
+
config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
|
35
34
|
end
|
36
35
|
|
37
36
|
private
|
38
37
|
|
39
38
|
def project
|
40
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields:
|
39
|
+
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
41
40
|
end
|
42
41
|
|
43
42
|
def select_task
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
task = cli.prompt.choice 'Select a task', tasks, true
|
55
|
-
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
|
56
52
|
end
|
57
53
|
end
|
58
54
|
|
59
55
|
def tasks_in_section(section)
|
60
56
|
tasks = api.get_paged(
|
61
|
-
|
62
|
-
section: section[
|
63
|
-
opt_fields:
|
57
|
+
"tasks",
|
58
|
+
section: section["gid"],
|
59
|
+
opt_fields: "name,completed,permalink_url"
|
64
60
|
)
|
65
61
|
|
66
62
|
# The below filtering is the best we can do with Asanas api, see this:
|
67
63
|
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
68
|
-
tasks.
|
64
|
+
tasks.reject { |task| task["completed"] }
|
69
65
|
end
|
70
66
|
|
71
67
|
def sections
|
72
68
|
@sections ||= begin
|
73
|
-
warn
|
74
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields:
|
69
|
+
warn("Fetching sections...")
|
70
|
+
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
75
71
|
end
|
76
72
|
end
|
77
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
|
-
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,17 +6,19 @@ 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
|
-
cli.
|
17
|
+
if path != ""
|
18
|
+
cli.print_ari("asana", path)
|
19
|
+
elsif cli.output.isatty
|
20
|
+
warn("No configuration for project. Did you initialize Asana?")
|
21
|
+
end
|
20
22
|
end
|
21
23
|
end
|
22
24
|
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,39 @@ 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.dig('assignee')
|
46
|
-
|
47
45
|
if current_assignee.nil?
|
48
|
-
warn
|
46
|
+
warn("Assigning task to user: #{current_user['name']}")
|
49
47
|
update_assignee
|
50
|
-
elsif current_assignee[
|
51
|
-
warn
|
52
|
-
elsif
|
53
|
-
warn
|
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']}")
|
54
52
|
update_assignee
|
55
53
|
end
|
56
54
|
end
|
57
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
|
+
|
58
64
|
def move_if_needed
|
59
65
|
unless project_gid == config.path.project_gid
|
60
|
-
warn
|
66
|
+
warn("Task was not moved, this is not implemented for tasks outside current project")
|
61
67
|
return
|
62
68
|
end
|
63
69
|
|
64
70
|
if task_already_in_wip_section?
|
65
|
-
warn
|
71
|
+
warn("Task already in section: #{current_task_section['name']}")
|
66
72
|
else
|
67
|
-
warn
|
73
|
+
warn("Moving task to section: #{wip_section['name']}")
|
68
74
|
move_task
|
69
75
|
end
|
70
76
|
end
|
@@ -74,17 +80,17 @@ module Abt
|
|
74
80
|
end
|
75
81
|
|
76
82
|
def current_task_section
|
77
|
-
task_section_membership&.dig(
|
83
|
+
task_section_membership&.dig("section")
|
78
84
|
end
|
79
85
|
|
80
86
|
def task_section_membership
|
81
|
-
task[
|
82
|
-
membership.dig(
|
87
|
+
task["memberships"].find do |membership|
|
88
|
+
membership.dig("section", "gid") == config.wip_section_gid
|
83
89
|
end
|
84
90
|
end
|
85
91
|
|
86
92
|
def wip_section
|
87
|
-
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields:
|
93
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: "name")
|
88
94
|
end
|
89
95
|
|
90
96
|
def move_task
|
@@ -94,17 +100,18 @@ module Abt
|
|
94
100
|
end
|
95
101
|
|
96
102
|
def update_assignee
|
97
|
-
body = { data: { assignee: current_user[
|
103
|
+
body = { data: { assignee: current_user["gid"] } }
|
98
104
|
body_json = Oj.dump(body, mode: :json)
|
99
105
|
api.put("tasks/#{task_gid}", body_json)
|
100
106
|
end
|
101
107
|
|
102
108
|
def current_user
|
103
|
-
@current_user ||= api.get(
|
109
|
+
@current_user ||= api.get("users/me", opt_fields: "name")
|
104
110
|
end
|
105
111
|
|
106
112
|
def task
|
107
|
-
@task ||= api.get("tasks/#{task_gid}",
|
113
|
+
@task ||= api.get("tasks/#{task_gid}",
|
114
|
+
opt_fields: "name,memberships.section.name,assignee.name,permalink_url")
|
108
115
|
end
|
109
116
|
end
|
110
117
|
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
|
@@ -15,18 +15,18 @@ module Abt
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def path
|
18
|
-
Path.new(local_available? && git[
|
18
|
+
Path.new(local_available? && git["path"] || "")
|
19
19
|
end
|
20
20
|
|
21
21
|
def path=(new_path)
|
22
|
-
git[
|
22
|
+
git["path"] = new_path
|
23
23
|
end
|
24
24
|
|
25
25
|
def workspace_gid
|
26
26
|
@workspace_gid ||= begin
|
27
|
-
current = git_global[
|
27
|
+
current = git_global["workspaceGid"]
|
28
28
|
if current.nil?
|
29
|
-
|
29
|
+
prompt_workspace_gid
|
30
30
|
else
|
31
31
|
current
|
32
32
|
end
|
@@ -36,13 +36,13 @@ module Abt
|
|
36
36
|
def wip_section_gid
|
37
37
|
return nil unless local_available?
|
38
38
|
|
39
|
-
@wip_section_gid ||= git[
|
39
|
+
@wip_section_gid ||= git["wipSectionGid"] || prompt_wip_section["gid"]
|
40
40
|
end
|
41
41
|
|
42
42
|
def finalized_section_gid
|
43
43
|
return nil unless local_available?
|
44
44
|
|
45
|
-
@finalized_section_gid ||= git[
|
45
|
+
@finalized_section_gid ||= git["finalizedSectionGid"] || prompt_finalized_section["gid"]
|
46
46
|
end
|
47
47
|
|
48
48
|
def clear_local(verbose: true)
|
@@ -54,58 +54,66 @@ module Abt
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def access_token
|
57
|
-
return git_global[
|
57
|
+
return git_global["accessToken"] unless git_global["accessToken"].nil?
|
58
58
|
|
59
|
-
git_global[
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
git_global["accessToken"] = cli.prompt.text([
|
60
|
+
"Please provide your personal access token for Asana.",
|
61
|
+
"If you don't have one, create one here: https://app.asana.com/0/developer-console",
|
62
|
+
"",
|
63
|
+
"Enter access token"
|
64
64
|
].join("\n"))
|
65
65
|
end
|
66
66
|
|
67
67
|
private
|
68
68
|
|
69
69
|
def git
|
70
|
-
@git ||= GitConfig.new(
|
70
|
+
@git ||= GitConfig.new("local", "abt.asana")
|
71
71
|
end
|
72
72
|
|
73
73
|
def git_global
|
74
|
-
@git_global ||= GitConfig.new(
|
74
|
+
@git_global ||= GitConfig.new("global", "abt.asana")
|
75
75
|
end
|
76
76
|
|
77
77
|
def prompt_finalized_section
|
78
78
|
section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
|
79
|
-
git[
|
79
|
+
git["finalizedSectionGid"] = section["gid"]
|
80
80
|
section
|
81
81
|
end
|
82
82
|
|
83
83
|
def prompt_wip_section
|
84
|
-
section = prompt_section(
|
85
|
-
git[
|
84
|
+
section = prompt_section("Select WIP (Work In Progress) section")
|
85
|
+
git["wipSectionGid"] = section["gid"]
|
86
86
|
section
|
87
87
|
end
|
88
88
|
|
89
89
|
def prompt_section(message)
|
90
|
-
cli.warn
|
91
|
-
sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields:
|
90
|
+
cli.warn("Fetching sections...")
|
91
|
+
sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: "name")
|
92
92
|
cli.prompt.choice(message, sections)
|
93
93
|
end
|
94
94
|
|
95
|
-
def
|
96
|
-
cli.
|
97
|
-
|
98
|
-
if workspaces.
|
99
|
-
cli.abort 'Your asana access token does not have access to any workspaces'
|
100
|
-
elsif workspaces.one?
|
95
|
+
def prompt_workspace_gid
|
96
|
+
cli.abort("Your asana access token does not have access to any workspaces") if workspaces.empty?
|
97
|
+
|
98
|
+
if workspaces.one?
|
101
99
|
workspace = workspaces.first
|
102
|
-
cli.warn
|
100
|
+
cli.warn("Selected Asana workspace: #{workspace['name']}")
|
103
101
|
else
|
104
|
-
workspace =
|
102
|
+
workspace = pick_workspace
|
105
103
|
end
|
106
104
|
|
107
|
-
git_global[
|
108
|
-
|
105
|
+
git_global["workspaceGid"] = workspace["gid"]
|
106
|
+
end
|
107
|
+
|
108
|
+
def pick_workspace
|
109
|
+
cli.prompt.choice("Select Asana workspace", workspaces)
|
110
|
+
end
|
111
|
+
|
112
|
+
def workspaces
|
113
|
+
@workspaces ||= begin
|
114
|
+
cli.warn("Fetching workspaces...")
|
115
|
+
api.get_paged("workspaces", opt_fields: "name")
|
116
|
+
end
|
109
117
|
end
|
110
118
|
|
111
119
|
def api
|
@@ -4,15 +4,15 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Asana
|
6
6
|
class Path < String
|
7
|
-
PATH_REGEX = %r{^(?<project_gid>\d+)
|
7
|
+
PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
|
8
8
|
|
9
|
-
def self.from_ids(project_gid
|
10
|
-
path = project_gid ? [project_gid, *task_gid].join(
|
11
|
-
new
|
9
|
+
def self.from_ids(project_gid: nil, task_gid: nil)
|
10
|
+
path = project_gid ? [project_gid, *task_gid].join("/") : ""
|
11
|
+
new(path)
|
12
12
|
end
|
13
13
|
|
14
|
-
def initialize(path =
|
15
|
-
raise Abt::Cli::Abort, "Invalid path: #{path}" unless path
|
14
|
+
def initialize(path = "")
|
15
|
+
raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
|
16
16
|
|
17
17
|
super
|
18
18
|
end
|