abt-cli 0.0.17 → 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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -3
  3. data/lib/abt.rb +6 -6
  4. data/lib/abt/ari.rb +20 -0
  5. data/lib/abt/ari_list.rb +13 -0
  6. data/lib/abt/base_command.rb +63 -0
  7. data/lib/abt/cli.rb +58 -59
  8. data/lib/abt/cli/arguments_parser.rb +8 -27
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +23 -0
  11. data/lib/abt/cli/global_commands/examples.rb +23 -0
  12. data/lib/abt/cli/global_commands/help.rb +23 -0
  13. data/lib/abt/cli/global_commands/readme.rb +23 -0
  14. data/lib/abt/cli/global_commands/share.rb +36 -0
  15. data/lib/abt/cli/global_commands/version.rb +23 -0
  16. data/lib/abt/cli/prompt.rb +52 -20
  17. data/lib/abt/docs.rb +48 -25
  18. data/lib/abt/docs/cli.rb +7 -7
  19. data/lib/abt/docs/markdown.rb +13 -12
  20. data/lib/abt/git_config.rb +21 -39
  21. data/lib/abt/providers/asana/api.rb +9 -9
  22. data/lib/abt/providers/asana/base_command.rb +16 -38
  23. data/lib/abt/providers/asana/commands/add.rb +18 -15
  24. data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
  25. data/lib/abt/providers/asana/commands/clear.rb +8 -7
  26. data/lib/abt/providers/asana/commands/current.rb +23 -38
  27. data/lib/abt/providers/asana/commands/finalize.rb +11 -16
  28. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +14 -9
  29. data/lib/abt/providers/asana/commands/init.rb +8 -41
  30. data/lib/abt/providers/asana/commands/pick.rb +22 -17
  31. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  32. data/lib/abt/providers/asana/commands/share.rb +6 -8
  33. data/lib/abt/providers/asana/commands/start.rb +26 -23
  34. data/lib/abt/providers/asana/commands/tasks.rb +6 -5
  35. data/lib/abt/providers/asana/configuration.rb +34 -40
  36. data/lib/abt/providers/asana/path.rb +36 -0
  37. data/lib/abt/providers/devops/api.rb +23 -11
  38. data/lib/abt/providers/devops/base_command.rb +18 -43
  39. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  40. data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
  41. data/lib/abt/providers/devops/commands/clear.rb +8 -7
  42. data/lib/abt/providers/devops/commands/current.rb +25 -49
  43. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +20 -12
  44. data/lib/abt/providers/devops/commands/init.rb +29 -25
  45. data/lib/abt/providers/devops/commands/pick.rb +11 -18
  46. data/lib/abt/providers/devops/commands/share.rb +7 -6
  47. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  48. data/lib/abt/providers/devops/configuration.rb +31 -56
  49. data/lib/abt/providers/devops/path.rb +51 -0
  50. data/lib/abt/providers/git/commands/branch.rb +29 -25
  51. data/lib/abt/providers/harvest/api.rb +8 -8
  52. data/lib/abt/providers/harvest/base_command.rb +18 -38
  53. data/lib/abt/providers/harvest/commands/clear.rb +8 -7
  54. data/lib/abt/providers/harvest/commands/current.rb +28 -35
  55. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  56. data/lib/abt/providers/harvest/commands/pick.rb +11 -12
  57. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  58. data/lib/abt/providers/harvest/commands/share.rb +6 -8
  59. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  60. data/lib/abt/providers/harvest/commands/stop.rb +13 -13
  61. data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
  62. data/lib/abt/providers/harvest/commands/track.rb +40 -32
  63. data/lib/abt/providers/harvest/configuration.rb +28 -37
  64. data/lib/abt/providers/harvest/path.rb +36 -0
  65. data/lib/abt/version.rb +1 -1
  66. metadata +18 -6
  67. data/lib/abt/cli/base_command.rb +0 -61
@@ -6,27 +6,28 @@ module Abt
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
8
  def self.usage
9
- 'abt clear devops'
9
+ "abt clear devops"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear DevOps configuration'
13
+ "Clear DevOps configuration"
14
14
  end
15
15
 
16
16
  def self.flags
17
17
  [
18
- ['-g', '--global', 'Clear global instead of local DevOp configuration (credentials etc.)'],
19
- ['-a', '--all', 'Clear all DevOp configuration']
18
+ ["-g", "--global",
19
+ "Clear global instead of local DevOp configuration (credentials etc.)"],
20
+ ["-a", "--all", "Clear all DevOp configuration"]
20
21
  ]
21
22
  end
22
23
 
23
24
  def perform
24
- if flags[:global] && flags[:all]
25
- abort('Flags --global and --all cannot be used at the same time')
26
- end
25
+ abort("Flags --global and --all cannot be used at the same time") if flags[:global] && flags[:all]
27
26
 
28
27
  config.clear_local unless flags[:global]
29
28
  config.clear_global if flags[:global] || flags[:all]
29
+
30
+ warn("Configuration cleared")
30
31
  end
31
32
  end
32
33
  end
@@ -6,85 +6,61 @@ module Abt
6
6
  module Commands
7
7
  class Current < BaseCommand
8
8
  def self.usage
9
- 'abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
9
+ "abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Get or set DevOps configuration for current git repository'
13
+ "Get or set DevOps configuration for current git repository"
14
14
  end
15
15
 
16
16
  def perform
17
+ abort("Must be run inside a git repository") unless config.local_available?
18
+
17
19
  require_board!
20
+ ensure_valid_configuration!
18
21
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
22
+ if path != config.path && config.local_available?
23
+ config.path = path
24
+ warn("Configuration updated")
24
25
  end
25
- end
26
-
27
- private
28
26
 
29
- def show_current_configuration
30
- if work_item_id.nil?
31
- print_board(organization_name, project_name, board)
32
- else
33
- print_work_item(organization_name, project_name, board, work_item)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_board_is_valid!
30
+ private
39
31
 
32
+ def print_configuration
40
33
  if work_item_id.nil?
41
- update_board_config
42
- config.work_item_id = nil
43
-
44
34
  print_board(organization_name, project_name, board)
45
35
  else
46
- ensure_work_item_is_valid!
47
-
48
- update_board_config
49
- config.work_item_id = work_item_id
50
-
51
36
  print_work_item(organization_name, project_name, board, work_item)
52
37
  end
53
38
  end
54
39
 
55
- def update_board_config
56
- config.organization_name = organization_name
57
- config.project_name = project_name
58
- config.board_id = board_id
59
- end
60
-
61
- def ensure_board_is_valid!
40
+ def ensure_valid_configuration!
62
41
  if board.nil?
63
- cli.abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
42
+ abort("Board could not be found, ensure that settings for organization, project, and board are correct")
64
43
  end
65
- end
66
-
67
- def ensure_work_item_is_valid!
68
- cli.abort "No such work item: ##{work_item_id}" if work_item.nil?
44
+ abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
69
45
  end
70
46
 
71
47
  def board
72
48
  @board ||= begin
73
- cli.warn 'Fetching board...'
74
- api.get("work/boards/#{board_id}")
75
- rescue HttpError::NotFoundError
76
- nil
77
- end
49
+ warn("Fetching board...")
50
+ api.get("work/boards/#{board_id}")
51
+ rescue HttpError::NotFoundError
52
+ nil
53
+ end
78
54
  end
79
55
 
80
56
  def work_item
81
57
  @work_item ||= begin
82
- cli.warn 'Fetching work item...'
83
- work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
84
- sanitize_work_item(work_item)
85
- rescue HttpError::NotFoundError
86
- nil
87
- end
58
+ warn("Fetching work item...")
59
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
60
+ sanitize_work_item(work_item)
61
+ rescue HttpError::NotFoundError
62
+ nil
63
+ end
88
64
  end
89
65
  end
90
66
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
8
  def self.usage
9
- 'abt harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
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
- 'Print Harvest time entry data for DevOps work item as json. Used by harvest start script.'
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,30 +19,38 @@ module Abt
19
19
  body = {
20
20
  notes: notes,
21
21
  external_reference: {
22
- id: work_item['id'],
23
- group_id: 'AzureDevOpsWorkItem',
24
- permalink: work_item['url']
22
+ id: work_item["id"],
23
+ group_id: "AzureDevOpsWorkItem",
24
+ permalink: work_item["url"]
25
25
  }
26
26
  }
27
27
 
28
- cli.puts Oj.dump(body, mode: :json)
28
+ puts Oj.dump(body, mode: :json)
29
+ rescue HttpError::NotFoundError
30
+ args = [organization_name, project_name, board_id, work_item_id].compact
31
+
32
+ error_message = [
33
+ "Unable to find work item for configuration:",
34
+ "devops:#{args.join('/')}"
35
+ ].join("\n")
36
+ abort(error_message)
29
37
  end
30
38
 
31
39
  private
32
40
 
33
41
  def notes
34
42
  [
35
- 'Azure DevOps',
36
- work_item['fields']['System.WorkItemType'],
43
+ "Azure DevOps",
44
+ work_item["fields"]["System.WorkItemType"],
37
45
  "##{work_item['id']}",
38
- '-',
39
- work_item['name']
40
- ].join(' ')
46
+ "-",
47
+ work_item["name"]
48
+ ].join(" ")
41
49
  end
42
50
 
43
51
  def work_item
44
52
  @work_item ||= begin
45
- work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
53
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
46
54
  sanitize_work_item(work_item)
47
55
  end
48
56
  end
@@ -9,62 +9,66 @@ module Abt
9
9
  VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
10
10
 
11
11
  def self.usage
12
- 'abt init devops'
12
+ "abt init devops"
13
13
  end
14
14
 
15
15
  def self.description
16
- 'Pick DevOps board for current git repository'
16
+ "Pick DevOps board for current git repository"
17
17
  end
18
18
 
19
19
  def perform
20
- cli.abort 'Must be run inside a git repository' unless config.local_available?
20
+ abort("Must be run inside a git repository") unless config.local_available?
21
21
 
22
- @organization_name = config.organization_name = organization_name_from_url
23
- @project_name = config.project_name = project_name_from_url
24
-
25
- board = cli.prompt.choice 'Select a project work board', boards
26
-
27
- config.board_id = board['id']
22
+ board = cli.prompt.choice("Select a project work board", boards)
28
23
 
24
+ config.path = Path.from_ids(organization_name, project_name, board["id"])
29
25
  print_board(organization_name, project_name, board)
30
26
  end
31
27
 
32
28
  private
33
29
 
34
30
  def boards
35
- @boards ||= api.get_paged('work/boards')
31
+ @boards ||= api.get_paged("work/boards")
36
32
  end
37
33
 
38
- def project_name_from_url
39
- if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
40
- (match = VS_URL_REGEX.match(project_url))
41
- match[:project]
34
+ def project_name
35
+ @project_name ||= begin
36
+ if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
37
+ (match = VS_URL_REGEX.match(project_url))
38
+ match[:project]
39
+ end
42
40
  end
43
41
  end
44
42
 
45
- def organization_name_from_url
46
- if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
47
- (match = VS_URL_REGEX.match(project_url))
48
- match[:organization]
43
+ def organization_name
44
+ @organization_name ||= begin
45
+ if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
46
+ (match = VS_URL_REGEX.match(project_url))
47
+ match[:organization]
48
+ end
49
49
  end
50
50
  end
51
51
 
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
- cli.warn 'Invalid URL'
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,54 +6,47 @@ module Abt
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
8
  def self.usage
9
- 'abt pick devops[:<organization-name>/<project-name>/<board-id>]'
9
+ "abt pick devops[:<organization-name>/<project-name>/<board-id>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Pick work item for current git repository'
13
+ "Pick work item for current git repository"
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
19
  ]
20
20
  end
21
21
 
22
22
  def perform
23
- cli.abort 'Must be run inside a git repository' unless config.local_available?
23
+ abort("Must be run inside a git repository") unless config.local_available?
24
24
  require_board!
25
25
 
26
- cli.warn "#{project_name} - #{board['name']}"
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
- update_config!(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
- def update_config!(work_item)
39
- config.organization_name = organization_name
40
- config.project_name = project_name
41
- config.board_id = board_id
42
- config.work_item_id = work_item['id']
43
- end
44
-
45
38
  def select_work_item
46
39
  loop do
47
- column = cli.prompt.choice 'Which column?', columns
48
- cli.warn 'Fetching work items...'
40
+ column = cli.prompt.choice("Which column?", columns)
41
+ warn("Fetching work items...")
49
42
  work_items = work_items_in_column(column)
50
43
 
51
44
  if work_items.length.zero?
52
- cli.warn 'Section is empty'
45
+ warn("Section is empty")
53
46
  next
54
47
  end
55
48
 
56
- work_item = cli.prompt.choice 'Select a work item', work_items, true
49
+ work_item = cli.prompt.choice("Select a work item", work_items, nil_option: true)
57
50
  return work_item if work_item
58
51
  end
59
52
  end
@@ -72,7 +65,7 @@ module Abt
72
65
  end
73
66
 
74
67
  def columns
75
- board['columns']
68
+ board["columns"]
76
69
  end
77
70
 
78
71
  def board
@@ -6,18 +6,19 @@ module Abt
6
6
  module Commands
7
7
  class Share < BaseCommand
8
8
  def self.usage
9
- 'abt share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
9
+ "abt share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print DevOps config string'
13
+ "Print DevOps ARI"
14
14
  end
15
15
 
16
16
  def perform
17
- require_work_item!
18
-
19
- args = [organization_name, project_name, board_id, work_item_id].compact
20
- cli.print_scheme_argument('devops', args.join('/'))
17
+ if path != ""
18
+ cli.print_ari("devops", path)
19
+ elsif cli.output.isatty
20
+ warn("No configuration for project. Did you initialize DevOps?")
21
+ end
21
22
  end
22
23
  end
23
24
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class WorkItems < BaseCommand
8
8
  def self.usage
9
- 'abt work-items devops'
9
+ "abt work-items devops"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'List all work items on board - useful for piping into grep etc.'
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
- cli.warn 'Fetching work items...'
28
+ warn("Fetching work items...")
29
29
  api.work_item_query(
30
30
  <<~WIQL
31
31
  SELECT [System.Id]
@@ -8,53 +8,18 @@ module Abt
8
8
 
9
9
  def initialize(cli:)
10
10
  @cli = cli
11
- @git = GitConfig.new(namespace: 'abt.devops')
12
11
  end
13
12
 
14
13
  def local_available?
15
- GitConfig.local_available?
14
+ git.available?
16
15
  end
17
16
 
18
- def organization_name
19
- local_available? ? git['organizationName'] : nil
17
+ def path
18
+ Path.new(local_available? && git["path"] || "")
20
19
  end
21
20
 
22
- def project_name
23
- local_available? ? git['projectName'] : nil
24
- end
25
-
26
- def board_id
27
- local_available? ? git['boardId'] : nil
28
- end
29
-
30
- def work_item_id
31
- local_available? ? git['workItemId'] : nil
32
- end
33
-
34
- def organization_name=(value)
35
- return if organization_name == value
36
-
37
- clear_local(verbose: false)
38
- git['organizationName'] = value unless value.nil?
39
- end
40
-
41
- def project_name=(value)
42
- return if project_name == value
43
-
44
- git['projectName'] = value unless value.nil?
45
- git['boardId'] = nil
46
- git['workItemId'] = nil
47
- end
48
-
49
- def board_id=(value)
50
- return if board_id == value
51
-
52
- git['boardId'] = value unless value.nil?
53
- git['workItemId'] = nil
54
- end
55
-
56
- def work_item_id=(value)
57
- git['workItemId'] = value
21
+ def path=(new_path)
22
+ git["path"] = new_path
58
23
  end
59
24
 
60
25
  def clear_local(verbose: true)
@@ -62,40 +27,50 @@ module Abt
62
27
  end
63
28
 
64
29
  def clear_global(verbose: true)
65
- git.global.clear(output: verbose ? cli.err_output : nil)
30
+ git_global.clear(output: verbose ? cli.err_output : nil)
66
31
  end
67
32
 
68
33
  def username_for_organization(organization_name)
69
34
  username_key = "organizations.#{organization_name}.username"
70
35
 
71
- return git.global[username_key] unless git.global[username_key].nil?
36
+ return git_global[username_key] unless git_global[username_key].nil?
72
37
 
73
- git.global[username_key] = cli.prompt.text([
38
+ git_global[username_key] = cli.prompt.text([
74
39
  "Please provide your username for the DevOps organization (#{organization_name}).",
75
- '',
76
- 'Enter username'
40
+ "",
41
+ "Enter username"
77
42
  ].join("\n"))
78
43
  end
79
44
 
80
45
  def access_token_for_organization(organization_name)
81
46
  access_token_key = "organizations.#{organization_name}.accessToken"
82
47
 
83
- return git.global[access_token_key] unless git.global[access_token_key].nil?
48
+ return git_global[access_token_key] unless git_global[access_token_key].nil?
84
49
 
85
- git.global[access_token_key] = cli.prompt.text([
86
- "Please provide your personal access token for the DevOps organization (#{organization_name}).",
87
- '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',
88
- '',
89
- 'The token MUST have "Read" permission for Work Items',
90
- 'Future features will likely require "Write" or "Manage"',
91
- '',
92
- 'Enter access token'
93
- ].join("\n"))
50
+ git_global[access_token_key] = cli.prompt.text(access_token_prompt_text)
94
51
  end
95
52
 
96
53
  private
97
54
 
98
- attr_reader :git
55
+ def git
56
+ @git ||= GitConfig.new("local", "abt.devops")
57
+ end
58
+
59
+ def git_global
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
73
+ end
99
74
  end
100
75
  end
101
76
  end