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,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 devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Print Harvest time entry data for DevOps work item as json. Used by harvest start script."
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
@@ -19,9 +19,9 @@ module Abt
|
|
19
19
|
body = {
|
20
20
|
notes: notes,
|
21
21
|
external_reference: {
|
22
|
-
id: work_item[
|
23
|
-
group_id:
|
24
|
-
permalink: work_item[
|
22
|
+
id: work_item["id"],
|
23
|
+
group_id: "AzureDevOpsWorkItem",
|
24
|
+
permalink: work_item["url"]
|
25
25
|
}
|
26
26
|
}
|
27
27
|
|
@@ -30,27 +30,27 @@ module Abt
|
|
30
30
|
args = [organization_name, project_name, board_id, work_item_id].compact
|
31
31
|
|
32
32
|
error_message = [
|
33
|
-
|
33
|
+
"Unable to find work item for configuration:",
|
34
34
|
"devops:#{args.join('/')}"
|
35
35
|
].join("\n")
|
36
|
-
abort
|
36
|
+
abort(error_message)
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
40
40
|
|
41
41
|
def notes
|
42
42
|
[
|
43
|
-
|
44
|
-
work_item[
|
43
|
+
"Azure DevOps",
|
44
|
+
work_item["fields"]["System.WorkItemType"],
|
45
45
|
"##{work_item['id']}",
|
46
|
-
|
47
|
-
work_item[
|
48
|
-
].join(
|
46
|
+
"-",
|
47
|
+
work_item["name"]
|
48
|
+
].join(" ")
|
49
49
|
end
|
50
50
|
|
51
51
|
def work_item
|
52
52
|
@work_item ||= begin
|
53
|
-
work_item = api.get_paged(
|
53
|
+
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
54
54
|
sanitize_work_item(work_item)
|
55
55
|
end
|
56
56
|
end
|
@@ -9,26 +9,26 @@ module Abt
|
|
9
9
|
VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
|
10
10
|
|
11
11
|
def self.usage
|
12
|
-
|
12
|
+
"abt init devops"
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.description
|
16
|
-
|
16
|
+
"Pick DevOps board for current git repository"
|
17
17
|
end
|
18
18
|
|
19
19
|
def perform
|
20
|
-
abort
|
20
|
+
abort("Must be run inside a git repository") unless config.local_available?
|
21
21
|
|
22
|
-
board = cli.prompt.choice
|
22
|
+
board = cli.prompt.choice("Select a project work board", boards)
|
23
23
|
|
24
|
-
config.path = Path.from_ids(organization_name, project_name, board[
|
24
|
+
config.path = Path.from_ids(organization_name, project_name, board["id"])
|
25
25
|
print_board(organization_name, project_name, board)
|
26
26
|
end
|
27
27
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def boards
|
31
|
-
@boards ||= api.get_paged(
|
31
|
+
@boards ||= api.get_paged("work/boards")
|
32
32
|
end
|
33
33
|
|
34
34
|
def project_name
|
@@ -52,19 +52,23 @@ module Abt
|
|
52
52
|
def project_url
|
53
53
|
@project_url ||= begin
|
54
54
|
loop do
|
55
|
-
url = cli.prompt.text(
|
56
|
-
'Please provide the URL for the devops project',
|
57
|
-
'For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}',
|
58
|
-
'',
|
59
|
-
'Enter URL'
|
60
|
-
].join("\n"))
|
55
|
+
url = cli.prompt.text(project_url_prompt_text)
|
61
56
|
|
62
57
|
break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
|
63
58
|
|
64
|
-
warn
|
59
|
+
warn("Invalid URL")
|
65
60
|
end
|
66
61
|
end
|
67
62
|
end
|
63
|
+
|
64
|
+
def project_url_prompt_text
|
65
|
+
<<~TXT
|
66
|
+
Please provide the URL for the devops project
|
67
|
+
For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
|
68
|
+
|
69
|
+
Enter URL
|
70
|
+
TXT
|
71
|
+
end
|
68
72
|
end
|
69
73
|
end
|
70
74
|
end
|
@@ -6,47 +6,47 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Pick < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt pick devops[:<organization-name>/<project-name>/<board-id>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Pick work item 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_board!
|
25
25
|
|
26
|
-
warn
|
26
|
+
warn("#{project_name} - #{board['name']}")
|
27
27
|
|
28
28
|
work_item = select_work_item
|
29
29
|
print_work_item(organization_name, project_name, board, work_item)
|
30
30
|
|
31
31
|
return if flags[:"dry-run"]
|
32
32
|
|
33
|
-
config.path = Path.from_ids(organization_name, project_name, board_id, work_item[
|
33
|
+
config.path = Path.from_ids(organization_name, project_name, board_id, work_item["id"])
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
38
|
def select_work_item
|
39
39
|
loop do
|
40
|
-
column = cli.prompt.choice
|
41
|
-
warn
|
40
|
+
column = cli.prompt.choice("Which column?", columns)
|
41
|
+
warn("Fetching work items...")
|
42
42
|
work_items = work_items_in_column(column)
|
43
43
|
|
44
44
|
if work_items.length.zero?
|
45
|
-
warn
|
45
|
+
warn("Section is empty")
|
46
46
|
next
|
47
47
|
end
|
48
48
|
|
49
|
-
work_item = cli.prompt.choice
|
49
|
+
work_item = cli.prompt.choice("Select a work item", work_items, nil_option: true)
|
50
50
|
return work_item if work_item
|
51
51
|
end
|
52
52
|
end
|
@@ -65,7 +65,7 @@ module Abt
|
|
65
65
|
end
|
66
66
|
|
67
67
|
def columns
|
68
|
-
board[
|
68
|
+
board["columns"]
|
69
69
|
end
|
70
70
|
|
71
71
|
def board
|
@@ -6,18 +6,18 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Share < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Print DevOps ARI"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
if path !=
|
18
|
-
cli.print_ari(
|
17
|
+
if path != ""
|
18
|
+
cli.print_ari("devops", path)
|
19
19
|
elsif cli.output.isatty
|
20
|
-
warn
|
20
|
+
warn("No configuration for project. Did you initialize DevOps?")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -6,11 +6,11 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class WorkItems < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt work-items devops"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"List all work items on board - useful for piping into grep etc."
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
@@ -25,7 +25,7 @@ module Abt
|
|
25
25
|
|
26
26
|
def work_items
|
27
27
|
@work_items ||= begin
|
28
|
-
warn
|
28
|
+
warn("Fetching work items...")
|
29
29
|
api.work_item_query(
|
30
30
|
<<~WIQL
|
31
31
|
SELECT [System.Id]
|
@@ -15,11 +15,11 @@ 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 clear_local(verbose: true)
|
@@ -37,8 +37,8 @@ module Abt
|
|
37
37
|
|
38
38
|
git_global[username_key] = cli.prompt.text([
|
39
39
|
"Please provide your username for the DevOps organization (#{organization_name}).",
|
40
|
-
|
41
|
-
|
40
|
+
"",
|
41
|
+
"Enter username"
|
42
42
|
].join("\n"))
|
43
43
|
end
|
44
44
|
|
@@ -47,25 +47,29 @@ module Abt
|
|
47
47
|
|
48
48
|
return git_global[access_token_key] unless git_global[access_token_key].nil?
|
49
49
|
|
50
|
-
git_global[access_token_key] = cli.prompt.text(
|
51
|
-
"Please provide your personal access token for the DevOps organization (#{organization_name}).",
|
52
|
-
'If you don\'t have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate',
|
53
|
-
'',
|
54
|
-
'The token MUST have "Read" permission for Work Items',
|
55
|
-
'Future features will likely require "Write" or "Manage"',
|
56
|
-
'',
|
57
|
-
'Enter access token'
|
58
|
-
].join("\n"))
|
50
|
+
git_global[access_token_key] = cli.prompt.text(access_token_prompt_text)
|
59
51
|
end
|
60
52
|
|
61
53
|
private
|
62
54
|
|
63
55
|
def git
|
64
|
-
@git ||= GitConfig.new(
|
56
|
+
@git ||= GitConfig.new("local", "abt.devops")
|
65
57
|
end
|
66
58
|
|
67
59
|
def git_global
|
68
|
-
@git_global ||= GitConfig.new(
|
60
|
+
@git_global ||= GitConfig.new("global", "abt.devops")
|
61
|
+
end
|
62
|
+
|
63
|
+
def access_token_prompt_text
|
64
|
+
<<~TXT
|
65
|
+
Please provide your personal access token for the DevOps organization (#{organization_name}).
|
66
|
+
If you don't have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate
|
67
|
+
|
68
|
+
The token MUST have "Read" permission for Work Items
|
69
|
+
Future features will likely require "Write" or "Manage
|
70
|
+
|
71
|
+
Enter access token"
|
72
|
+
TXT
|
69
73
|
end
|
70
74
|
end
|
71
75
|
end
|
@@ -9,16 +9,17 @@ module Abt
|
|
9
9
|
BOARD_ID_REGEX = /(?<board_id>[a-z0-9\-]+)/.freeze
|
10
10
|
WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
|
11
11
|
|
12
|
-
PATH_REGEX =
|
12
|
+
PATH_REGEX =
|
13
|
+
%r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX})?(/#{WORK_ITEM_ID_REGEX})?}.freeze
|
13
14
|
|
14
15
|
def self.from_ids(organization_id = nil, project_name = nil, board_id = nil, work_item_id = nil)
|
15
16
|
return new unless organization_id && project_name && board_id
|
16
17
|
|
17
|
-
new
|
18
|
+
new([organization_id, project_name, board_id, *work_item_id].join("/"))
|
18
19
|
end
|
19
20
|
|
20
|
-
def initialize(path =
|
21
|
-
raise Abt::Cli::Abort, "Invalid path: #{path}" unless path
|
21
|
+
def initialize(path = "")
|
22
|
+
raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
|
22
23
|
|
23
24
|
super
|
24
25
|
end
|
@@ -6,16 +6,16 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Branch < Abt::BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt branch git <scheme>[:<path>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Switch branch. Uses a compatible scheme to generate the branch-name: E.g. `abt branch git asana`"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
17
|
switch || create_and_switch
|
18
|
-
warn
|
18
|
+
warn("Switched to #{branch_name}")
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
@@ -29,8 +29,8 @@ module Abt
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def create_and_switch
|
32
|
-
warn
|
33
|
-
abort(
|
32
|
+
warn("No such branch: #{branch_name}")
|
33
|
+
abort("Aborting") unless cli.prompt.boolean("Create branch?")
|
34
34
|
|
35
35
|
Open3.popen3("git switch -c #{branch_name}") do |_i, _o, _e, thread|
|
36
36
|
thread.value
|
@@ -40,20 +40,20 @@ module Abt
|
|
40
40
|
def branch_name # rubocop:disable Metrics/MethodLength
|
41
41
|
@branch_name ||= begin
|
42
42
|
if branch_names_from_aris.empty?
|
43
|
-
abort
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
].join("\n")
|
43
|
+
abort([
|
44
|
+
"None of the specified ARIs responded to `branch-name`.",
|
45
|
+
"Did you add compatible scheme? e.g.:",
|
46
|
+
" abt branch git asana",
|
47
|
+
" abt branch git devops"
|
48
|
+
].join("\n"))
|
49
49
|
end
|
50
50
|
|
51
51
|
if branch_names_from_aris.length > 1
|
52
|
-
abort
|
53
|
-
|
54
|
-
|
52
|
+
abort([
|
53
|
+
"Got branch names from multiple ARIs, only one is supported",
|
54
|
+
"Branch names were:",
|
55
55
|
*branch_names_from_aris.map { |name| " #{name}" }
|
56
|
-
].join("\n")
|
56
|
+
].join("\n"))
|
57
57
|
end
|
58
58
|
|
59
59
|
branch_names_from_aris.first
|
@@ -63,13 +63,11 @@ module Abt
|
|
63
63
|
def branch_names_from_aris
|
64
64
|
other_aris = cli.aris - [ari]
|
65
65
|
|
66
|
-
if other_aris.empty?
|
67
|
-
abort 'You must provide an additional ARI that responds to: branch-name. E.g., asana'
|
68
|
-
end
|
66
|
+
abort("You must provide an additional ARI that responds to: branch-name. E.g., asana") if other_aris.empty?
|
69
67
|
|
70
68
|
input = StringIO.new(cli.aris.to_s)
|
71
69
|
output = StringIO.new
|
72
|
-
Abt::Cli.new(argv: [
|
70
|
+
Abt::Cli.new(argv: ["branch-name"], output: output, input: input).perform
|
73
71
|
|
74
72
|
output.string.lines.map(&:strip).compact
|
75
73
|
end
|
@@ -4,8 +4,8 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Harvest
|
6
6
|
class Api
|
7
|
-
API_ENDPOINT =
|
8
|
-
VERBS =
|
7
|
+
API_ENDPOINT = "https://api.harvestapp.com/v2"
|
8
|
+
VERBS = [:get, :post, :patch].freeze
|
9
9
|
|
10
10
|
attr_reader :access_token, :account_id
|
11
11
|
|
@@ -21,7 +21,7 @@ module Abt
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def get_paged(path, query = {})
|
24
|
-
result_key = path.split(
|
24
|
+
result_key = path.split("?").first.split("/").last
|
25
25
|
|
26
26
|
page = 1
|
27
27
|
records = []
|
@@ -29,7 +29,7 @@ module Abt
|
|
29
29
|
loop do
|
30
30
|
result = get(path, query.merge(page: page))
|
31
31
|
records += result[result_key]
|
32
|
-
break if result[
|
32
|
+
break if result["total_pages"] == page
|
33
33
|
|
34
34
|
page += 1
|
35
35
|
end
|
@@ -44,16 +44,16 @@ module Abt
|
|
44
44
|
Oj.load(response.body)
|
45
45
|
else
|
46
46
|
error_class = Abt::HttpError.error_class_for_status(response.status)
|
47
|
-
encoded_response_body = response.body.force_encoding(
|
47
|
+
encoded_response_body = response.body.force_encoding("utf-8")
|
48
48
|
raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
52
|
def connection
|
53
53
|
@connection ||= Faraday.new(API_ENDPOINT) do |connection|
|
54
|
-
connection.headers[
|
55
|
-
connection.headers[
|
56
|
-
connection.headers[
|
54
|
+
connection.headers["Authorization"] = "Bearer #{access_token}"
|
55
|
+
connection.headers["Harvest-Account-Id"] = account_id
|
56
|
+
connection.headers["Content-Type"] = "application/json"
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|