abt-cli 0.0.26 → 0.0.27
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/docs.rb +10 -6
- data/lib/abt/providers/asana.rb +1 -0
- data/lib/abt/providers/asana/base_command.rb +33 -3
- data/lib/abt/providers/asana/commands/add.rb +0 -4
- data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
- data/lib/abt/providers/asana/commands/current.rb +0 -18
- data/lib/abt/providers/asana/commands/pick.rb +11 -41
- data/lib/abt/providers/asana/commands/tasks.rb +2 -7
- data/lib/abt/providers/asana/path.rb +1 -1
- data/lib/abt/providers/asana/services/project_picker.rb +54 -0
- data/lib/abt/providers/asana/services/task_picker.rb +83 -0
- data/lib/abt/providers/devops.rb +1 -0
- data/lib/abt/providers/devops/api.rb +10 -0
- data/lib/abt/providers/devops/base_command.rb +34 -14
- data/lib/abt/providers/devops/commands/boards.rb +1 -2
- data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
- data/lib/abt/providers/devops/commands/current.rb +0 -19
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
- data/lib/abt/providers/devops/commands/pick.rb +14 -53
- data/lib/abt/providers/devops/commands/work_items.rb +3 -6
- data/lib/abt/providers/devops/path.rb +2 -2
- data/lib/abt/providers/devops/services/board_picker.rb +54 -0
- data/lib/abt/providers/devops/services/project_picker.rb +79 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +45 -3
- data/lib/abt/providers/harvest/commands/current.rb +0 -28
- data/lib/abt/providers/harvest/commands/pick.rb +12 -27
- data/lib/abt/providers/harvest/commands/projects.rb +0 -5
- data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
- data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
- data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
- data/lib/abt/version.rb +1 -1
- metadata +9 -5
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/init.rb +0 -79
- data/lib/abt/providers/harvest/commands/init.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0bffe51f05670b4fcbf1bf2a023c354e26a387b0a74fd34751ef180b1f619c62
|
4
|
+
data.tar.gz: 7e5c476b8e1b803adfbb92590676c883e66a6ac74b5111e83aed9f4d6b9feb8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9dba1d7966f28966e5fe8d33fd3b380e823f707536b86e1f21a82bfdb74e20ba4d76b2c08e1fb9da9b7f1dc1d74ea6a787979b6ae86ca59b3ce9305964f7a063
|
7
|
+
data.tar.gz: 33b61911f3018b0e51ece32264c98f8327af60fcfb94bb59e4d210171bf8562fb885c120be565f79a827dd7f35c72d4d91acf48aa8497079107e120d8e4535bf
|
data/lib/abt/docs.rb
CHANGED
@@ -10,11 +10,10 @@ module Abt
|
|
10
10
|
def basic_examples
|
11
11
|
{
|
12
12
|
"Getting started:" => {
|
13
|
-
"abt init asana harvest" => "Setup asana and harvest project for local git repo",
|
14
13
|
"abt pick harvest" => "Pick harvest task. This will likely stay the same throughout the project",
|
15
14
|
"abt pick asana | abt start harvest" => "Pick asana task and start tracking time",
|
16
15
|
"abt stop harvest" => "Stop time tracker",
|
17
|
-
"abt
|
16
|
+
"abt track asana harvest" => "Continue tracking time, e.g., after a break",
|
18
17
|
"abt finalize asana" => "Finalize the selected asana task"
|
19
18
|
}
|
20
19
|
}
|
@@ -31,10 +30,15 @@ module Abt
|
|
31
30
|
"abt tasks asana | grep -i <name of task> | abt start" => nil
|
32
31
|
},
|
33
32
|
"Sharing ARIs:" => {
|
34
|
-
|
35
|
-
|
36
|
-
"abt
|
37
|
-
"abt current <ARIs from coworker> | abt
|
33
|
+
"abt share" => "Print current asana and harvest ARIs on a single line",
|
34
|
+
"abt share | pbcopy" => "Copy ARIs to clipboard (mac only)",
|
35
|
+
"abt track <ARIs from coworker>" => "Start tracking on the task your coworker shared with you",
|
36
|
+
"abt current <ARIs from coworker> | abt track" => "Set task as current, then start tracking"
|
37
|
+
},
|
38
|
+
"One-off tracking on any project": {
|
39
|
+
"abt pick asana -dc -- harvest -dc | abt track" =>
|
40
|
+
"Find a track any task on any project, without reusing/affecting previous settings",
|
41
|
+
"abt pick asana harvest | abt track" => "Can be used instead of the above when outside a git repo"
|
38
42
|
},
|
39
43
|
"Flags:" => {
|
40
44
|
'abt start harvest -c "comment"' => "Add command flags after ARIs",
|
data/lib/abt/providers/asana.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each { |file| require file }
|
4
4
|
Dir.glob("#{File.expand_path(__dir__)}/asana/commands/*.rb").sort.each { |file| require file }
|
5
|
+
Dir.glob("#{File.expand_path(__dir__)}/asana/services/*.rb").sort.each { |file| require file }
|
5
6
|
|
6
7
|
module Abt
|
7
8
|
module Providers
|
@@ -25,12 +25,42 @@ module Abt
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def require_project!
|
28
|
-
abort("No current/specified project. Did you
|
28
|
+
abort("No current/specified project. Did you forget to run `pick`?") if project_gid.nil?
|
29
29
|
end
|
30
30
|
|
31
31
|
def require_task!
|
32
|
-
|
33
|
-
abort("No current/specified task. Did you
|
32
|
+
require_project!
|
33
|
+
abort("No current/specified task. Did you forget to run `pick`?") if task_gid.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def prompt_project!
|
37
|
+
result = Services::ProjectPicker.call(cli: cli, config: config)
|
38
|
+
@path = result.path
|
39
|
+
@project = result.project
|
40
|
+
end
|
41
|
+
|
42
|
+
def prompt_task!
|
43
|
+
result = Services::TaskPicker.call(cli: cli, path: path, config: config, project: project)
|
44
|
+
@path = result.path
|
45
|
+
@task = result.task
|
46
|
+
end
|
47
|
+
|
48
|
+
def task
|
49
|
+
@task ||= begin
|
50
|
+
warn("Fetching task...")
|
51
|
+
api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
|
52
|
+
rescue Abt::HttpError::NotFoundError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def project
|
58
|
+
@project ||= begin
|
59
|
+
warn("Fetching project...")
|
60
|
+
api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
|
61
|
+
rescue Abt::HttpError::NotFoundError
|
62
|
+
nil
|
63
|
+
end
|
34
64
|
end
|
35
65
|
|
36
66
|
def print_project(project)
|
@@ -56,10 +56,6 @@ module Abt
|
|
56
56
|
@notes ||= cli.prompt.text("Enter task notes")
|
57
57
|
end
|
58
58
|
|
59
|
-
def project
|
60
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
61
|
-
end
|
62
|
-
|
63
59
|
def section
|
64
60
|
@section ||= cli.prompt.choice("Add to section?", sections,
|
65
61
|
nil_option: ["q", "Don't add to section"])
|
@@ -28,19 +28,6 @@ module Abt
|
|
28
28
|
|
29
29
|
def ensure_current_is_valid!
|
30
30
|
abort("Invalid task gid: #{task_gid}") if task.nil?
|
31
|
-
|
32
|
-
return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
|
33
|
-
|
34
|
-
abort("Invalid or unmatching project gid: #{project_gid}")
|
35
|
-
end
|
36
|
-
|
37
|
-
def task
|
38
|
-
@task ||= begin
|
39
|
-
warn("Fetching task...")
|
40
|
-
api.get("tasks/#{task_gid}", opt_fields: "name,memberships.project")
|
41
|
-
rescue Abt::HttpError::NotFoundError
|
42
|
-
nil
|
43
|
-
end
|
44
31
|
end
|
45
32
|
end
|
46
33
|
end
|
@@ -36,24 +36,6 @@ module Abt
|
|
36
36
|
abort("Invalid project: #{project_gid}") if project.nil?
|
37
37
|
abort("Invalid task: #{task_gid}") if task_gid && task.nil?
|
38
38
|
end
|
39
|
-
|
40
|
-
def project
|
41
|
-
@project ||= begin
|
42
|
-
warn("Fetching project...")
|
43
|
-
api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
|
44
|
-
rescue Abt::HttpError::NotFoundError
|
45
|
-
nil
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def task
|
50
|
-
@task ||= begin
|
51
|
-
warn("Fetching task...")
|
52
|
-
api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
|
53
|
-
rescue Abt::HttpError::NotFoundError
|
54
|
-
nil
|
55
|
-
end
|
56
|
-
end
|
57
39
|
end
|
58
40
|
end
|
59
41
|
end
|
@@ -10,65 +10,35 @@ module Abt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
"Pick task
|
13
|
+
"Pick a task and - unless told not to - make it current"
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
["-d", "--dry-run", "Keep existing configuration"]
|
18
|
+
["-d", "--dry-run", "Keep existing configuration"],
|
19
|
+
["-c", "--clean", "Don't reuse project configuration"]
|
19
20
|
]
|
20
21
|
end
|
21
22
|
|
22
23
|
def perform
|
23
|
-
|
24
|
-
require_project!
|
25
|
-
|
26
|
-
warn(project["name"])
|
27
|
-
task = select_task
|
24
|
+
pick!
|
28
25
|
|
29
26
|
print_task(project, task)
|
30
27
|
|
31
28
|
return if flags[:"dry-run"]
|
32
29
|
|
33
|
-
config.
|
34
|
-
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def project
|
39
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
40
|
-
end
|
41
|
-
|
42
|
-
def select_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
|
30
|
+
if config.local_available?
|
31
|
+
config.path = Path.from_gids(project_gid: project["gid"], task_gid: task["gid"])
|
50
32
|
else
|
51
|
-
|
33
|
+
warn("No local configuration to update - will function as dry run")
|
52
34
|
end
|
53
35
|
end
|
54
36
|
|
55
|
-
|
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"] }
|
65
|
-
end
|
37
|
+
private
|
66
38
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
71
|
-
end
|
39
|
+
def pick!
|
40
|
+
prompt_project! if project_gid.nil? || flags[:clean]
|
41
|
+
prompt_task!
|
72
42
|
end
|
73
43
|
end
|
74
44
|
end
|
@@ -14,7 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
prompt_project! unless project_gid
|
18
18
|
|
19
19
|
tasks.each do |task|
|
20
20
|
print_task(project, task)
|
@@ -23,14 +23,9 @@ module Abt
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
def project
|
27
|
-
@project ||= begin
|
28
|
-
api.get("projects/#{project_gid}", opt_fields: "name")
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
26
|
def tasks
|
33
27
|
@tasks ||= begin
|
28
|
+
project
|
34
29
|
warn("Fetching tasks...")
|
35
30
|
tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
|
36
31
|
tasks.reject { |task| task["completed"] }
|
@@ -6,7 +6,7 @@ module Abt
|
|
6
6
|
class Path < String
|
7
7
|
PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
|
8
8
|
|
9
|
-
def self.
|
9
|
+
def self.from_gids(project_gid: nil, task_gid: nil)
|
10
10
|
path = project_gid ? [project_gid, *task_gid].join("/") : ""
|
11
11
|
new(path)
|
12
12
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Services
|
7
|
+
class ProjectPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :project, :path
|
10
|
+
|
11
|
+
def initialize(project:, path:)
|
12
|
+
@project = project
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.call(**args)
|
18
|
+
new(**args).call
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :cli, :config
|
22
|
+
|
23
|
+
def initialize(cli:, config:)
|
24
|
+
@cli = cli
|
25
|
+
@config = config
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
project = cli.prompt.search("Select a project", projects)
|
30
|
+
path = Path.from_gids(project_gid: project["gid"])
|
31
|
+
|
32
|
+
Result.new(project: project, path: path)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def projects
|
38
|
+
@projects ||= begin
|
39
|
+
cli.warn("Fetching projects...")
|
40
|
+
api.get_paged("projects",
|
41
|
+
workspace: config.workspace_gid,
|
42
|
+
archived: false,
|
43
|
+
opt_fields: "name,permalink_url")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def api
|
48
|
+
Abt::Providers::Asana::Api.new(access_token: config.access_token)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Services
|
7
|
+
class TaskPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :task, :path
|
10
|
+
|
11
|
+
def initialize(task:, path:)
|
12
|
+
@task = task
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.call(**args)
|
18
|
+
new(**args).call
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :cli, :path, :config, :project
|
22
|
+
|
23
|
+
def initialize(cli:, path:, config:, project:)
|
24
|
+
@cli = cli
|
25
|
+
@path = path
|
26
|
+
@config = config
|
27
|
+
@project = project
|
28
|
+
end
|
29
|
+
|
30
|
+
def call
|
31
|
+
task = select_task
|
32
|
+
|
33
|
+
path_with_task = Path.new([path, task["gid"]].join("/"))
|
34
|
+
|
35
|
+
Result.new(task: task, path: path_with_task)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def select_task
|
41
|
+
section = prompt_section
|
42
|
+
tasks = tasks_in_section(section)
|
43
|
+
|
44
|
+
if tasks.length.zero?
|
45
|
+
cli.warn("Section is empty")
|
46
|
+
select_task
|
47
|
+
else
|
48
|
+
cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def prompt_section
|
53
|
+
cli.prompt.choice("Which section in #{project['name']}?", sections)
|
54
|
+
end
|
55
|
+
|
56
|
+
def tasks_in_section(section)
|
57
|
+
cli.warn("Fetching tasks...")
|
58
|
+
tasks = api.get_paged(
|
59
|
+
"tasks",
|
60
|
+
section: section["gid"],
|
61
|
+
opt_fields: "name,completed,permalink_url"
|
62
|
+
)
|
63
|
+
|
64
|
+
# The below filtering is the best we can do with Asanas api, see this:
|
65
|
+
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
66
|
+
tasks.reject { |task| task["completed"] }
|
67
|
+
end
|
68
|
+
|
69
|
+
def sections
|
70
|
+
@sections ||= begin
|
71
|
+
cli.warn("Fetching sections...")
|
72
|
+
api.get_paged("projects/#{project['gid']}/sections", opt_fields: "name")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def api
|
77
|
+
Abt::Providers::Asana::Api.new(access_token: config.access_token)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/abt/providers/devops.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
|
4
4
|
Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
|
5
|
+
Dir.glob("#{File.expand_path(__dir__)}/devops/services/*.rb").sort.each { |file| require file }
|
5
6
|
|
6
7
|
module Abt
|
7
8
|
module Providers
|
@@ -73,6 +73,16 @@ module Abt
|
|
73
73
|
"#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
|
74
74
|
end
|
75
75
|
|
76
|
+
def sanitize_work_item(work_item)
|
77
|
+
return nil if work_item.nil?
|
78
|
+
|
79
|
+
work_item.merge(
|
80
|
+
"id" => work_item["id"].to_s,
|
81
|
+
"name" => work_item["fields"]["System.Title"],
|
82
|
+
"url" => url_for_work_item(work_item)
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
76
86
|
def connection
|
77
87
|
@connection ||= Faraday.new(api_endpoint) do |connection|
|
78
88
|
connection.basic_auth(username, access_token)
|
@@ -24,29 +24,49 @@ module Abt
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def require_board!
|
27
|
-
return if
|
27
|
+
return if board_id && organization_name && project_name
|
28
28
|
|
29
|
-
abort("No current/specified board. Did you
|
29
|
+
abort("No current/specified board. Did you forget to `pick`?")
|
30
30
|
end
|
31
31
|
|
32
32
|
def require_work_item!
|
33
|
-
|
34
|
-
abort("No current/specified board. Did you initialize DevOps and pick a work item?")
|
35
|
-
end
|
36
|
-
|
33
|
+
require_board!
|
37
34
|
return if work_item_id
|
38
35
|
|
39
|
-
abort("No current/specified work item. Did you
|
36
|
+
abort("No current/specified work item. Did you forget to `pick`?")
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt_project!
|
40
|
+
@path = Services::ProjectPicker.call(cli: cli).path
|
40
41
|
end
|
41
42
|
|
42
|
-
def
|
43
|
-
|
43
|
+
def prompt_board!
|
44
|
+
result = Services::BoardPicker.call(cli: cli, path: path, config: config)
|
45
|
+
@path = result.path
|
46
|
+
@board = result.board
|
47
|
+
end
|
44
48
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
def prompt_work_item!
|
50
|
+
result = Services::WorkItemPicker.call(cli: cli, path: path, config: config, board: board)
|
51
|
+
@path = result.path
|
52
|
+
@work_item = result.work_item
|
53
|
+
end
|
54
|
+
|
55
|
+
def board
|
56
|
+
@board ||= begin
|
57
|
+
api.get("work/boards/#{board_id}")
|
58
|
+
rescue HttpError::NotFoundError
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def work_item
|
64
|
+
@work_item ||= begin
|
65
|
+
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
66
|
+
api.sanitize_work_item(work_item)
|
67
|
+
rescue HttpError::NotFoundError
|
68
|
+
nil
|
69
|
+
end
|
50
70
|
end
|
51
71
|
|
52
72
|
def print_board(organization_name, project_name, board)
|