abt-cli 0.0.24 → 0.0.29

Sign up to get free protection for your applications and to get access to all the features.
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