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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/abt/docs.rb +10 -6
  3. data/lib/abt/providers/asana.rb +1 -0
  4. data/lib/abt/providers/asana/base_command.rb +33 -3
  5. data/lib/abt/providers/asana/commands/add.rb +0 -4
  6. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  7. data/lib/abt/providers/asana/commands/current.rb +0 -18
  8. data/lib/abt/providers/asana/commands/pick.rb +11 -41
  9. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  10. data/lib/abt/providers/asana/path.rb +1 -1
  11. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  12. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  13. data/lib/abt/providers/devops.rb +1 -0
  14. data/lib/abt/providers/devops/api.rb +10 -0
  15. data/lib/abt/providers/devops/base_command.rb +34 -14
  16. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  17. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  18. data/lib/abt/providers/devops/commands/current.rb +0 -19
  19. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
  20. data/lib/abt/providers/devops/commands/pick.rb +14 -53
  21. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  22. data/lib/abt/providers/devops/path.rb +2 -2
  23. data/lib/abt/providers/devops/services/board_picker.rb +54 -0
  24. data/lib/abt/providers/devops/services/project_picker.rb +79 -0
  25. data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
  26. data/lib/abt/providers/harvest.rb +1 -0
  27. data/lib/abt/providers/harvest/base_command.rb +45 -3
  28. data/lib/abt/providers/harvest/commands/current.rb +0 -28
  29. data/lib/abt/providers/harvest/commands/pick.rb +12 -27
  30. data/lib/abt/providers/harvest/commands/projects.rb +0 -5
  31. data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
  32. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  33. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  34. data/lib/abt/version.rb +1 -1
  35. metadata +9 -5
  36. data/lib/abt/providers/asana/commands/init.rb +0 -42
  37. data/lib/abt/providers/devops/commands/init.rb +0 -79
  38. data/lib/abt/providers/harvest/commands/init.rb +0 -53
@@ -14,8 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- abort("No organization selected. Did you initialize DevOps?") if organization_name.nil?
18
- abort("No project selected. Did you initialize DevOps?") if project_name.nil?
17
+ prompt_project! unless project_name
19
18
 
20
19
  boards.map do |board|
21
20
  print_board(organization_name, project_name, board)
@@ -16,15 +16,16 @@ module Abt
16
16
  def perform
17
17
  require_work_item!
18
18
 
19
- puts name
20
- rescue HttpError::NotFoundError
21
- args = [organization_name, project_name, board_id, work_item_id].compact
22
-
23
- error_message = [
24
- "Unable to find work item for configuration:",
25
- "devops:#{args.join('/')}"
26
- ].join("\n")
27
- abort(error_message)
19
+ if work_item
20
+ puts name
21
+ else
22
+ args = [organization_name, project_name, board_id, work_item_id].compact
23
+
24
+ abort(<<~TXT)
25
+ Unable to find work item for configuration:
26
+ devops:#{args.join('/')}
27
+ TXT
28
+ end
28
29
  end
29
30
 
30
31
  private
@@ -35,13 +36,6 @@ module Abt
35
36
  str += work_item["name"].downcase.gsub(/[^\w]/, "-")
36
37
  str.squeeze("-").gsub(/(^-|-$)/, "")
37
38
  end
38
-
39
- def work_item
40
- @work_item ||= begin
41
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
42
- sanitize_work_item(work_item)
43
- end
44
- end
45
39
  end
46
40
  end
47
41
  end
@@ -42,25 +42,6 @@ module Abt
42
42
  end
43
43
  abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
44
44
  end
45
-
46
- def board
47
- @board ||= begin
48
- warn("Fetching board...")
49
- api.get("work/boards/#{board_id}")
50
- rescue HttpError::NotFoundError
51
- nil
52
- end
53
- end
54
-
55
- def work_item
56
- @work_item ||= begin
57
- warn("Fetching work item...")
58
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
59
- sanitize_work_item(work_item)
60
- rescue HttpError::NotFoundError
61
- nil
62
- end
63
- end
64
45
  end
65
46
  end
66
47
  end
@@ -16,15 +16,16 @@ module Abt
16
16
  def perform
17
17
  require_work_item!
18
18
 
19
- puts Oj.dump(body, mode: :json)
20
- rescue HttpError::NotFoundError
21
- args = [organization_name, project_name, board_id, work_item_id].compact
22
-
23
- error_message = [
24
- "Unable to find work item for configuration:",
25
- "devops:#{args.join('/')}"
26
- ].join("\n")
27
- abort(error_message)
19
+ if work_item
20
+ puts Oj.dump(body, mode: :json)
21
+ else
22
+ args = [organization_name, project_name, board_id, work_item_id].compact
23
+
24
+ abort(<<~TXT)
25
+ Unable to find work item for configuration:
26
+ devops:#{args.join('/')}
27
+ TXT
28
+ end
28
29
  end
29
30
 
30
31
  private
@@ -49,13 +50,6 @@ module Abt
49
50
  work_item["name"]
50
51
  ].join(" ")
51
52
  end
52
-
53
- def work_item
54
- @work_item ||= begin
55
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
56
- sanitize_work_item(work_item)
57
- end
58
- end
59
53
  end
60
54
  end
61
55
  end
@@ -15,26 +15,33 @@ module Abt
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/board configuration"]
19
20
  ]
20
21
  end
21
22
 
22
23
  def perform
23
- require_local_config!
24
- require_board!
24
+ pick!
25
25
 
26
- warn("#{project_name} - #{board['name']}")
27
-
28
- work_item = select_work_item
29
26
  print_work_item(organization_name, project_name, board, work_item)
30
27
 
31
28
  return if flags[:"dry-run"]
32
29
 
33
- update_config(work_item)
30
+ if config.local_available?
31
+ update_config(work_item)
32
+ else
33
+ warn("No local configuration to update - will function as dry run")
34
+ end
34
35
  end
35
36
 
36
37
  private
37
38
 
39
+ def pick!
40
+ prompt_project! if project_name.nil? || flags[:clean]
41
+ prompt_board! if board_id.nil? || flags[:clean]
42
+ prompt_work_item!
43
+ end
44
+
38
45
  def update_config(work_item)
39
46
  config.path = Path.from_ids(
40
47
  organization_name: organization_name,
@@ -43,52 +50,6 @@ module Abt
43
50
  work_item_id: work_item["id"]
44
51
  )
45
52
  end
46
-
47
- def select_work_item
48
- column = cli.prompt.choice("Which column?", columns)
49
- warn("Fetching work items...")
50
- work_items = work_items_in_column(column)
51
-
52
- if work_items.length.zero?
53
- warn("Section is empty")
54
- select_work_item
55
- else
56
- prompt_work_item(work_items) || select_work_item
57
- end
58
- end
59
-
60
- def prompt_work_item(work_items)
61
- options = work_items.map do |work_item|
62
- {
63
- "id" => work_item["id"],
64
- "name" => "##{work_item['id']} #{work_item['name']}"
65
- }
66
- end
67
-
68
- choice = cli.prompt.choice("Select a work item", options, nil_option: true)
69
- choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
70
- end
71
-
72
- def work_items_in_column(column)
73
- work_items = api.work_item_query(
74
- <<~WIQL
75
- SELECT [System.Id]
76
- FROM WorkItems
77
- WHERE [System.BoardColumn] = '#{column['name']}'
78
- ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
79
- WIQL
80
- )
81
-
82
- work_items.map { |work_item| sanitize_work_item(work_item) }
83
- end
84
-
85
- def columns
86
- board["columns"]
87
- end
88
-
89
- def board
90
- @board ||= api.get("work/boards/#{board_id}")
91
- end
92
53
  end
93
54
  end
94
55
  end
@@ -14,7 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- require_board!
17
+ prompt_project! unless project_name
18
+ prompt_board! unless board_id
18
19
 
19
20
  work_items.each do |work_item|
20
21
  print_work_item(organization_name, project_name, board, work_item)
@@ -32,13 +33,9 @@ module Abt
32
33
  FROM WorkItems
33
34
  ORDER BY [System.Title] ASC
34
35
  WIQL
35
- ).map { |work_item| sanitize_work_item(work_item) }
36
+ ).map { |work_item| api.sanitize_work_item(work_item) }
36
37
  end
37
38
  end
38
-
39
- def board
40
- @board ||= api.get("work/boards/#{board_id}")
41
- end
42
39
  end
43
40
  end
44
41
  end
@@ -13,9 +13,9 @@ module Abt
13
13
  %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{BOARD_ID_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?}.freeze
14
14
 
15
15
  def self.from_ids(organization_name: nil, project_name: nil, board_id: nil, work_item_id: nil)
16
- return new unless organization_name && project_name && board_id
16
+ return new unless organization_name && project_name
17
17
 
18
- new([organization_name, project_name, board_id, *work_item_id].join("/"))
18
+ new([organization_name, project_name, *board_id, *work_item_id].join("/"))
19
19
  end
20
20
 
21
21
  def initialize(path = "")
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Services
7
+ class BoardPicker
8
+ class Result
9
+ attr_reader :board, :path
10
+
11
+ def initialize(board:, path:)
12
+ @board = board
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, :path
22
+
23
+ def initialize(cli:, path:, config:)
24
+ @cli = cli
25
+ @config = config
26
+ @path = path
27
+ end
28
+
29
+ def call
30
+ board = cli.prompt.choice("Select a project work board", boards)
31
+
32
+ path_with_board = Path.new([path, board["id"]].join("/"))
33
+
34
+ Result.new(board: board, path: path_with_board)
35
+ end
36
+
37
+ private
38
+
39
+ def boards
40
+ @boards ||= api.get_paged("work/boards")
41
+ end
42
+
43
+ def api
44
+ Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
45
+ project_name: path.project_name,
46
+ username: config.username_for_organization(path.organization_name),
47
+ access_token: config.access_token_for_organization(path.organization_name),
48
+ cli: cli)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Services
7
+ class ProjectPicker
8
+ class Result
9
+ attr_reader :board, :path
10
+
11
+ def initialize(path:)
12
+ @path = path
13
+ end
14
+ end
15
+
16
+ AZURE_DEV_URL_REGEX = %r{^https://dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)}.freeze
17
+ VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
18
+
19
+ extend Forwardable
20
+
21
+ def self.call(**args)
22
+ new(**args).call
23
+ end
24
+
25
+ attr_reader :cli
26
+
27
+ def initialize(cli:)
28
+ @cli = cli
29
+ end
30
+
31
+ def call
32
+ Result.new(
33
+ path: Path.from_ids(organization_name: organization_name, project_name: project_name)
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def project_name
40
+ @project_name ||= begin
41
+ project_url_match && project_url_match[:project]
42
+ end
43
+ end
44
+
45
+ def organization_name
46
+ @organization_name ||= begin
47
+ project_url_match && project_url_match[:organization]
48
+ end
49
+ end
50
+
51
+ def project_url_match
52
+ AZURE_DEV_URL_REGEX.match(project_url) || VS_URL_REGEX.match(project_url)
53
+ end
54
+
55
+ def project_url
56
+ @project_url ||= begin
57
+ loop do
58
+ url = prompt_url
59
+
60
+ break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
61
+
62
+ cli.warn("Invalid URL")
63
+ end
64
+ end
65
+ end
66
+
67
+ def prompt_url
68
+ cli.prompt.text(<<~TXT)
69
+ Please provide the URL for the devops project
70
+ For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
71
+
72
+ Enter URL
73
+ TXT
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Services
7
+ class WorkItemPicker
8
+ class Result
9
+ attr_reader :work_item, :path
10
+
11
+ def initialize(work_item:, path:)
12
+ @work_item = work_item
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, :path, :board
22
+
23
+ def initialize(cli:, path:, config:, board:)
24
+ @cli = cli
25
+ @config = config
26
+ @path = path
27
+ @board = board
28
+ end
29
+
30
+ def call
31
+ work_item = select_work_item
32
+
33
+ path_with_work_item = Path.new([path, work_item["id"]].join("/"))
34
+
35
+ Result.new(work_item: work_item, path: path_with_work_item)
36
+ end
37
+
38
+ def select_work_item
39
+ column = cli.prompt.choice("Which column in #{board['name']}?", columns)
40
+ cli.warn("Fetching work items...")
41
+ work_items = work_items_in_column(column)
42
+
43
+ if work_items.length.zero?
44
+ cli.warn("Section is empty")
45
+ select_work_item
46
+ else
47
+ prompt_work_item(work_items) || select_work_item
48
+ end
49
+ end
50
+
51
+ def prompt_work_item(work_items)
52
+ options = work_items.map do |work_item|
53
+ {
54
+ "id" => work_item["id"],
55
+ "name" => "##{work_item['id']} #{work_item['name']}"
56
+ }
57
+ end
58
+
59
+ choice = cli.prompt.choice("Select a work item", options, nil_option: true)
60
+ choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
61
+ end
62
+
63
+ def work_items_in_column(column)
64
+ work_items = api.work_item_query(
65
+ <<~WIQL
66
+ SELECT [System.Id]
67
+ FROM WorkItems
68
+ WHERE [System.BoardColumn] = '#{column['name']}'
69
+ ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
70
+ WIQL
71
+ )
72
+
73
+ work_items.map { |work_item| api.sanitize_work_item(work_item) }
74
+ end
75
+
76
+ def columns
77
+ board["columns"] || api.get("work/boards/#{path.board_id}")["columns"]
78
+ end
79
+
80
+ private
81
+
82
+ def api
83
+ Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
84
+ project_name: path.project_name,
85
+ username: config.username_for_organization(path.organization_name),
86
+ access_token: config.access_token_for_organization(path.organization_name),
87
+ cli: cli)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end