abt-cli 0.0.24 → 0.0.29

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +1 -0
  4. data/lib/abt/cli.rb +32 -9
  5. data/lib/abt/cli/prompt.rb +12 -9
  6. data/lib/abt/directory_config.rb +43 -0
  7. data/lib/abt/docs.rb +10 -6
  8. data/lib/abt/providers/asana.rb +1 -0
  9. data/lib/abt/providers/asana/base_command.rb +33 -3
  10. data/lib/abt/providers/asana/commands/add.rb +0 -4
  11. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  12. data/lib/abt/providers/asana/commands/current.rb +0 -18
  13. data/lib/abt/providers/asana/commands/finalize.rb +2 -4
  14. data/lib/abt/providers/asana/commands/pick.rb +11 -41
  15. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  16. data/lib/abt/providers/asana/commands/write_config.rb +73 -0
  17. data/lib/abt/providers/asana/configuration.rb +11 -3
  18. data/lib/abt/providers/asana/path.rb +2 -2
  19. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  20. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  21. data/lib/abt/providers/devops.rb +1 -0
  22. data/lib/abt/providers/devops/api.rb +10 -0
  23. data/lib/abt/providers/devops/base_command.rb +34 -14
  24. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  25. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  26. data/lib/abt/providers/devops/commands/current.rb +0 -19
  27. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
  28. data/lib/abt/providers/devops/commands/pick.rb +11 -47
  29. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  30. data/lib/abt/providers/devops/commands/write_config.rb +47 -0
  31. data/lib/abt/providers/devops/configuration.rb +1 -1
  32. data/lib/abt/providers/devops/path.rb +3 -3
  33. data/lib/abt/providers/devops/services/board_picker.rb +58 -0
  34. data/lib/abt/providers/devops/services/project_picker.rb +73 -0
  35. data/lib/abt/providers/devops/services/work_item_picker.rb +98 -0
  36. data/lib/abt/providers/git/commands/branch.rb +1 -1
  37. data/lib/abt/providers/harvest.rb +1 -0
  38. data/lib/abt/providers/harvest/base_command.rb +45 -3
  39. data/lib/abt/providers/harvest/commands/current.rb +0 -28
  40. data/lib/abt/providers/harvest/commands/pick.rb +12 -27
  41. data/lib/abt/providers/harvest/commands/projects.rb +2 -9
  42. data/lib/abt/providers/harvest/commands/tasks.rb +2 -19
  43. data/lib/abt/providers/harvest/commands/write_config.rb +41 -0
  44. data/lib/abt/providers/harvest/configuration.rb +1 -1
  45. data/lib/abt/providers/harvest/path.rb +1 -1
  46. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  47. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  48. data/lib/abt/version.rb +1 -1
  49. metadata +13 -5
  50. data/lib/abt/providers/asana/commands/init.rb +0 -42
  51. data/lib/abt/providers/devops/commands/init.rb +0 -79
  52. data/lib/abt/providers/harvest/commands/init.rb +0 -53
@@ -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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class WriteConfig < BaseCommand
8
+ def self.usage
9
+ "abt write-config devops[:<organization-name>/<project-name>/<board-id>]"
10
+ end
11
+
12
+ def self.description
13
+ "Write DevOps settings to .abt.yml"
14
+ end
15
+
16
+ def self.flags
17
+ [
18
+ ["-c", "--clean", "Don't reuse configuration"]
19
+ ]
20
+ end
21
+
22
+ def perform
23
+ prompt_project! if project_name.nil? || flags[:clean]
24
+ prompt_board! if board_id.nil? || flags[:clean]
25
+
26
+ update_directory_config!
27
+
28
+ warn("DevOps configuration written to #{Abt::DirectoryConfig::FILE_NAME}")
29
+ end
30
+
31
+ private
32
+
33
+ def update_directory_config!
34
+ cli.directory_config["devops"] = {
35
+ "path" => Path.from_ids(
36
+ organization_name: organization_name,
37
+ project_name: project_name,
38
+ board_id: board_id
39
+ ).to_s
40
+ }
41
+ cli.directory_config.save!
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -15,7 +15,7 @@ module Abt
15
15
  end
16
16
 
17
17
  def path
18
- Path.new(local_available? && git["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)
@@ -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 = "")
@@ -43,7 +43,7 @@ module Abt
43
43
  private
44
44
 
45
45
  def match
46
- @match ||= PATH_REGEX.match(self)
46
+ @match ||= PATH_REGEX.match(to_s)
47
47
  end
48
48
  end
49
49
  end
@@ -0,0 +1,58 @@
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.from_ids(
33
+ organization_name: path.organization_name,
34
+ project_name: path.project_name,
35
+ board_id: board["id"]
36
+ )
37
+
38
+ Result.new(board: board, path: path_with_board)
39
+ end
40
+
41
+ private
42
+
43
+ def boards
44
+ @boards ||= api.get_paged("work/boards")
45
+ end
46
+
47
+ def api
48
+ Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
49
+ project_name: path.project_name,
50
+ username: config.username_for_organization(path.organization_name),
51
+ access_token: config.access_token_for_organization(path.organization_name),
52
+ cli: cli)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,73 @@
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 ||= project_url_match && project_url_match[:project]
41
+ end
42
+
43
+ def organization_name
44
+ @organization_name ||= project_url_match && project_url_match[:organization]
45
+ end
46
+
47
+ def project_url_match
48
+ AZURE_DEV_URL_REGEX.match(project_url) || VS_URL_REGEX.match(project_url)
49
+ end
50
+
51
+ def project_url
52
+ @project_url ||= loop do
53
+ url = prompt_url
54
+
55
+ break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
56
+
57
+ cli.warn("Invalid URL")
58
+ end
59
+ end
60
+
61
+ def prompt_url
62
+ cli.prompt.text(<<~TXT)
63
+ Please provide the URL for the devops project
64
+ For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
65
+
66
+ Enter URL
67
+ TXT
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,98 @@
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.from_ids(
34
+ organization_name: path.organization_name,
35
+ project_name: path.project_name,
36
+ board_id: path.board_id,
37
+ work_item_id: work_item["id"]
38
+ )
39
+
40
+ Result.new(work_item: work_item, path: path_with_work_item)
41
+ end
42
+
43
+ def select_work_item
44
+ column = cli.prompt.choice("Which column in #{board['name']}?", columns)
45
+ cli.warn("Fetching work items...")
46
+ work_items = work_items_in_column(column)
47
+
48
+ if work_items.length.zero?
49
+ cli.warn("Section is empty")
50
+ select_work_item
51
+ else
52
+ prompt_work_item(work_items) || select_work_item
53
+ end
54
+ end
55
+
56
+ def prompt_work_item(work_items)
57
+ options = work_items.map do |work_item|
58
+ {
59
+ "id" => work_item["id"],
60
+ "name" => "##{work_item['id']} #{work_item['name']}"
61
+ }
62
+ end
63
+
64
+ choice = cli.prompt.choice("Select a work item", options, nil_option: true)
65
+ choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
66
+ end
67
+
68
+ def work_items_in_column(column)
69
+ work_items = api.work_item_query(
70
+ <<~WIQL
71
+ SELECT [System.Id]
72
+ FROM WorkItems
73
+ WHERE [System.BoardColumn] = '#{column['name']}'
74
+ ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
75
+ WIQL
76
+ )
77
+
78
+ work_items.map { |work_item| api.sanitize_work_item(work_item) }
79
+ end
80
+
81
+ def columns
82
+ board["columns"] || api.get("work/boards/#{path.board_id}")["columns"]
83
+ end
84
+
85
+ private
86
+
87
+ def api
88
+ Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
89
+ project_name: path.project_name,
90
+ username: config.username_for_organization(path.organization_name),
91
+ access_token: config.access_token_for_organization(path.organization_name),
92
+ cli: cli)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -30,7 +30,7 @@ module Abt
30
30
 
31
31
  def create_and_switch
32
32
  warn("No such branch: #{branch_name}")
33
- abort("Aborting") unless cli.prompt.boolean("Create branch?")
33
+ abort("Aborting") unless cli.prompt.boolean("Create branch?", default: true)
34
34
 
35
35
  Open3.popen3("git switch -c #{branch_name}") do |_i, _o, _e, thread|
36
36
  thread.value
@@ -2,6 +2,7 @@
2
2
 
3
3
  Dir.glob("#{File.expand_path(__dir__)}/harvest/*.rb").sort.each { |file| require file }
4
4
  Dir.glob("#{File.expand_path(__dir__)}/harvest/commands/*.rb").sort.each { |file| require file }
5
+ Dir.glob("#{File.expand_path(__dir__)}/harvest/services/*.rb").sort.each { |file| require file }
5
6
 
6
7
  module Abt
7
8
  module Providers
@@ -26,13 +26,55 @@ module Abt
26
26
  def require_project!
27
27
  return if project_id
28
28
 
29
- abort("No current/specified project. Did you initialize Harvest?")
29
+ abort("No current/specified project. Did you forget to run `pick`?")
30
30
  end
31
31
 
32
32
  def require_task!
33
- abort("No current/specified project. Did you initialize Harvest and pick a task?") unless project_id
33
+ require_project!
34
+ return if task_id
34
35
 
35
- abort("No current/specified task. Did you pick a Harvest task?") if task_id.nil?
36
+ abort("No current/specified task. Did you forget to run `pick`?")
37
+ end
38
+
39
+ def prompt_project!
40
+ result = Services::ProjectPicker.call(cli: cli, project_assignments: project_assignments)
41
+ @path = result.path
42
+ @project = result.project
43
+ end
44
+
45
+ def prompt_task!
46
+ result = Services::TaskPicker.call(cli: cli, path: path, project_assignment: project_assignment)
47
+ @path = result.path
48
+ @task = result.task
49
+ end
50
+
51
+ def task
52
+ return @task if instance_variable_defined?(:@task)
53
+
54
+ @task = if project_assignment
55
+ project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
56
+ task["id"].to_s == task_id
57
+ end
58
+ end
59
+ end
60
+
61
+ def project
62
+ return @project if instance_variable_defined?(:@project)
63
+
64
+ @project = if project_assignment
65
+ project_assignment["project"].merge("client" => project_assignment["client"])
66
+ end
67
+ end
68
+
69
+ def project_assignment
70
+ @project_assignment ||= project_assignments.find { |pa| pa["project"]["id"].to_s == path.project_id }
71
+ end
72
+
73
+ def project_assignments
74
+ @project_assignments ||= begin
75
+ warn("Fetching Harvest data...")
76
+ api.get_paged("users/me/project_assignments")
77
+ end
36
78
  end
37
79
 
38
80
  def print_project(project)
@@ -40,34 +40,6 @@ module Abt
40
40
  abort("Invalid project: #{project_id}") if project.nil?
41
41
  abort("Invalid task: #{task_id}") if task_id && task.nil?
42
42
  end
43
-
44
- def project
45
- return @project if instance_variable_defined?(:@project)
46
-
47
- @project = if project_assignment
48
- project_assignment["project"].merge("client" => project_assignment["client"])
49
- end
50
- end
51
-
52
- def task
53
- return @task if instance_variable_defined?(:@task)
54
-
55
- @task = if project_assignment
56
- project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
57
- task["id"].to_s == task_id
58
- end
59
- end
60
- end
61
-
62
- def project_assignment
63
- @project_assignment ||= begin
64
- project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
65
- end
66
- end
67
-
68
- def project_assignments
69
- @project_assignments ||= api.get_paged("users/me/project_assignments")
70
- end
71
43
  end
72
44
  end
73
45
  end