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
@@ -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"] || directory_config["path"] || "")
19
19
  end
20
20
 
21
21
  def path=(new_path)
@@ -36,13 +36,17 @@ module Abt
36
36
  def wip_section_gid
37
37
  return nil unless local_available?
38
38
 
39
- @wip_section_gid ||= git["wipSectionGid"] || prompt_wip_section["gid"]
39
+ @wip_section_gid ||= git["wipSectionGid"] ||
40
+ directory_config["wip_section_gid"] ||
41
+ prompt_wip_section["gid"]
40
42
  end
41
43
 
42
44
  def finalized_section_gid
43
45
  return nil unless local_available?
44
46
 
45
- @finalized_section_gid ||= git["finalizedSectionGid"] || prompt_finalized_section["gid"]
47
+ @finalized_section_gid ||= git["finalizedSectionGid"] ||
48
+ directory_config["finalized_section_gid"] ||
49
+ prompt_finalized_section["gid"]
46
50
  end
47
51
 
48
52
  def clear_local(verbose: true)
@@ -66,6 +70,10 @@ module Abt
66
70
 
67
71
  private
68
72
 
73
+ def directory_config
74
+ cli.directory_config.fetch("asana", {})
75
+ end
76
+
69
77
  def git
70
78
  @git ||= GitConfig.new("local", "abt.asana")
71
79
  end
@@ -6,7 +6,7 @@ module Abt
6
6
  class Path < String
7
7
  PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
8
8
 
9
- def self.from_ids(project_gid: nil, task_gid: nil)
9
+ def self.from_gids(project_gid: nil, task_gid: nil)
10
10
  path = project_gid ? [project_gid, *task_gid].join("/") : ""
11
11
  new(path)
12
12
  end
@@ -28,7 +28,7 @@ module Abt
28
28
  private
29
29
 
30
30
  def match
31
- @match ||= PATH_REGEX.match(self)
31
+ @match ||= PATH_REGEX.match(to_s)
32
32
  end
33
33
  end
34
34
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Services
7
+ class ProjectPicker
8
+ class Result
9
+ attr_reader :project, :path
10
+
11
+ def initialize(project:, path:)
12
+ @project = project
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
22
+
23
+ def initialize(cli:, config:)
24
+ @cli = cli
25
+ @config = config
26
+ end
27
+
28
+ def call
29
+ project = cli.prompt.search("Select a project", projects)
30
+ path = Path.from_gids(project_gid: project["gid"])
31
+
32
+ Result.new(project: project, path: path)
33
+ end
34
+
35
+ private
36
+
37
+ def projects
38
+ @projects ||= begin
39
+ cli.warn("Fetching projects...")
40
+ api.get_paged("projects",
41
+ workspace: config.workspace_gid,
42
+ archived: false,
43
+ opt_fields: "name,permalink_url")
44
+ end
45
+ end
46
+
47
+ def api
48
+ Abt::Providers::Asana::Api.new(access_token: config.access_token)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Services
7
+ class TaskPicker
8
+ class Result
9
+ attr_reader :task, :path
10
+
11
+ def initialize(task:, path:)
12
+ @task = task
13
+ @path = path
14
+ end
15
+ end
16
+
17
+ def self.call(**args)
18
+ new(**args).call
19
+ end
20
+
21
+ attr_reader :cli, :path, :config, :project
22
+
23
+ def initialize(cli:, path:, config:, project:)
24
+ @cli = cli
25
+ @path = path
26
+ @config = config
27
+ @project = project
28
+ end
29
+
30
+ def call
31
+ task = select_task
32
+
33
+ path_with_task = Path.from_gids(project_gid: path.project_gid, task_gid: task["gid"])
34
+
35
+ Result.new(task: task, path: path_with_task)
36
+ end
37
+
38
+ private
39
+
40
+ def select_task
41
+ section = prompt_section
42
+ tasks = tasks_in_section(section)
43
+
44
+ if tasks.length.zero?
45
+ cli.warn("Section is empty")
46
+ select_task
47
+ else
48
+ cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
49
+ end
50
+ end
51
+
52
+ def prompt_section
53
+ cli.prompt.choice("Which section in #{project['name']}?", sections)
54
+ end
55
+
56
+ def tasks_in_section(section)
57
+ cli.warn("Fetching tasks...")
58
+ tasks = api.get_paged(
59
+ "tasks",
60
+ section: section["gid"],
61
+ opt_fields: "name,completed,permalink_url"
62
+ )
63
+
64
+ # The below filtering is the best we can do with Asanas api, see this:
65
+ # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
66
+ tasks.reject { |task| task["completed"] }
67
+ end
68
+
69
+ def sections
70
+ @sections ||= begin
71
+ cli.warn("Fetching sections...")
72
+ api.get_paged("projects/#{project['gid']}/sections", opt_fields: "name")
73
+ end
74
+ end
75
+
76
+ def api
77
+ Abt::Providers::Asana::Api.new(access_token: config.access_token)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
4
4
  Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
5
+ Dir.glob("#{File.expand_path(__dir__)}/devops/services/*.rb").sort.each { |file| require file }
5
6
 
6
7
  module Abt
7
8
  module Providers
@@ -73,6 +73,16 @@ module Abt
73
73
  "#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
74
74
  end
75
75
 
76
+ def sanitize_work_item(work_item)
77
+ return nil if work_item.nil?
78
+
79
+ work_item.merge(
80
+ "id" => work_item["id"].to_s,
81
+ "name" => work_item["fields"]["System.Title"],
82
+ "url" => url_for_work_item(work_item)
83
+ )
84
+ end
85
+
76
86
  def connection
77
87
  @connection ||= Faraday.new(api_endpoint) do |connection|
78
88
  connection.basic_auth(username, access_token)
@@ -24,29 +24,49 @@ module Abt
24
24
  end
25
25
 
26
26
  def require_board!
27
- return if organization_name && project_name && board_id
27
+ return if board_id && organization_name && project_name
28
28
 
29
- abort("No current/specified board. Did you initialize DevOps?")
29
+ abort("No current/specified board. Did you forget to `pick`?")
30
30
  end
31
31
 
32
32
  def require_work_item!
33
- unless organization_name && project_name && board_id
34
- abort("No current/specified board. Did you initialize DevOps and pick a work item?")
35
- end
36
-
33
+ require_board!
37
34
  return if work_item_id
38
35
 
39
- abort("No current/specified work item. Did you pick a DevOps work item?")
36
+ abort("No current/specified work item. Did you forget to `pick`?")
37
+ end
38
+
39
+ def prompt_project!
40
+ @path = Services::ProjectPicker.call(cli: cli).path
40
41
  end
41
42
 
42
- def sanitize_work_item(work_item)
43
- return nil if work_item.nil?
43
+ def prompt_board!
44
+ result = Services::BoardPicker.call(cli: cli, path: path, config: config)
45
+ @path = result.path
46
+ @board = result.board
47
+ end
44
48
 
45
- work_item.merge(
46
- "id" => work_item["id"].to_s,
47
- "name" => work_item["fields"]["System.Title"],
48
- "url" => api.url_for_work_item(work_item)
49
- )
49
+ def prompt_work_item!
50
+ result = Services::WorkItemPicker.call(cli: cli, path: path, config: config, board: board)
51
+ @path = result.path
52
+ @work_item = result.work_item
53
+ end
54
+
55
+ def board
56
+ @board ||= begin
57
+ api.get("work/boards/#{board_id}")
58
+ rescue HttpError::NotFoundError
59
+ nil
60
+ end
61
+ end
62
+
63
+ def work_item
64
+ @work_item ||= begin
65
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
66
+ api.sanitize_work_item(work_item)
67
+ rescue HttpError::NotFoundError
68
+ nil
69
+ end
50
70
  end
51
71
 
52
72
  def print_board(organization_name, project_name, board)
@@ -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,67 +15,31 @@ 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)
34
- end
35
-
36
- private
37
-
38
- def update_config(work_item)
39
- config.path = Path.from_ids(
40
- organization_name: organization_name,
41
- project_name: project_name,
42
- board_id: board_id,
43
- work_item_id: work_item["id"]
44
- )
45
- 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
30
+ if config.local_available?
31
+ config.path = path
55
32
  else
56
- cli.prompt.choice("Select a work item", work_items, nil_option: true) || select_work_item
33
+ warn("No local configuration to update - will function as dry run")
57
34
  end
58
35
  end
59
36
 
60
- def work_items_in_column(column)
61
- work_items = api.work_item_query(
62
- <<~WIQL
63
- SELECT [System.Id]
64
- FROM WorkItems
65
- WHERE [System.BoardColumn] = '#{column['name']}'
66
- ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
67
- WIQL
68
- )
69
-
70
- work_items.map { |work_item| sanitize_work_item(work_item) }
71
- end
72
-
73
- def columns
74
- board["columns"]
75
- end
37
+ private
76
38
 
77
- def board
78
- @board ||= api.get("work/boards/#{board_id}")
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!
79
43
  end
80
44
  end
81
45
  end