abt-cli 0.0.15 → 0.0.20

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +4 -3
  4. data/lib/abt/ari.rb +20 -0
  5. data/lib/abt/ari_list.rb +13 -0
  6. data/lib/abt/base_command.rb +63 -0
  7. data/lib/abt/cli.rb +68 -49
  8. data/lib/abt/cli/arguments_parser.rb +48 -0
  9. data/lib/abt/cli/prompt.rb +7 -6
  10. data/lib/abt/docs.rb +35 -28
  11. data/lib/abt/docs/cli.rb +42 -11
  12. data/lib/abt/docs/markdown.rb +38 -11
  13. data/lib/abt/git_config.rb +26 -31
  14. data/lib/abt/providers/asana/base_command.rb +17 -37
  15. data/lib/abt/providers/asana/commands/add.rb +12 -10
  16. data/lib/abt/providers/asana/commands/{branch-name.rb → branch_name.rb} +12 -7
  17. data/lib/abt/providers/asana/commands/clear.rb +19 -6
  18. data/lib/abt/providers/asana/commands/current.rb +22 -37
  19. data/lib/abt/providers/asana/commands/finalize.rb +8 -12
  20. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
  21. data/lib/abt/providers/asana/commands/init.rb +9 -9
  22. data/lib/abt/providers/asana/commands/pick.rb +28 -15
  23. data/lib/abt/providers/asana/commands/projects.rb +4 -4
  24. data/lib/abt/providers/asana/commands/share.rb +5 -9
  25. data/lib/abt/providers/asana/commands/start.rb +26 -18
  26. data/lib/abt/providers/asana/commands/tasks.rb +7 -6
  27. data/lib/abt/providers/asana/configuration.rb +23 -37
  28. data/lib/abt/providers/asana/path.rb +36 -0
  29. data/lib/abt/providers/devops/api.rb +12 -0
  30. data/lib/abt/providers/devops/base_command.rb +18 -44
  31. data/lib/abt/providers/devops/commands/boards.rb +7 -5
  32. data/lib/abt/providers/devops/commands/{branch-name.rb → branch_name.rb} +10 -6
  33. data/lib/abt/providers/devops/commands/clear.rb +19 -6
  34. data/lib/abt/providers/devops/commands/current.rb +17 -41
  35. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +12 -4
  36. data/lib/abt/providers/devops/commands/init.rb +18 -18
  37. data/lib/abt/providers/devops/commands/pick.rb +16 -16
  38. data/lib/abt/providers/devops/commands/share.rb +6 -7
  39. data/lib/abt/providers/devops/commands/work-items.rb +4 -4
  40. data/lib/abt/providers/devops/configuration.rb +20 -57
  41. data/lib/abt/providers/devops/path.rb +50 -0
  42. data/lib/abt/providers/git/commands/branch.rb +28 -28
  43. data/lib/abt/providers/harvest/base_command.rb +18 -36
  44. data/lib/abt/providers/harvest/commands/clear.rb +19 -6
  45. data/lib/abt/providers/harvest/commands/current.rb +27 -34
  46. data/lib/abt/providers/harvest/commands/init.rb +8 -9
  47. data/lib/abt/providers/harvest/commands/pick.rb +15 -8
  48. data/lib/abt/providers/harvest/commands/projects.rb +4 -4
  49. data/lib/abt/providers/harvest/commands/share.rb +7 -11
  50. data/lib/abt/providers/harvest/commands/start.rb +6 -42
  51. data/lib/abt/providers/harvest/commands/stop.rb +10 -10
  52. data/lib/abt/providers/harvest/commands/tasks.rb +7 -4
  53. data/lib/abt/providers/harvest/commands/track.rb +66 -21
  54. data/lib/abt/providers/harvest/configuration.rb +23 -38
  55. data/lib/abt/providers/harvest/path.rb +36 -0
  56. data/lib/abt/version.rb +1 -1
  57. metadata +11 -7
  58. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  59. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  60. data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
@@ -5,69 +5,54 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Current < BaseCommand
8
- def self.command
9
- 'current asana[:<project-gid>[/<task-gid>]]'
8
+ def self.usage
9
+ 'abt current asana[:<project-gid>[/<task-gid>]]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Get or set project and or task for current git repository'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
+ abort 'Must be run inside a git repository' unless config.local_available?
18
+
17
19
  require_project!
20
+ ensure_valid_configuration!
18
21
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
22
+ if path != config.path
23
+ config.path = path
24
+ warn 'Configuration updated'
24
25
  end
25
- end
26
26
 
27
- private
28
-
29
- def show_current_configuration
30
- if task_gid.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_project_is_valid!
39
- config.project_gid = project_gid
40
-
41
- if task_gid.nil?
42
- print_project(project)
43
- config.task_gid = nil
44
- else
45
- ensure_task_is_valid!
46
- config.task_gid = task_gid
47
-
48
- print_task(project, task)
49
- end
50
- end
30
+ private
51
31
 
52
- def ensure_project_is_valid!
53
- cli.abort "Invalid project: #{project_gid}" if project.nil?
32
+ def print_configuration
33
+ task_gid.nil? ? print_project(project) : print_task(project, task)
54
34
  end
55
35
 
56
- def ensure_task_is_valid!
57
- cli.abort "Invalid task: #{task_gid}" if task.nil?
36
+ def ensure_valid_configuration!
37
+ abort "Invalid project: #{project_gid}" if project.nil?
38
+ abort "Invalid task: #{task_gid}" if task_gid && task.nil?
58
39
  end
59
40
 
60
41
  def project
61
42
  @project ||= begin
62
- cli.warn 'Fetching project...'
43
+ warn 'Fetching project...'
63
44
  api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
45
+ rescue Abt::HttpError::NotFoundError
46
+ nil
64
47
  end
65
48
  end
66
49
 
67
50
  def task
68
51
  @task ||= begin
69
- cli.warn 'Fetching task...'
52
+ warn 'Fetching task...'
70
53
  api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
54
+ rescue Abt::HttpError::NotFoundError
55
+ nil
71
56
  end
72
57
  end
73
58
  end
@@ -5,25 +5,25 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Finalize < BaseCommand
8
- def self.command
9
- 'finalize asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt finalize asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Move current/specified task to section (column) for finalized tasks'
14
14
  end
15
15
 
16
- def call
17
- unless config.local_available?
18
- cli.abort 'This is a no-op for tasks outside the current project'
16
+ def perform
17
+ unless project_gid == config.path.project_gid
18
+ abort 'This is a no-op for tasks outside the current project'
19
19
  end
20
20
  require_task!
21
21
  print_task(project_gid, task)
22
22
 
23
23
  if task_already_in_finalized_section?
24
- cli.warn "Task already in section: #{current_task_section['name']}"
24
+ warn "Task already in section: #{current_task_section['name']}"
25
25
  else
26
- cli.warn "Moving task to section: #{finalized_section['name']}"
26
+ warn "Moving task to section: #{finalized_section['name']}"
27
27
  move_task
28
28
  end
29
29
  end
@@ -57,11 +57,7 @@ module Abt
57
57
 
58
58
  def task
59
59
  @task ||= begin
60
- if task_gid.nil?
61
- nil
62
- else
63
- api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
64
- end
60
+ api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
65
61
  end
66
62
  end
67
63
  end
@@ -5,15 +5,15 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
- def self.command
9
- 'harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_task!
18
18
  ensure_current_is_valid!
19
19
 
@@ -26,21 +26,26 @@ module Abt
26
26
  }
27
27
  }
28
28
 
29
- cli.puts Oj.dump(body, mode: :json)
29
+ puts Oj.dump(body, mode: :json)
30
30
  end
31
31
 
32
32
  private
33
33
 
34
34
  def ensure_current_is_valid!
35
- cli.abort "Invalid task gid: #{task_gid}" if task.nil?
35
+ abort "Invalid task gid: #{task_gid}" if task.nil?
36
36
 
37
37
  return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
38
38
 
39
- cli.abort "Invalid project gid: #{project_gid}"
39
+ abort "Invalid or unmatching project gid: #{project_gid}"
40
40
  end
41
41
 
42
42
  def task
43
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
43
+ @task ||= begin
44
+ warn 'Fetching task...'
45
+ api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
46
+ rescue Abt::HttpError::NotFoundError
47
+ nil
48
+ end
44
49
  end
45
50
  end
46
51
  end
@@ -5,8 +5,8 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Init < BaseCommand
8
- def self.command
9
- 'init asana'
8
+ def self.usage
9
+ 'abt init asana'
10
10
  end
11
11
 
12
12
  def self.description
@@ -18,13 +18,13 @@ module Abt
18
18
  @cli = cli
19
19
  end
20
20
 
21
- def call
22
- cli.abort 'Must be run inside a git repository' unless config.local_available?
21
+ def perform
22
+ abort 'Must be run inside a git repository' unless config.local_available?
23
23
 
24
24
  projects # Load projects up front to make it obvious that searches are instant
25
25
  project = find_search_result
26
26
 
27
- config.project_gid = project['gid']
27
+ config.path = Path.from_ids(project['gid'])
28
28
 
29
29
  print_project(project)
30
30
  end
@@ -32,16 +32,16 @@ module Abt
32
32
  private
33
33
 
34
34
  def find_search_result
35
- cli.warn 'Select a project'
35
+ warn 'Select a project'
36
36
 
37
37
  loop do
38
38
  matches = matches_for_string cli.prompt.text('Enter search')
39
39
  if matches.empty?
40
- cli.warn 'No matches'
40
+ warn 'No matches'
41
41
  next
42
42
  end
43
43
 
44
- cli.warn 'Showing the 10 first matches' if matches.size > 10
44
+ warn 'Showing the 10 first matches' if matches.size > 10
45
45
  choice = cli.prompt.choice 'Select a project', matches[0...10], true
46
46
  break choice unless choice.nil?
47
47
  end
@@ -61,7 +61,7 @@ module Abt
61
61
 
62
62
  def projects
63
63
  @projects ||= begin
64
- cli.warn 'Fetching projects...'
64
+ warn 'Fetching projects...'
65
65
  api.get_paged('projects',
66
66
  workspace: config.workspace_gid,
67
67
  archived: false,
@@ -5,42 +5,49 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
- def self.command
9
- 'pick asana[:<project-gid>]'
8
+ def self.usage
9
+ 'abt pick asana[:<project-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Pick task for current git repository'
14
14
  end
15
15
 
16
- def call
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
16
+ def self.flags
17
+ [
18
+ ['-d', '--dry-run', 'Keep existing configuration']
19
+ ]
20
+ end
21
+
22
+ def perform
23
+ abort 'Must be run inside a git repository' unless config.local_available?
18
24
  require_project!
19
25
 
20
- cli.warn project['name']
26
+ warn project['name']
21
27
 
22
28
  task = select_task
23
29
 
24
- config.project_gid = project_gid # We might have gotten the project ID as an argument
25
- config.task_gid = task['gid']
26
-
27
30
  print_task(project, task)
31
+
32
+ return if flags[:"dry-run"]
33
+
34
+ config.path = Path.from_ids(project_gid, task['gid'])
28
35
  end
29
36
 
30
37
  private
31
38
 
32
39
  def project
33
- @project ||= api.get("projects/#{project_gid}")
40
+ @project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
34
41
  end
35
42
 
36
43
  def select_task
37
44
  loop do
38
45
  section = cli.prompt.choice 'Which section?', sections
39
- cli.warn 'Fetching tasks...'
46
+ warn 'Fetching tasks...'
40
47
  tasks = tasks_in_section(section)
41
48
 
42
49
  if tasks.length.zero?
43
- cli.warn 'Section is empty'
50
+ warn 'Section is empty'
44
51
  next
45
52
  end
46
53
 
@@ -50,15 +57,21 @@ module Abt
50
57
  end
51
58
 
52
59
  def tasks_in_section(section)
53
- api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
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.select { |task| !task['completed'] }
54
69
  end
55
70
 
56
71
  def sections
57
72
  @sections ||= begin
58
- cli.warn 'Fetching sections...'
73
+ warn 'Fetching sections...'
59
74
  api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
60
- rescue Abt::HttpError::HttpError
61
- []
62
75
  end
63
76
  end
64
77
  end
@@ -5,15 +5,15 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Projects < BaseCommand
8
- def self.command
9
- 'projects asana'
8
+ def self.usage
9
+ 'abt projects asana'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'List all available projects - useful for piping into grep etc.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  projects.map do |project|
18
18
  print_project(project)
19
19
  end
@@ -23,7 +23,7 @@ module Abt
23
23
 
24
24
  def projects
25
25
  @projects ||= begin
26
- cli.warn 'Fetching projects...'
26
+ warn 'Fetching projects...'
27
27
  api.get_paged(
28
28
  'projects',
29
29
  workspace: config.workspace_gid,
@@ -5,22 +5,18 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Share < BaseCommand
8
- def self.command
9
- 'share asana[:<project-gid>[/<task-gid>]]'
8
+ def self.usage
9
+ 'abt share asana[:<project-gid>[/<task-gid>]]'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print project/task config string'
13
+ 'Print project/task ARI'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_project!
18
18
 
19
- if task_gid.nil?
20
- cli.print_provider_command('asana', project_gid)
21
- else
22
- cli.print_provider_command('asana', "#{project_gid}/#{task_gid}")
23
- end
19
+ cli.print_ari('asana', path)
24
20
  end
25
21
  end
26
22
  end
@@ -5,58 +5,66 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Start < BaseCommand
8
- def self.command
9
- 'start asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt start asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Set current task and move it to a section (column) of your choice'
13
+ 'Move current or specified task to WIP section (column) and assign it to you'
14
14
  end
15
15
 
16
- def call
16
+ def self.flags
17
+ [
18
+ ['-s', '--set', 'Set specified task as current']
19
+ ]
20
+ end
21
+
22
+ def perform
17
23
  require_task!
18
24
 
19
- maybe_override_current_task
25
+ print_task(project_gid, task)
20
26
 
21
27
  update_assignee_if_needed
22
28
  move_if_needed
29
+ maybe_override_current_task
23
30
  end
24
31
 
25
32
  private
26
33
 
27
34
  def maybe_override_current_task
28
- return if arg_str.nil?
29
- return if same_args_as_config?
35
+ return unless flags[:set]
36
+ return if path.nil?
37
+ return if path == config.path
30
38
  return unless config.local_available?
31
39
 
32
- should_override = cli.prompt.boolean 'Set selected task as current?'
33
- Current.new(arg_str: arg_str, cli: cli).call if should_override
40
+ config.path = path
41
+ warn 'Current task updated'
34
42
  end
35
43
 
36
44
  def update_assignee_if_needed
37
45
  current_assignee = task.dig('assignee')
38
46
 
39
47
  if current_assignee.nil?
40
- cli.warn "Assigning task to user: #{current_user['name']}"
48
+ warn "Assigning task to user: #{current_user['name']}"
41
49
  update_assignee
42
50
  elsif current_assignee['gid'] == current_user['gid']
43
- cli.warn 'You are already assigned to this task'
51
+ warn 'You are already assigned to this task'
44
52
  elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
45
- cli.warn "Reassigning task to user: #{current_user['name']}"
53
+ warn "Reassigning task to user: #{current_user['name']}"
46
54
  update_assignee
47
55
  end
48
56
  end
49
57
 
50
58
  def move_if_needed
51
- unless project_gid == config.project_gid
52
- cli.warn 'Task was not moved, this is not implemented for tasks outside current project'
59
+ unless project_gid == config.path.project_gid
60
+ warn 'Task was not moved, this is not implemented for tasks outside current project'
53
61
  return
54
62
  end
55
63
 
56
64
  if task_already_in_wip_section?
57
- cli.warn "Task already in section: #{current_task_section['name']}"
65
+ warn "Task already in section: #{current_task_section['name']}"
58
66
  else
59
- cli.warn "Moving task to section: #{wip_section['name']}"
67
+ warn "Moving task to section: #{wip_section['name']}"
60
68
  move_task
61
69
  end
62
70
  end
@@ -76,7 +84,7 @@ module Abt
76
84
  end
77
85
 
78
86
  def wip_section
79
- @wip_section ||= api.get("sections/#{config.wip_section_gid}")
87
+ @wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: 'name')
80
88
  end
81
89
 
82
90
  def move_task
@@ -96,7 +104,7 @@ module Abt
96
104
  end
97
105
 
98
106
  def task
99
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name')
107
+ @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name,permalink_url')
100
108
  end
101
109
  end
102
110
  end