abt-cli 0.0.18 → 0.0.23
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 +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,75 +6,45 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Init < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt init harvest"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Pick Harvest project for current git repository"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
17
|
+
require_local_config!
|
19
18
|
projects # Load projects up front to make it obvious that searches are instant
|
20
|
-
project =
|
19
|
+
project = cli.prompt.search("Select a project", searchable_projects)["project"]
|
21
20
|
|
22
|
-
config.
|
23
|
-
config.task_id = nil
|
21
|
+
config.path = Path.from_ids(project_id: project["id"])
|
24
22
|
|
25
23
|
print_project(project)
|
26
24
|
end
|
27
25
|
|
28
26
|
private
|
29
27
|
|
30
|
-
def find_search_result
|
31
|
-
cli.warn 'Select a project'
|
32
|
-
|
33
|
-
loop do
|
34
|
-
matches = matches_for_string cli.prompt.text('Enter search')
|
35
|
-
if matches.empty?
|
36
|
-
warn 'No matches'
|
37
|
-
next
|
38
|
-
end
|
39
|
-
|
40
|
-
cli.warn 'Showing the 10 first matches' if matches.size > 10
|
41
|
-
choice = cli.prompt.choice 'Select a project', matches[0...10], true
|
42
|
-
break choice['project'] unless choice.nil?
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def matches_for_string(string)
|
47
|
-
search_string = sanitize_string(string)
|
48
|
-
|
49
|
-
searchable_projects.select do |project|
|
50
|
-
sanitize_string(project['name']).include?(search_string)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def sanitize_string(string)
|
55
|
-
string.downcase.gsub(/[^\w]/, '')
|
56
|
-
end
|
57
|
-
|
58
28
|
def searchable_projects
|
59
29
|
@searchable_projects ||= projects.map do |project|
|
60
30
|
{
|
61
|
-
|
62
|
-
|
31
|
+
"name" => "#{project['client']['name']} > #{project['name']}",
|
32
|
+
"project" => project
|
63
33
|
}
|
64
34
|
end
|
65
35
|
end
|
66
36
|
|
67
37
|
def projects
|
68
38
|
@projects ||= begin
|
69
|
-
|
39
|
+
warn("Fetching projects...")
|
70
40
|
project_assignments.map do |project_assignment|
|
71
|
-
project_assignment[
|
41
|
+
project_assignment["project"].merge("client" => project_assignment["client"])
|
72
42
|
end
|
73
43
|
end
|
74
44
|
end
|
75
45
|
|
76
46
|
def project_assignments
|
77
|
-
@project_assignments ||= api.get_paged(
|
47
|
+
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
78
48
|
end
|
79
49
|
end
|
80
50
|
end
|
@@ -6,52 +6,55 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Pick < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt pick harvest[:<project-id>]"
|
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
|
-
task =
|
26
|
+
warn(project["name"])
|
27
|
+
task = pick_task
|
28
28
|
|
29
29
|
print_task(project, task)
|
30
30
|
|
31
31
|
return if flags[:"dry-run"]
|
32
32
|
|
33
|
-
config.
|
34
|
-
config.task_id = task['id']
|
33
|
+
config.path = Path.from_ids(project_id: project_id, task_id: task["id"])
|
35
34
|
end
|
36
35
|
|
37
36
|
private
|
38
37
|
|
39
38
|
def project
|
40
|
-
project_assignment[
|
39
|
+
project_assignment["project"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def pick_task
|
43
|
+
cli.prompt.choice("Select a task", tasks)
|
41
44
|
end
|
42
45
|
|
43
46
|
def tasks
|
44
|
-
@tasks ||= project_assignment[
|
47
|
+
@tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
|
45
48
|
end
|
46
49
|
|
47
50
|
def project_assignment
|
48
51
|
@project_assignment ||= begin
|
49
|
-
project_assignments.find { |pa| pa[
|
52
|
+
project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
|
50
53
|
end
|
51
54
|
end
|
52
55
|
|
53
56
|
def project_assignments
|
54
|
-
@project_assignments ||= api.get_paged(
|
57
|
+
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
55
58
|
end
|
56
59
|
end
|
57
60
|
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 harvest"
|
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,15 +23,15 @@ module Abt
|
|
23
23
|
|
24
24
|
def projects
|
25
25
|
@projects ||= begin
|
26
|
-
|
26
|
+
warn("Fetching projects...")
|
27
27
|
project_assignments.map do |project_assignment|
|
28
|
-
project_assignment[
|
28
|
+
project_assignment["project"].merge("client" => project_assignment["client"])
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
def project_assignments
|
34
|
-
@project_assignments ||= api.get_paged(
|
34
|
+
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
35
35
|
end
|
36
36
|
end
|
37
37
|
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 harvest[:<project-id>[/<task-id>]]"
|
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
|
18
|
-
cli.
|
19
|
-
elsif
|
20
|
-
|
21
|
-
else
|
22
|
-
cli.print_ari('harvest', "#{project_id}/#{task_id}")
|
17
|
+
if path != ""
|
18
|
+
cli.print_ari("harvest", path)
|
19
|
+
elsif cli.output.isatty
|
20
|
+
warn("No configuration for project. Did you initialize Harvest?")
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "track"
|
4
4
|
|
5
5
|
module Abt
|
6
6
|
module Providers
|
@@ -8,11 +8,13 @@ module Abt
|
|
8
8
|
module Commands
|
9
9
|
class Start < Track
|
10
10
|
def self.usage
|
11
|
-
|
11
|
+
"abt start harvest[:<project-id>/<task-id>] [options]"
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.description
|
15
|
-
|
15
|
+
<<~TXT
|
16
|
+
Alias for: `abt track harvest`. Meant to used in combination with other ARIs, e.g. `abt start harvest asana`
|
17
|
+
TXT
|
16
18
|
end
|
17
19
|
end
|
18
20
|
end
|
@@ -6,49 +6,49 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Stop < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt stop harvest"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Stop running harvest tracker"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
abort("No running time entry") if time_entry.nil?
|
18
18
|
|
19
19
|
stop_time_entry
|
20
20
|
|
21
|
-
|
21
|
+
warn("Harvest time entry stopped")
|
22
22
|
print_task(project, task)
|
23
|
-
rescue Abt::HttpError::HttpError => e
|
24
|
-
cli.warn e
|
25
|
-
cli.abort 'Unable to stop time entry'
|
26
23
|
end
|
27
24
|
|
28
25
|
private
|
29
26
|
|
30
27
|
def stop_time_entry
|
31
28
|
api.patch("time_entries/#{time_entry['id']}/stop")
|
29
|
+
rescue Abt::HttpError::HttpError => e
|
30
|
+
warn(e)
|
31
|
+
abort("Unable to stop time entry")
|
32
32
|
end
|
33
33
|
|
34
34
|
def project
|
35
|
-
time_entry[
|
35
|
+
time_entry["project"]
|
36
36
|
end
|
37
37
|
|
38
38
|
def task
|
39
|
-
time_entry[
|
39
|
+
time_entry["task"]
|
40
40
|
end
|
41
41
|
|
42
42
|
def time_entry
|
43
43
|
@time_entry ||= begin
|
44
44
|
api.get_paged(
|
45
|
-
|
45
|
+
"time_entries",
|
46
46
|
is_running: true,
|
47
47
|
user_id: config.user_id
|
48
48
|
).first
|
49
|
-
rescue Abt::HttpError::HttpError => e
|
50
|
-
|
51
|
-
|
49
|
+
rescue Abt::HttpError::HttpError => e
|
50
|
+
warn(e)
|
51
|
+
abort("Unable to fetch running time entry")
|
52
52
|
end
|
53
53
|
end
|
54
54
|
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 harvest"
|
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
|
@@ -24,21 +24,24 @@ module Abt
|
|
24
24
|
private
|
25
25
|
|
26
26
|
def project
|
27
|
-
project_assignment[
|
27
|
+
project_assignment["project"]
|
28
28
|
end
|
29
29
|
|
30
30
|
def tasks
|
31
|
-
@tasks ||=
|
31
|
+
@tasks ||= begin
|
32
|
+
warn("Fetching tasks...")
|
33
|
+
project_assignment["task_assignments"].map { |ta| ta["task"] }
|
34
|
+
end
|
32
35
|
end
|
33
36
|
|
34
37
|
def project_assignment
|
35
38
|
@project_assignment ||= begin
|
36
|
-
project_assignments.find { |pa| pa[
|
39
|
+
project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
40
43
|
def project_assignments
|
41
|
-
@project_assignments ||= api.get_paged(
|
44
|
+
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
42
45
|
end
|
43
46
|
end
|
44
47
|
end
|
@@ -6,30 +6,33 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Track < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt track harvest[:<project-id>/<task-id>] [options]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
<<~TXT
|
14
|
+
Start tracker for current or specified task. Add a relevant ARI to link the time entry, e.g. `abt track harvest asana`
|
15
|
+
TXT
|
14
16
|
end
|
15
17
|
|
16
18
|
def self.flags
|
17
19
|
[
|
18
|
-
[
|
19
|
-
[
|
20
|
-
[
|
21
|
-
|
20
|
+
["-s", "--set", "Set specified task as current"],
|
21
|
+
["-c", "--comment COMMENT", "Override comment"],
|
22
|
+
["-t", "--time HOURS",
|
23
|
+
"Set hours. Creates a stopped entry unless used with --running"],
|
24
|
+
["-r", "--running", "Used with --time, starts the created time entry"]
|
22
25
|
]
|
23
26
|
end
|
24
27
|
|
25
28
|
def perform
|
26
29
|
require_task!
|
27
30
|
|
28
|
-
print_task(created_time_entry[
|
31
|
+
print_task(created_time_entry["project"], created_time_entry["task"])
|
29
32
|
|
30
33
|
maybe_override_current_task
|
31
34
|
rescue Abt::HttpError::HttpError => _e
|
32
|
-
|
35
|
+
abort("Invalid task")
|
33
36
|
end
|
34
37
|
|
35
38
|
private
|
@@ -39,69 +42,88 @@ module Abt
|
|
39
42
|
end
|
40
43
|
|
41
44
|
def create_time_entry
|
42
|
-
body =
|
43
|
-
body.merge!(hours: flags[:time]) if flags.key? :time
|
45
|
+
body = time_entry_data
|
44
46
|
|
45
|
-
result = api.post(
|
47
|
+
result = api.post("time_entries", Oj.dump(body, mode: :json))
|
46
48
|
|
47
|
-
if flags.key?(:time) && flags[:running]
|
48
|
-
api.patch("time_entries/#{result['id']}/restart")
|
49
|
-
end
|
49
|
+
api.patch("time_entries/#{result['id']}/restart") if flags.key?(:time) && flags[:running]
|
50
50
|
|
51
51
|
result
|
52
52
|
end
|
53
53
|
|
54
|
+
def time_entry_data
|
55
|
+
body = time_entry_base_data
|
56
|
+
|
57
|
+
maybe_add_external_link(body)
|
58
|
+
maybe_add_comment(body)
|
59
|
+
maybe_add_time(body)
|
60
|
+
|
61
|
+
body
|
62
|
+
end
|
63
|
+
|
54
64
|
def time_entry_base_data
|
55
|
-
|
65
|
+
{
|
56
66
|
project_id: project_id,
|
57
67
|
task_id: task_id,
|
58
68
|
user_id: config.user_id,
|
59
69
|
spent_date: Date.today.iso8601
|
60
70
|
}
|
71
|
+
end
|
61
72
|
|
73
|
+
def maybe_add_external_link(body)
|
62
74
|
if external_link_data
|
63
|
-
|
75
|
+
warn(<<~TXT)
|
64
76
|
Linking to:
|
65
|
-
|
66
|
-
|
77
|
+
#{external_link_data[:notes]}
|
78
|
+
#{external_link_data[:external_reference][:permalink]}
|
67
79
|
TXT
|
68
|
-
body.merge!
|
80
|
+
body.merge!(external_link_data)
|
69
81
|
else
|
70
|
-
|
82
|
+
warn("No external link provided")
|
71
83
|
end
|
84
|
+
end
|
72
85
|
|
86
|
+
def maybe_add_comment(body)
|
73
87
|
body[:notes] = flags[:comment] if flags.key?(:comment)
|
74
|
-
body[:notes] ||= cli.prompt.text(
|
75
|
-
body
|
88
|
+
body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
|
76
89
|
end
|
77
90
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
output = StringIO.new
|
82
|
-
Abt::Cli.new(argv: ['harvest-time-entry-data'], output: output, input: input).perform
|
91
|
+
def maybe_add_time(body)
|
92
|
+
body[:hours] = flags[:time] if flags.key?(:time)
|
93
|
+
end
|
83
94
|
|
84
|
-
|
95
|
+
def external_link_data
|
96
|
+
return @external_link_data if instance_variable_defined?(:@external_link_data)
|
85
97
|
|
86
|
-
|
98
|
+
lines = fetch_link_data_lines
|
87
99
|
|
88
|
-
|
89
|
-
if lines.length > 1
|
90
|
-
cli.abort('Got reference data from multiple scheme providers, only one is supported at a time')
|
91
|
-
end
|
100
|
+
return @external_link_data = nil if lines.empty?
|
92
101
|
|
93
|
-
|
102
|
+
if lines.length > 1
|
103
|
+
abort("Got reference data from multiple scheme providers, only one is supported at a time")
|
94
104
|
end
|
105
|
+
|
106
|
+
@external_link_data = Oj.load(lines.first, symbol_keys: true)
|
107
|
+
end
|
108
|
+
|
109
|
+
def fetch_link_data_lines
|
110
|
+
other_aris = cli.aris - [ari]
|
111
|
+
return [] if other_aris.empty?
|
112
|
+
|
113
|
+
input = StringIO.new(other_aris.to_s)
|
114
|
+
output = StringIO.new
|
115
|
+
Abt::Cli.new(argv: ["harvest-time-entry-data"], output: output, input: input).perform
|
116
|
+
|
117
|
+
output.string.strip.lines
|
95
118
|
end
|
96
119
|
|
97
120
|
def maybe_override_current_task
|
98
121
|
return unless flags[:set]
|
99
|
-
return if
|
122
|
+
return if path == config.path
|
100
123
|
return unless config.local_available?
|
101
124
|
|
102
|
-
|
103
|
-
|
104
|
-
Abt::Cli.new(argv: ['current'], output: output, input: input).perform
|
125
|
+
config.path = path
|
126
|
+
warn("Current task updated")
|
105
127
|
end
|
106
128
|
end
|
107
129
|
end
|