abt-cli 0.0.22 → 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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +2 -2
  3. data/lib/abt.rb +5 -0
  4. data/lib/abt/cli.rb +28 -9
  5. data/lib/abt/cli/prompt.rb +37 -53
  6. data/lib/abt/directory_config.rb +25 -0
  7. data/lib/abt/docs.rb +10 -6
  8. data/lib/abt/docs/markdown.rb +5 -2
  9. data/lib/abt/helpers.rb +26 -8
  10. data/lib/abt/providers/asana.rb +1 -0
  11. data/lib/abt/providers/asana/base_command.rb +37 -3
  12. data/lib/abt/providers/asana/commands/add.rb +0 -4
  13. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  14. data/lib/abt/providers/asana/commands/current.rb +1 -20
  15. data/lib/abt/providers/asana/commands/finalize.rb +6 -2
  16. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +7 -5
  17. data/lib/abt/providers/asana/commands/pick.rb +12 -46
  18. data/lib/abt/providers/asana/commands/start.rb +9 -3
  19. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  20. data/lib/abt/providers/asana/configuration.rb +28 -12
  21. data/lib/abt/providers/asana/path.rb +1 -1
  22. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  23. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  24. data/lib/abt/providers/devops.rb +1 -0
  25. data/lib/abt/providers/devops/api.rb +10 -0
  26. data/lib/abt/providers/devops/base_command.rb +38 -14
  27. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  28. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  29. data/lib/abt/providers/devops/commands/current.rb +1 -21
  30. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +16 -20
  31. data/lib/abt/providers/devops/commands/pick.rb +18 -39
  32. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  33. data/lib/abt/providers/devops/configuration.rb +10 -14
  34. data/lib/abt/providers/devops/path.rb +4 -4
  35. data/lib/abt/providers/devops/services/board_picker.rb +54 -0
  36. data/lib/abt/providers/devops/services/project_picker.rb +79 -0
  37. data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
  38. data/lib/abt/providers/git/commands/branch.rb +7 -3
  39. data/lib/abt/providers/harvest.rb +1 -0
  40. data/lib/abt/providers/harvest/base_command.rb +49 -3
  41. data/lib/abt/providers/harvest/commands/current.rb +1 -30
  42. data/lib/abt/providers/harvest/commands/pick.rb +12 -23
  43. data/lib/abt/providers/harvest/commands/projects.rb +0 -5
  44. data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
  45. data/lib/abt/providers/harvest/commands/track.rb +33 -19
  46. data/lib/abt/providers/harvest/configuration.rb +1 -1
  47. data/lib/abt/providers/harvest/path.rb +1 -1
  48. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  49. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  50. data/lib/abt/version.rb +1 -1
  51. metadata +10 -5
  52. data/lib/abt/providers/asana/commands/init.rb +0 -42
  53. data/lib/abt/providers/devops/commands/init.rb +0 -76
  54. data/lib/abt/providers/harvest/commands/init.rb +0 -54
@@ -14,8 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- abort("Must be run inside a git repository") unless config.local_available?
18
-
17
+ require_local_config!
19
18
  require_project!
20
19
  ensure_valid_configuration!
21
20
 
@@ -37,24 +36,6 @@ module Abt
37
36
  abort("Invalid project: #{project_gid}") if project.nil?
38
37
  abort("Invalid task: #{task_gid}") if task_gid && task.nil?
39
38
  end
40
-
41
- def project
42
- @project ||= begin
43
- warn("Fetching project...")
44
- api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
45
- rescue Abt::HttpError::NotFoundError
46
- nil
47
- end
48
- end
49
-
50
- def task
51
- @task ||= begin
52
- warn("Fetching task...")
53
- api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
54
- rescue Abt::HttpError::NotFoundError
55
- nil
56
- end
57
- end
58
39
  end
59
40
  end
60
41
  end
@@ -18,6 +18,12 @@ module Abt
18
18
  require_task!
19
19
  print_task(project_gid, task)
20
20
 
21
+ maybe_move_task
22
+ end
23
+
24
+ private
25
+
26
+ def maybe_move_task
21
27
  if task_already_in_finalized_section?
22
28
  warn("Task already in section: #{current_task_section['name']}")
23
29
  else
@@ -26,8 +32,6 @@ module Abt
26
32
  end
27
33
  end
28
34
 
29
- private
30
-
31
35
  def task_already_in_finalized_section?
32
36
  !task_section_membership.nil?
33
37
  end
@@ -17,7 +17,13 @@ module Abt
17
17
  require_task!
18
18
  ensure_current_is_valid!
19
19
 
20
- body = {
20
+ puts Oj.dump(body, mode: :json)
21
+ end
22
+
23
+ private
24
+
25
+ def body
26
+ {
21
27
  notes: task["name"],
22
28
  external_reference: {
23
29
  id: task_gid.to_i,
@@ -25,12 +31,8 @@ module Abt
25
31
  permalink: task["permalink_url"]
26
32
  }
27
33
  }
28
-
29
- puts Oj.dump(body, mode: :json)
30
34
  end
31
35
 
32
- private
33
-
34
36
  def ensure_current_is_valid!
35
37
  abort("Invalid task gid: #{task_gid}") if task.nil?
36
38
 
@@ -10,69 +10,35 @@ module Abt
10
10
  end
11
11
 
12
12
  def self.description
13
- "Pick task for current git repository"
13
+ "Pick a task and - unless told not to - make it current"
14
14
  end
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 configuration"]
19
20
  ]
20
21
  end
21
22
 
22
23
  def perform
23
- abort("Must be run inside a git repository") unless config.local_available?
24
- require_project!
25
-
26
- warn(project["name"])
27
-
28
- task = select_task
24
+ pick!
29
25
 
30
26
  print_task(project, task)
31
27
 
32
28
  return if flags[:"dry-run"]
33
29
 
34
- config.path = Path.from_ids(project_gid, task["gid"])
35
- end
36
-
37
- private
38
-
39
- def project
40
- @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
41
- end
42
-
43
- def select_task
44
- loop do
45
- section = cli.prompt.choice("Which section?", sections)
46
- warn("Fetching tasks...")
47
- tasks = tasks_in_section(section)
48
-
49
- if tasks.length.zero?
50
- warn("Section is empty")
51
- next
52
- end
53
-
54
- task = cli.prompt.choice("Select a task", tasks, nil_option: true)
55
- return task if task
30
+ if config.local_available?
31
+ config.path = Path.from_gids(project_gid: project["gid"], task_gid: task["gid"])
32
+ else
33
+ warn("No local configuration to update - will function as dry run")
56
34
  end
57
35
  end
58
36
 
59
- def tasks_in_section(section)
60
- tasks = api.get_paged(
61
- "tasks",
62
- section: section["gid"],
63
- opt_fields: "name,completed,permalink_url"
64
- )
65
-
66
- # The below filtering is the best we can do with Asanas api, see this:
67
- # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
68
- tasks.reject { |task| task["completed"] }
69
- end
37
+ private
70
38
 
71
- def sections
72
- @sections ||= begin
73
- warn("Fetching sections...")
74
- api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
75
- end
39
+ def pick!
40
+ prompt_project! if project_gid.nil? || flags[:clean]
41
+ prompt_task!
76
42
  end
77
43
  end
78
44
  end
@@ -42,19 +42,25 @@ module Abt
42
42
  end
43
43
 
44
44
  def update_assignee_if_needed
45
- current_assignee = task["assignee"]
46
-
47
45
  if current_assignee.nil?
48
46
  warn("Assigning task to user: #{current_user['name']}")
49
47
  update_assignee
50
48
  elsif current_assignee["gid"] == current_user["gid"]
51
49
  warn("You are already assigned to this task")
52
- elsif cli.prompt.boolean("Task is assigned to: #{current_assignee['name']}, take over?")
50
+ elsif should_reassign?
53
51
  warn("Reassigning task to user: #{current_user['name']}")
54
52
  update_assignee
55
53
  end
56
54
  end
57
55
 
56
+ def current_assignee
57
+ task["assignee"]
58
+ end
59
+
60
+ def should_reassign?
61
+ cli.prompt.boolean("Task is assigned to: #{current_assignee['name']}, take over?")
62
+ end
63
+
58
64
  def move_if_needed
59
65
  unless project_gid == config.path.project_gid
60
66
  warn("Task was not moved, this is not implemented for tasks outside current project")
@@ -14,7 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- require_project!
17
+ prompt_project! unless project_gid
18
18
 
19
19
  tasks.each do |task|
20
20
  print_task(project, task)
@@ -23,14 +23,9 @@ module Abt
23
23
 
24
24
  private
25
25
 
26
- def project
27
- @project ||= begin
28
- api.get("projects/#{project_gid}", opt_fields: "name")
29
- end
30
- end
31
-
32
26
  def tasks
33
27
  @tasks ||= begin
28
+ project
34
29
  warn("Fetching tasks...")
35
30
  tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
36
31
  tasks.reject { |task| task["completed"] }
@@ -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)
@@ -26,7 +26,7 @@ module Abt
26
26
  @workspace_gid ||= begin
27
27
  current = git_global["workspaceGid"]
28
28
  if current.nil?
29
- prompt_workspace["gid"]
29
+ prompt_workspace_gid
30
30
  else
31
31
  current
32
32
  end
@@ -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
+ Abt.directory_config.fetch("asana", {})
75
+ end
76
+
69
77
  def git
70
78
  @git ||= GitConfig.new("local", "abt.asana")
71
79
  end
@@ -92,20 +100,28 @@ module Abt
92
100
  cli.prompt.choice(message, sections)
93
101
  end
94
102
 
95
- def prompt_workspace
96
- cli.warn("Fetching workspaces...")
97
- workspaces = api.get_paged("workspaces", opt_fields: "name")
98
- if workspaces.empty?
99
- cli.abort("Your asana access token does not have access to any workspaces")
100
- elsif workspaces.one?
103
+ def prompt_workspace_gid
104
+ cli.abort("Your asana access token does not have access to any workspaces") if workspaces.empty?
105
+
106
+ if workspaces.one?
101
107
  workspace = workspaces.first
102
108
  cli.warn("Selected Asana workspace: #{workspace['name']}")
103
109
  else
104
- workspace = cli.prompt.choice("Select Asana workspace", workspaces)
110
+ workspace = pick_workspace
105
111
  end
106
112
 
107
113
  git_global["workspaceGid"] = workspace["gid"]
108
- workspace
114
+ end
115
+
116
+ def pick_workspace
117
+ cli.prompt.choice("Select Asana workspace", workspaces)
118
+ end
119
+
120
+ def workspaces
121
+ @workspaces ||= begin
122
+ cli.warn("Fetching workspaces...")
123
+ api.get_paged("workspaces", opt_fields: "name")
124
+ end
109
125
  end
110
126
 
111
127
  def api
@@ -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
@@ -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.new([path, task["gid"]].join("/"))
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