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
@@ -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