abt-cli 0.0.22 → 0.0.27

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