abt-cli 0.0.28 → 0.0.32
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/providers/asana/commands/clear.rb +1 -1
- data/lib/abt/providers/asana/commands/pick.rb +1 -1
- data/lib/abt/providers/asana/configuration.rb +1 -1
- data/lib/abt/providers/asana/services/task_picker.rb +23 -3
- data/lib/abt/providers/devops/api.rb +18 -21
- data/lib/abt/providers/devops/base_command.rb +16 -19
- data/lib/abt/providers/devops/commands/branch_name.rb +1 -3
- data/lib/abt/providers/devops/commands/clear.rb +1 -1
- data/lib/abt/providers/devops/commands/current.rb +2 -2
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +1 -3
- data/lib/abt/providers/devops/commands/pick.rb +3 -13
- data/lib/abt/providers/devops/commands/work_items.rb +2 -3
- data/lib/abt/providers/devops/commands/write_config.rb +3 -3
- data/lib/abt/providers/devops/configuration.rb +1 -1
- data/lib/abt/providers/devops/path.rb +22 -6
- data/lib/abt/providers/devops/services/board_picker.rb +26 -11
- data/lib/abt/providers/devops/services/work_item_picker.rb +9 -3
- data/lib/abt/providers/harvest/commands/clear.rb +1 -1
- data/lib/abt/providers/harvest/commands/pick.rb +1 -1
- data/lib/abt/providers/harvest/commands/track.rb +72 -39
- data/lib/abt/providers/harvest/configuration.rb +1 -1
- data/lib/abt/providers/harvest/harvest_helpers.rb +25 -0
- data/lib/abt/providers/harvest/services/task_picker.rb +1 -1
- data/lib/abt/version.rb +1 -1
- metadata +9 -8
- data/lib/abt/providers/devops/commands/boards.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38d3fa16692941ffa0e720a7a0331d91215e33e37092946df35053f040ee2af8
|
4
|
+
data.tar.gz: b2471d250f4580b9334f7005496e45ae1487eb8e979bcf3aedd975e3f418c79b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5232fcb04eb5c3c35cae6b29a82586ef289316a3540eec50f63aa55b42a79226e4000d3c6cff37a318cf8b93d17f47df3cccd8a61f68d02e2fccbe0f9aacd18
|
7
|
+
data.tar.gz: 59cc3c70175c71420cbb2c0bc0b6e96d40b34c21a225cf4f2e3e310fa33f228a1792a31d6eeb9b4485ef140f75f88358679e08d301ef7adde736b5f45fe86ddf
|
@@ -22,7 +22,7 @@ module Abt
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def perform
|
25
|
-
abort("Flags --global and --all cannot be used
|
25
|
+
abort("Flags --global and --all cannot be used together") if flags[:global] && flags[:all]
|
26
26
|
|
27
27
|
config.clear_local unless flags[:global]
|
28
28
|
config.clear_global if flags[:global] || flags[:all]
|
@@ -28,7 +28,7 @@ module Abt
|
|
28
28
|
return if flags[:"dry-run"]
|
29
29
|
|
30
30
|
if config.local_available?
|
31
|
-
config.path =
|
31
|
+
config.path = path
|
32
32
|
else
|
33
33
|
warn("No local configuration to update - will function as dry run")
|
34
34
|
end
|
@@ -30,7 +30,7 @@ module Abt
|
|
30
30
|
def call
|
31
31
|
task = select_task
|
32
32
|
|
33
|
-
path_with_task = Path.
|
33
|
+
path_with_task = Path.from_gids(project_gid: path.project_gid, task_gid: task["gid"])
|
34
34
|
|
35
35
|
Result.new(task: task, path: path_with_task)
|
36
36
|
end
|
@@ -45,7 +45,7 @@ module Abt
|
|
45
45
|
cli.warn("Section is empty")
|
46
46
|
select_task
|
47
47
|
else
|
48
|
-
cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
|
48
|
+
cli.prompt.choice("Select a task", options_for_tasks(tasks), nil_option: true) || select_task
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -53,12 +53,32 @@ module Abt
|
|
53
53
|
cli.prompt.choice("Which section in #{project['name']}?", sections)
|
54
54
|
end
|
55
55
|
|
56
|
+
def options_for_tasks(tasks)
|
57
|
+
tasks.map do |task|
|
58
|
+
formatted_name = [
|
59
|
+
task["name"],
|
60
|
+
formatted_assignee(task)
|
61
|
+
].compact.join(" ")
|
62
|
+
|
63
|
+
task.merge("name" => formatted_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def formatted_assignee(task)
|
68
|
+
name = task.dig("assignee", "name")
|
69
|
+
|
70
|
+
return unless name
|
71
|
+
|
72
|
+
initials = name.split.map(&:chr).join.upcase
|
73
|
+
"(#{initials}👤)"
|
74
|
+
end
|
75
|
+
|
56
76
|
def tasks_in_section(section)
|
57
77
|
cli.warn("Fetching tasks...")
|
58
78
|
tasks = api.get_paged(
|
59
79
|
"tasks",
|
60
80
|
section: section["gid"],
|
61
|
-
opt_fields: "name,completed,permalink_url"
|
81
|
+
opt_fields: "name,completed,permalink_url,assignee.name"
|
62
82
|
)
|
63
83
|
|
64
84
|
# The below filtering is the best we can do with Asanas api, see this:
|
@@ -4,15 +4,22 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Devops
|
6
6
|
class Api
|
7
|
+
# Shamelessly copied from ERB::Util.url_encode
|
8
|
+
# https://apidock.com/ruby/ERB/Util/url_encode
|
9
|
+
def self.rfc_3986_encode_path_segment(string)
|
10
|
+
string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
|
11
|
+
format("%%%02X", match.unpack1("C"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
7
15
|
VERBS = [:get, :post, :put].freeze
|
8
16
|
|
9
17
|
CONDITIONAL_ACCESS_POLICY_ERROR_CODE = "VS403463"
|
10
18
|
|
11
19
|
attr_reader :organization_name, :project_name, :username, :access_token, :cli
|
12
20
|
|
13
|
-
def initialize(organization_name:,
|
21
|
+
def initialize(organization_name:, username:, access_token:, cli:)
|
14
22
|
@organization_name = organization_name
|
15
|
-
@project_name = project_name
|
16
23
|
@username = username
|
17
24
|
@access_token = access_token
|
18
25
|
@cli = cli
|
@@ -32,12 +39,12 @@ module Abt
|
|
32
39
|
end
|
33
40
|
|
34
41
|
def work_item_query(wiql)
|
35
|
-
response = post("wit/wiql", Oj.dump({ query: wiql }, mode: :json))
|
42
|
+
response = post("_apis/wit/wiql", Oj.dump({ query: wiql }, mode: :json))
|
36
43
|
ids = response["workItems"].map { |work_item| work_item["id"] }
|
37
44
|
|
38
45
|
work_items = []
|
39
46
|
ids.each_slice(200) do |page_ids|
|
40
|
-
work_items += get_paged("wit/workitems", ids: page_ids.join(","))
|
47
|
+
work_items += get_paged("_apis/wit/workitems", ids: page_ids.join(","))
|
41
48
|
end
|
42
49
|
|
43
50
|
work_items
|
@@ -58,19 +65,17 @@ module Abt
|
|
58
65
|
end
|
59
66
|
|
60
67
|
def base_url
|
61
|
-
"https://#{organization_name}.visualstudio.com
|
62
|
-
end
|
63
|
-
|
64
|
-
def api_endpoint
|
65
|
-
"#{base_url}/_apis"
|
68
|
+
"https://#{organization_name}.visualstudio.com"
|
66
69
|
end
|
67
70
|
|
68
71
|
def url_for_work_item(work_item)
|
69
|
-
|
72
|
+
project_name = self.class.rfc_3986_encode_path_segment(work_item["fields"]["System.TeamProject"])
|
73
|
+
"#{base_url}/#{project_name}/_workitems/edit/#{work_item['id']}"
|
70
74
|
end
|
71
75
|
|
72
|
-
def url_for_board(board)
|
73
|
-
|
76
|
+
def url_for_board(project_name, team_name, board)
|
77
|
+
board_name = self.class.rfc_3986_encode_path_segment(board["name"])
|
78
|
+
"#{base_url}/#{project_name}/_boards/board/t/#{team_name}/#{board_name}"
|
74
79
|
end
|
75
80
|
|
76
81
|
def sanitize_work_item(work_item)
|
@@ -84,7 +89,7 @@ module Abt
|
|
84
89
|
end
|
85
90
|
|
86
91
|
def connection
|
87
|
-
@connection ||= Faraday.new(
|
92
|
+
@connection ||= Faraday.new(base_url) do |connection|
|
88
93
|
connection.basic_auth(username, access_token)
|
89
94
|
connection.headers["Content-Type"] = "application/json"
|
90
95
|
connection.headers["Accept"] = "application/json; api-version=6.0"
|
@@ -93,14 +98,6 @@ module Abt
|
|
93
98
|
|
94
99
|
private
|
95
100
|
|
96
|
-
# Shamelessly copied from ERB::Util.url_encode
|
97
|
-
# https://apidock.com/ruby/ERB/Util/url_encode
|
98
|
-
def rfc_3986_encode_path_segment(string)
|
99
|
-
string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
|
100
|
-
format("%%%02X", match.unpack1("C"))
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
101
|
def handle_denied_by_conditional_access_policy!(exception)
|
105
102
|
raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
|
106
103
|
|
@@ -8,7 +8,7 @@ module Abt
|
|
8
8
|
|
9
9
|
attr_reader :config, :path
|
10
10
|
|
11
|
-
def_delegators(:@path, :organization_name, :project_name, :
|
11
|
+
def_delegators(:@path, :organization_name, :project_name, :team_name, :board_name, :work_item_id)
|
12
12
|
|
13
13
|
def initialize(ari:, cli:)
|
14
14
|
super
|
@@ -24,7 +24,7 @@ module Abt
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def require_board!
|
27
|
-
return if
|
27
|
+
return if board_name && organization_name && project_name && team_name
|
28
28
|
|
29
29
|
abort("No current/specified board. Did you forget to `pick`?")
|
30
30
|
end
|
@@ -36,12 +36,8 @@ module Abt
|
|
36
36
|
abort("No current/specified work item. Did you forget to `pick`?")
|
37
37
|
end
|
38
38
|
|
39
|
-
def prompt_project!
|
40
|
-
@path = Services::ProjectPicker.call(cli: cli).path
|
41
|
-
end
|
42
|
-
|
43
39
|
def prompt_board!
|
44
|
-
result = Services::BoardPicker.call(cli: cli,
|
40
|
+
result = Services::BoardPicker.call(cli: cli, config: config)
|
45
41
|
@path = result.path
|
46
42
|
@board = result.board
|
47
43
|
end
|
@@ -54,7 +50,7 @@ module Abt
|
|
54
50
|
|
55
51
|
def board
|
56
52
|
@board ||= begin
|
57
|
-
api.get("work/boards/#{
|
53
|
+
api.get("#{project_name}/#{team_name}/_apis/work/boards/#{board_name}")
|
58
54
|
rescue HttpError::NotFoundError
|
59
55
|
nil
|
60
56
|
end
|
@@ -62,33 +58,34 @@ module Abt
|
|
62
58
|
|
63
59
|
def work_item
|
64
60
|
@work_item ||= begin
|
65
|
-
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
61
|
+
work_item = api.get_paged("_apis/wit/workitems", ids: work_item_id)[0]
|
66
62
|
api.sanitize_work_item(work_item)
|
67
63
|
rescue HttpError::NotFoundError
|
68
64
|
nil
|
69
65
|
end
|
70
66
|
end
|
71
67
|
|
72
|
-
def print_board(organization_name, project_name, board)
|
73
|
-
|
68
|
+
def print_board(organization_name, project_name, team_name, board)
|
69
|
+
board_name = Api.rfc_3986_encode_path_segment(board["name"])
|
70
|
+
path = "#{organization_name}/#{project_name}/#{team_name}/#{board_name}"
|
74
71
|
|
75
72
|
cli.print_ari("devops", path, board["name"])
|
76
|
-
warn(api.url_for_board(board)) if cli.output.isatty
|
73
|
+
warn(api.url_for_board(project_name, team_name, board)) if cli.output.isatty
|
77
74
|
end
|
78
75
|
|
79
|
-
def print_work_item(organization, project, board, work_item)
|
80
|
-
|
76
|
+
def print_work_item(organization, project, team_name, board, work_item)
|
77
|
+
board_name = Api.rfc_3986_encode_path_segment(board["name"])
|
78
|
+
path = "#{organization}/#{project}/#{team_name}/#{board_name}/#{work_item['id']}"
|
81
79
|
|
82
80
|
cli.print_ari("devops", path, work_item["name"])
|
83
81
|
warn(work_item["url"]) if work_item.key?("url") && cli.output.isatty
|
84
82
|
end
|
85
83
|
|
86
84
|
def api
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
cli: cli)
|
85
|
+
Api.new(organization_name: organization_name,
|
86
|
+
username: config.username_for_organization(organization_name),
|
87
|
+
access_token: config.access_token_for_organization(organization_name),
|
88
|
+
cli: cli)
|
92
89
|
end
|
93
90
|
end
|
94
91
|
end
|
@@ -19,11 +19,9 @@ module Abt
|
|
19
19
|
if work_item
|
20
20
|
puts name
|
21
21
|
else
|
22
|
-
args = [organization_name, project_name, board_id, work_item_id].compact
|
23
|
-
|
24
22
|
abort(<<~TXT)
|
25
23
|
Unable to find work item for configuration:
|
26
|
-
devops:#{
|
24
|
+
devops:#{path}
|
27
25
|
TXT
|
28
26
|
end
|
29
27
|
end
|
@@ -22,7 +22,7 @@ module Abt
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def perform
|
25
|
-
abort("Flags --global and --all cannot be used
|
25
|
+
abort("Flags --global and --all cannot be used together") if flags[:global] && flags[:all]
|
26
26
|
|
27
27
|
config.clear_local unless flags[:global]
|
28
28
|
config.clear_global if flags[:global] || flags[:all]
|
@@ -30,9 +30,9 @@ module Abt
|
|
30
30
|
|
31
31
|
def print_configuration
|
32
32
|
if work_item_id.nil?
|
33
|
-
print_board(organization_name, project_name, board)
|
33
|
+
print_board(organization_name, project_name, team_name, board)
|
34
34
|
else
|
35
|
-
print_work_item(organization_name, project_name, board, work_item)
|
35
|
+
print_work_item(organization_name, project_name, team_name, board, work_item)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -19,11 +19,9 @@ module Abt
|
|
19
19
|
if work_item
|
20
20
|
puts Oj.dump(body, mode: :json)
|
21
21
|
else
|
22
|
-
args = [organization_name, project_name, board_id, work_item_id].compact
|
23
|
-
|
24
22
|
abort(<<~TXT)
|
25
23
|
Unable to find work item for configuration:
|
26
|
-
devops:#{
|
24
|
+
devops:#{path}
|
27
25
|
TXT
|
28
26
|
end
|
29
27
|
end
|
@@ -23,12 +23,12 @@ module Abt
|
|
23
23
|
def perform
|
24
24
|
pick!
|
25
25
|
|
26
|
-
print_work_item(organization_name, project_name, board, work_item)
|
26
|
+
print_work_item(organization_name, project_name, team_name, board, work_item)
|
27
27
|
|
28
28
|
return if flags[:"dry-run"]
|
29
29
|
|
30
30
|
if config.local_available?
|
31
|
-
|
31
|
+
config.path = path
|
32
32
|
else
|
33
33
|
warn("No local configuration to update - will function as dry run")
|
34
34
|
end
|
@@ -37,19 +37,9 @@ module Abt
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def pick!
|
40
|
-
|
41
|
-
prompt_board! if board_id.nil? || flags[:clean]
|
40
|
+
prompt_board! if board_name.nil? || flags[:clean]
|
42
41
|
prompt_work_item!
|
43
42
|
end
|
44
|
-
|
45
|
-
def update_config(work_item)
|
46
|
-
config.path = Path.from_ids(
|
47
|
-
organization_name: organization_name,
|
48
|
-
project_name: project_name,
|
49
|
-
board_id: board_id,
|
50
|
-
work_item_id: work_item["id"]
|
51
|
-
)
|
52
|
-
end
|
53
43
|
end
|
54
44
|
end
|
55
45
|
end
|
@@ -14,11 +14,10 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
prompt_board! unless board_id
|
17
|
+
prompt_board! unless board_name
|
19
18
|
|
20
19
|
work_items.each do |work_item|
|
21
|
-
print_work_item(organization_name, project_name, board, work_item)
|
20
|
+
print_work_item(organization_name, project_name, team_name, board, work_item)
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -20,8 +20,7 @@ module Abt
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
24
|
-
prompt_board! if board_id.nil? || flags[:clean]
|
23
|
+
prompt_board! if board_name.nil? || flags[:clean]
|
25
24
|
|
26
25
|
update_directory_config!
|
27
26
|
|
@@ -35,7 +34,8 @@ module Abt
|
|
35
34
|
"path" => Path.from_ids(
|
36
35
|
organization_name: organization_name,
|
37
36
|
project_name: project_name,
|
38
|
-
|
37
|
+
team_name: team_name,
|
38
|
+
board_name: board_name
|
39
39
|
).to_s
|
40
40
|
}
|
41
41
|
cli.directory_config.save!
|
@@ -15,7 +15,7 @@ module Abt
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def path
|
18
|
-
Path.new(local_available? && git["path"] || cli.directory_config.dig("devops", "path") || "")
|
18
|
+
Path.new((local_available? && git["path"]) || cli.directory_config.dig("devops", "path") || "")
|
19
19
|
end
|
20
20
|
|
21
21
|
def path=(new_path)
|
@@ -6,16 +6,28 @@ module Abt
|
|
6
6
|
class Path < String
|
7
7
|
ORGANIZATION_NAME_REGEX = %r{(?<organization_name>[^/ ]+)}.freeze
|
8
8
|
PROJECT_NAME_REGEX = %r{(?<project_name>[^/ ]+)}.freeze
|
9
|
-
|
9
|
+
TEAM_NAME_REGEX = %r{(?<team_name>[^/ ]+)}.freeze
|
10
|
+
BOARD_NAME_REGEX = %r{(?<board_name>[^/ ]+)}.freeze
|
10
11
|
WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
|
11
12
|
|
12
13
|
PATH_REGEX =
|
13
|
-
%r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{
|
14
|
+
%r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{TEAM_NAME_REGEX}(/#{BOARD_NAME_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?)?}.freeze # rubocop:disable Layout/LineLength
|
14
15
|
|
15
|
-
def self.from_ids(organization_name: nil, project_name: nil,
|
16
|
+
def self.from_ids(organization_name: nil, project_name: nil, team_name: nil, board_name: nil, work_item_id: nil)
|
16
17
|
return new unless organization_name && project_name
|
17
18
|
|
18
|
-
|
19
|
+
parts = [organization_name, project_name]
|
20
|
+
|
21
|
+
if team_name
|
22
|
+
parts << team_name
|
23
|
+
|
24
|
+
if board_name
|
25
|
+
parts << board_name
|
26
|
+
parts << work_item_id if work_item_id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
new(parts.join("/"))
|
19
31
|
end
|
20
32
|
|
21
33
|
def initialize(path = "")
|
@@ -32,8 +44,12 @@ module Abt
|
|
32
44
|
match[:project_name]
|
33
45
|
end
|
34
46
|
|
35
|
-
def
|
36
|
-
match[:
|
47
|
+
def team_name
|
48
|
+
match[:team_name]
|
49
|
+
end
|
50
|
+
|
51
|
+
def board_name
|
52
|
+
match[:board_name]
|
37
53
|
end
|
38
54
|
|
39
55
|
def work_item_id
|
@@ -20,32 +20,47 @@ module Abt
|
|
20
20
|
|
21
21
|
attr_reader :cli, :config, :path
|
22
22
|
|
23
|
-
def initialize(cli:,
|
23
|
+
def initialize(cli:, config:)
|
24
24
|
@cli = cli
|
25
25
|
@config = config
|
26
|
-
@path = path
|
27
26
|
end
|
28
27
|
|
29
28
|
def call
|
29
|
+
@path = ProjectPicker.call(cli: cli).path
|
30
30
|
board = cli.prompt.choice("Select a project work board", boards)
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
Result.new(board: board, path: path_with_board)
|
32
|
+
Result.new(board: board, path: path_with_board(team, board))
|
35
33
|
end
|
36
34
|
|
37
35
|
private
|
38
36
|
|
37
|
+
def path_with_board(team, board)
|
38
|
+
Path.from_ids(
|
39
|
+
organization_name: path.organization_name,
|
40
|
+
project_name: path.project_name,
|
41
|
+
team_name: Api.rfc_3986_encode_path_segment(team["name"]),
|
42
|
+
board_name: Api.rfc_3986_encode_path_segment(board["name"])
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def team
|
47
|
+
@team ||= cli.prompt.choice("Select a team", teams)
|
48
|
+
end
|
49
|
+
|
50
|
+
def teams
|
51
|
+
@teams ||= api.get_paged("/_apis/projects/#{path.project_name}/teams")
|
52
|
+
end
|
53
|
+
|
39
54
|
def boards
|
40
|
-
|
55
|
+
team_name = Api.rfc_3986_encode_path_segment(team["name"])
|
56
|
+
@boards ||= api.get_paged("#{path.project_name}/#{team_name}/_apis/work/boards")
|
41
57
|
end
|
42
58
|
|
43
59
|
def api
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
cli: cli)
|
60
|
+
Api.new(organization_name: path.organization_name,
|
61
|
+
username: config.username_for_organization(path.organization_name),
|
62
|
+
access_token: config.access_token_for_organization(path.organization_name),
|
63
|
+
cli: cli)
|
49
64
|
end
|
50
65
|
end
|
51
66
|
end
|
@@ -30,7 +30,13 @@ module Abt
|
|
30
30
|
def call
|
31
31
|
work_item = select_work_item
|
32
32
|
|
33
|
-
path_with_work_item = Path.
|
33
|
+
path_with_work_item = Path.from_ids(
|
34
|
+
organization_name: path.organization_name,
|
35
|
+
project_name: path.project_name,
|
36
|
+
team_name: path.team_name,
|
37
|
+
board_name: path.board_name,
|
38
|
+
work_item_id: work_item["id"]
|
39
|
+
)
|
34
40
|
|
35
41
|
Result.new(work_item: work_item, path: path_with_work_item)
|
36
42
|
end
|
@@ -74,14 +80,14 @@ module Abt
|
|
74
80
|
end
|
75
81
|
|
76
82
|
def columns
|
77
|
-
board["columns"] ||
|
83
|
+
board["columns"] ||
|
84
|
+
api.get("#{path.project_name}/#{path.team_name}/_apis/work/boards/#{path.board_name}")["columns"]
|
78
85
|
end
|
79
86
|
|
80
87
|
private
|
81
88
|
|
82
89
|
def api
|
83
90
|
Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
|
84
|
-
project_name: path.project_name,
|
85
91
|
username: config.username_for_organization(path.organization_name),
|
86
92
|
access_token: config.access_token_for_organization(path.organization_name),
|
87
93
|
cli: cli)
|
@@ -22,7 +22,7 @@ module Abt
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def perform
|
25
|
-
abort("Flags --global and --all cannot be used
|
25
|
+
abort("Flags --global and --all cannot be used together") if flags[:global] && flags[:all]
|
26
26
|
|
27
27
|
config.clear_local unless flags[:global]
|
28
28
|
config.clear_global if flags[:global] || flags[:all]
|
@@ -4,7 +4,7 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Harvest
|
6
6
|
module Commands
|
7
|
-
class Track < BaseCommand
|
7
|
+
class Track < BaseCommand # rubocop:disable Metrics/ClassLength
|
8
8
|
def self.usage
|
9
9
|
"abt track harvest[:<project-id>/<task-id>] [options]"
|
10
10
|
end
|
@@ -20,15 +20,21 @@ module Abt
|
|
20
20
|
["-s", "--set", "Set specified task as current"],
|
21
21
|
["-c", "--comment COMMENT", "Override comment"],
|
22
22
|
["-t", "--time HOURS",
|
23
|
-
"
|
24
|
-
["-
|
23
|
+
"Track amount of hours, this will create a stopped entry."],
|
24
|
+
["-i", "--since HH:MM",
|
25
|
+
"Start entry today at specified time. The computed duration will be deducted from the running entry if one exists."] # rubocop:disable Layout/LineLength
|
25
26
|
]
|
26
27
|
end
|
27
28
|
|
28
29
|
def perform
|
30
|
+
abort("Flags --time and --since cannot be used together") if flags[:time] && flags[:since]
|
31
|
+
|
29
32
|
require_task!
|
30
33
|
|
31
|
-
|
34
|
+
maybe_adjust_previous_entry
|
35
|
+
entry = create_entry!
|
36
|
+
|
37
|
+
print_task(entry["project"], entry["task"])
|
32
38
|
|
33
39
|
maybe_override_current_task
|
34
40
|
rescue Abt::HttpError::HttpError => _e
|
@@ -37,31 +43,41 @@ module Abt
|
|
37
43
|
|
38
44
|
private
|
39
45
|
|
40
|
-
def
|
41
|
-
|
46
|
+
def create_entry!
|
47
|
+
result = api.post("time_entries", Oj.dump(entry_data, mode: :json))
|
48
|
+
api.patch("time_entries/#{result['id']}/restart") if flags.key?(:since)
|
49
|
+
result
|
42
50
|
end
|
43
51
|
|
44
|
-
def
|
45
|
-
|
52
|
+
def maybe_adjust_previous_entry
|
53
|
+
return unless flags.key?(:since)
|
54
|
+
return unless since_flag_duration # Ensure --since flag is valid before fetching data
|
55
|
+
return unless previous_entry
|
56
|
+
|
57
|
+
adjust_previous_entry
|
58
|
+
end
|
46
59
|
|
47
|
-
|
60
|
+
def adjust_previous_entry
|
61
|
+
updated_hours = previous_entry["hours"] - since_flag_duration
|
62
|
+
abort("Cannot adjust previous entry to a negative duration") if updated_hours <= 0
|
48
63
|
|
49
|
-
api.patch("time_entries/#{
|
64
|
+
api.patch("time_entries/#{previous_entry['id']}", Oj.dump({ hours: updated_hours }, mode: :json))
|
50
65
|
|
51
|
-
|
66
|
+
subtracted_minutes = (since_flag_duration * 60).round
|
67
|
+
warn("~#{subtracted_minutes} minute(s) subtracted from previous entry")
|
52
68
|
end
|
53
69
|
|
54
|
-
def
|
55
|
-
body =
|
70
|
+
def entry_data
|
71
|
+
body = entry_base_data
|
56
72
|
|
57
73
|
maybe_add_external_link(body)
|
58
74
|
maybe_add_comment(body)
|
59
|
-
|
75
|
+
maybe_add_hours(body)
|
60
76
|
|
61
77
|
body
|
62
78
|
end
|
63
79
|
|
64
|
-
def
|
80
|
+
def entry_base_data
|
65
81
|
{
|
66
82
|
project_id: project_id,
|
67
83
|
task_id: task_id,
|
@@ -74,8 +90,8 @@ module Abt
|
|
74
90
|
if external_link_data
|
75
91
|
warn(<<~TXT)
|
76
92
|
Linking to:
|
77
|
-
|
78
|
-
|
93
|
+
#{external_link_data[:notes]}
|
94
|
+
#{external_link_data[:external_reference][:permalink]}
|
79
95
|
TXT
|
80
96
|
body.merge!(external_link_data)
|
81
97
|
else
|
@@ -83,38 +99,40 @@ module Abt
|
|
83
99
|
end
|
84
100
|
end
|
85
101
|
|
86
|
-
def maybe_add_comment(body)
|
87
|
-
body[:notes] = flags[:comment] if flags.key?(:comment)
|
88
|
-
body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
|
89
|
-
end
|
90
|
-
|
91
|
-
def maybe_add_time(body)
|
92
|
-
body[:hours] = flags[:time] if flags.key?(:time)
|
93
|
-
end
|
94
|
-
|
95
102
|
def external_link_data
|
96
103
|
return @external_link_data if instance_variable_defined?(:@external_link_data)
|
104
|
+
return @external_link_data = nil if link_data_lines.empty?
|
97
105
|
|
98
|
-
|
99
|
-
|
100
|
-
return @external_link_data = nil if lines.empty?
|
101
|
-
|
102
|
-
if lines.length > 1
|
106
|
+
if link_data_lines.length > 1
|
103
107
|
abort("Got reference data from multiple scheme providers, only one is supported at a time")
|
104
108
|
end
|
105
109
|
|
106
|
-
@external_link_data = Oj.load(
|
110
|
+
@external_link_data = Oj.load(link_data_lines.first, symbol_keys: true)
|
107
111
|
end
|
108
112
|
|
109
|
-
def
|
110
|
-
|
111
|
-
|
113
|
+
def link_data_lines
|
114
|
+
@link_data_lines ||= begin
|
115
|
+
other_aris = cli.aris - [ari]
|
116
|
+
other_aris.map do |other_ari|
|
117
|
+
input = StringIO.new(other_ari.to_s)
|
118
|
+
output = StringIO.new
|
119
|
+
Abt::Cli.new(argv: ["harvest-time-entry-data"], output: output, input: input).perform
|
120
|
+
output.string.chomp
|
121
|
+
end.reject(&:empty?)
|
122
|
+
end
|
123
|
+
end
|
112
124
|
|
113
|
-
|
114
|
-
|
115
|
-
|
125
|
+
def maybe_add_comment(body)
|
126
|
+
body[:notes] = flags[:comment] if flags.key?(:comment)
|
127
|
+
body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
|
128
|
+
end
|
116
129
|
|
117
|
-
|
130
|
+
def maybe_add_hours(body)
|
131
|
+
if flags[:time]
|
132
|
+
body[:hours] = flags[:time]
|
133
|
+
elsif flags[:since]
|
134
|
+
body[:hours] = since_flag_duration
|
135
|
+
end
|
118
136
|
end
|
119
137
|
|
120
138
|
def maybe_override_current_task
|
@@ -125,6 +143,21 @@ module Abt
|
|
125
143
|
config.path = path
|
126
144
|
warn("Current task updated")
|
127
145
|
end
|
146
|
+
|
147
|
+
def since_flag_duration
|
148
|
+
@since_flag_duration ||= begin
|
149
|
+
since_hours = HarvestHelpers.decimal_hours_from_string(flags[:since])
|
150
|
+
now_hours = HarvestHelpers.decimal_hours_from_string(Time.now.strftime("%T"))
|
151
|
+
|
152
|
+
abort("Specified \"since\" time (#{flags[:since]}) is in the future") if now_hours <= since_hours
|
153
|
+
|
154
|
+
now_hours - since_hours
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def previous_entry
|
159
|
+
@previous_entry ||= api.get_paged("time_entries", is_running: true, user_id: config.user_id).first
|
160
|
+
end
|
128
161
|
end
|
129
162
|
end
|
130
163
|
end
|
@@ -15,7 +15,7 @@ module Abt
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def path
|
18
|
-
Path.new(local_available? && git["path"] || cli.directory_config.dig("harvest", "path") || "")
|
18
|
+
Path.new((local_available? && git["path"]) || cli.directory_config.dig("harvest", "path") || "")
|
19
19
|
end
|
20
20
|
|
21
21
|
def path=(new_path)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
class HarvestHelpers
|
7
|
+
class << self
|
8
|
+
HOURS_REGEX = /(?<hours>\d+)/.freeze
|
9
|
+
MINUTES_REGEX = /(?<minutes>[0-5][0-9])/.freeze
|
10
|
+
SECONDS_REGEX = /(?<seconds>[0-5][0-9])/.freeze
|
11
|
+
TIME_REGEX = /^#{HOURS_REGEX}:#{MINUTES_REGEX}(?::#{SECONDS_REGEX})?$/.freeze
|
12
|
+
|
13
|
+
def decimal_hours_from_string(hh_mm_ss)
|
14
|
+
match = TIME_REGEX.match(hh_mm_ss)
|
15
|
+
raise Abt::Cli::Abort, "Invalid time: #{hh_mm_ss}, supported formats are: HH:MM, HH:MM:SS" if match.nil?
|
16
|
+
|
17
|
+
match[:hours].to_i +
|
18
|
+
(match[:minutes].to_i / 60.0) +
|
19
|
+
(match[:seconds].to_i / 60.0 / 60.0)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -29,7 +29,7 @@ module Abt
|
|
29
29
|
def call
|
30
30
|
task = cli.prompt.choice("Select a task from #{project['name']}", tasks)
|
31
31
|
|
32
|
-
path_with_task = Path.
|
32
|
+
path_with_task = Path.from_ids(project_id: path.project_id, task_id: task["id"])
|
33
33
|
|
34
34
|
Result.new(task: task, path: path_with_task)
|
35
35
|
end
|
data/lib/abt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abt-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.32
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesper Sørensen
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-inflector
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '2.0'
|
69
|
-
description:
|
69
|
+
description:
|
70
70
|
email:
|
71
71
|
- js@abtion.com
|
72
72
|
executables:
|
@@ -118,7 +118,6 @@ files:
|
|
118
118
|
- "./lib/abt/providers/devops.rb"
|
119
119
|
- "./lib/abt/providers/devops/api.rb"
|
120
120
|
- "./lib/abt/providers/devops/base_command.rb"
|
121
|
-
- "./lib/abt/providers/devops/commands/boards.rb"
|
122
121
|
- "./lib/abt/providers/devops/commands/branch_name.rb"
|
123
122
|
- "./lib/abt/providers/devops/commands/clear.rb"
|
124
123
|
- "./lib/abt/providers/devops/commands/current.rb"
|
@@ -148,6 +147,7 @@ files:
|
|
148
147
|
- "./lib/abt/providers/harvest/commands/track.rb"
|
149
148
|
- "./lib/abt/providers/harvest/commands/write_config.rb"
|
150
149
|
- "./lib/abt/providers/harvest/configuration.rb"
|
150
|
+
- "./lib/abt/providers/harvest/harvest_helpers.rb"
|
151
151
|
- "./lib/abt/providers/harvest/path.rb"
|
152
152
|
- "./lib/abt/providers/harvest/services/project_picker.rb"
|
153
153
|
- "./lib/abt/providers/harvest/services/task_picker.rb"
|
@@ -159,7 +159,8 @@ licenses:
|
|
159
159
|
metadata:
|
160
160
|
homepage_uri: https://github.com/abtion/abt
|
161
161
|
source_code_uri: https://github.com/abtion/abt
|
162
|
-
|
162
|
+
rubygems_mfa_required: 'true'
|
163
|
+
post_install_message:
|
163
164
|
rdoc_options: []
|
164
165
|
require_paths:
|
165
166
|
- lib
|
@@ -174,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
175
|
- !ruby/object:Gem::Version
|
175
176
|
version: '0'
|
176
177
|
requirements: []
|
177
|
-
rubygems_version: 3.
|
178
|
-
signing_key:
|
178
|
+
rubygems_version: 3.2.15
|
179
|
+
signing_key:
|
179
180
|
specification_version: 4
|
180
181
|
summary: Versatile scripts
|
181
182
|
test_files: []
|
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Abt
|
4
|
-
module Providers
|
5
|
-
module Devops
|
6
|
-
module Commands
|
7
|
-
class Boards < BaseCommand
|
8
|
-
def self.usage
|
9
|
-
"abt boards devops"
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.description
|
13
|
-
"List all boards - useful for piping into grep etc"
|
14
|
-
end
|
15
|
-
|
16
|
-
def perform
|
17
|
-
prompt_project! unless project_name
|
18
|
-
|
19
|
-
boards.map do |board|
|
20
|
-
print_board(organization_name, project_name, board)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def boards
|
27
|
-
@boards ||= api.get_paged("work/boards")
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|