abt-cli 0.0.16 → 0.0.21

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/abt/ari.rb +20 -0
  3. data/lib/abt/ari_list.rb +13 -0
  4. data/lib/abt/base_command.rb +63 -0
  5. data/lib/abt/cli.rb +59 -47
  6. data/lib/abt/cli/arguments_parser.rb +9 -24
  7. data/lib/abt/cli/global_commands/commands.rb +23 -0
  8. data/lib/abt/cli/global_commands/examples.rb +23 -0
  9. data/lib/abt/cli/global_commands/help.rb +23 -0
  10. data/lib/abt/cli/global_commands/readme.rb +23 -0
  11. data/lib/abt/cli/global_commands/share.rb +36 -0
  12. data/lib/abt/cli/global_commands/version.rb +23 -0
  13. data/lib/abt/cli/prompt.rb +5 -4
  14. data/lib/abt/docs.rb +32 -15
  15. data/lib/abt/docs/cli.rb +5 -5
  16. data/lib/abt/docs/markdown.rb +8 -7
  17. data/lib/abt/git_config.rb +20 -36
  18. data/lib/abt/providers/asana/base_command.rb +15 -35
  19. data/lib/abt/providers/asana/commands/add.rb +9 -7
  20. data/lib/abt/providers/asana/commands/branch_name.rb +9 -4
  21. data/lib/abt/providers/asana/commands/clear.rb +2 -0
  22. data/lib/abt/providers/asana/commands/current.rb +19 -34
  23. data/lib/abt/providers/asana/commands/finalize.rb +5 -9
  24. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +9 -4
  25. data/lib/abt/providers/asana/commands/init.rb +6 -6
  26. data/lib/abt/providers/asana/commands/pick.rb +16 -11
  27. data/lib/abt/providers/asana/commands/projects.rb +1 -1
  28. data/lib/abt/providers/asana/commands/share.rb +5 -7
  29. data/lib/abt/providers/asana/commands/start.rb +14 -12
  30. data/lib/abt/providers/asana/commands/tasks.rb +4 -3
  31. data/lib/abt/providers/asana/configuration.rb +20 -26
  32. data/lib/abt/providers/asana/path.rb +36 -0
  33. data/lib/abt/providers/devops/api.rb +12 -0
  34. data/lib/abt/providers/devops/base_command.rb +15 -40
  35. data/lib/abt/providers/devops/commands/boards.rb +2 -2
  36. data/lib/abt/providers/devops/commands/branch_name.rb +7 -3
  37. data/lib/abt/providers/devops/commands/clear.rb +2 -0
  38. data/lib/abt/providers/devops/commands/current.rb +14 -38
  39. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +9 -1
  40. data/lib/abt/providers/devops/commands/init.rb +15 -15
  41. data/lib/abt/providers/devops/commands/pick.rb +5 -12
  42. data/lib/abt/providers/devops/commands/share.rb +6 -5
  43. data/lib/abt/providers/devops/commands/work-items.rb +1 -1
  44. data/lib/abt/providers/devops/configuration.rb +17 -46
  45. data/lib/abt/providers/devops/path.rb +50 -0
  46. data/lib/abt/providers/git/commands/branch.rb +22 -16
  47. data/lib/abt/providers/harvest/base_command.rb +16 -34
  48. data/lib/abt/providers/harvest/commands/clear.rb +2 -0
  49. data/lib/abt/providers/harvest/commands/current.rb +24 -31
  50. data/lib/abt/providers/harvest/commands/init.rb +5 -6
  51. data/lib/abt/providers/harvest/commands/pick.rb +3 -4
  52. data/lib/abt/providers/harvest/commands/projects.rb +1 -1
  53. data/lib/abt/providers/harvest/commands/share.rb +5 -7
  54. data/lib/abt/providers/harvest/commands/start.rb +1 -1
  55. data/lib/abt/providers/harvest/commands/stop.rb +7 -7
  56. data/lib/abt/providers/harvest/commands/tasks.rb +4 -1
  57. data/lib/abt/providers/harvest/commands/track.rb +26 -19
  58. data/lib/abt/providers/harvest/configuration.rb +20 -29
  59. data/lib/abt/providers/harvest/path.rb +36 -0
  60. data/lib/abt/version.rb +1 -1
  61. metadata +14 -3
  62. data/lib/abt/cli/base_command.rb +0 -61
@@ -14,16 +14,16 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- unless config.local_available?
18
- cli.abort 'This is a no-op for tasks outside the current project'
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
@@ -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
@@ -19,12 +19,12 @@ module Abt
19
19
  end
20
20
 
21
21
  def perform
22
- cli.abort 'Must be run inside a git repository' unless config.local_available?
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,
@@ -20,10 +20,10 @@ module Abt
20
20
  end
21
21
 
22
22
  def perform
23
- cli.abort 'Must be run inside a git repository' unless config.local_available?
23
+ abort 'Must be run inside a git repository' unless config.local_available?
24
24
  require_project!
25
25
 
26
- cli.warn project['name']
26
+ warn project['name']
27
27
 
28
28
  task = select_task
29
29
 
@@ -31,24 +31,23 @@ module Abt
31
31
 
32
32
  return if flags[:"dry-run"]
33
33
 
34
- config.project_gid = project_gid # We might have gotten the project ID as an argument
35
- config.task_gid = task['gid']
34
+ config.path = Path.from_ids(project_gid, task['gid'])
36
35
  end
37
36
 
38
37
  private
39
38
 
40
39
  def project
41
- @project ||= api.get("projects/#{project_gid}")
40
+ @project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
42
41
  end
43
42
 
44
43
  def select_task
45
44
  loop do
46
45
  section = cli.prompt.choice 'Which section?', sections
47
- cli.warn 'Fetching tasks...'
46
+ warn 'Fetching tasks...'
48
47
  tasks = tasks_in_section(section)
49
48
 
50
49
  if tasks.length.zero?
51
- cli.warn 'Section is empty'
50
+ warn 'Section is empty'
52
51
  next
53
52
  end
54
53
 
@@ -58,15 +57,21 @@ module Abt
58
57
  end
59
58
 
60
59
  def tasks_in_section(section)
61
- 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'] }
62
69
  end
63
70
 
64
71
  def sections
65
72
  @sections ||= begin
66
- cli.warn 'Fetching sections...'
73
+ warn 'Fetching sections...'
67
74
  api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
68
- rescue Abt::HttpError::HttpError
69
- []
70
75
  end
71
76
  end
72
77
  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,
@@ -10,16 +10,14 @@ module Abt
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
16
  def perform
17
- require_project!
18
-
19
- if task_gid.nil?
20
- cli.print_scheme_argument('asana', project_gid)
21
- else
22
- cli.print_scheme_argument('asana', "#{project_gid}/#{task_gid}")
17
+ if path != ''
18
+ cli.print_ari('asana', path)
19
+ elsif cli.output.isatty
20
+ warn 'No configuration for project. Did you initialize Asana?'
23
21
  end
24
22
  end
25
23
  end
@@ -22,10 +22,11 @@ module Abt
22
22
  def perform
23
23
  require_task!
24
24
 
25
- maybe_override_current_task
25
+ print_task(project_gid, task)
26
26
 
27
27
  update_assignee_if_needed
28
28
  move_if_needed
29
+ maybe_override_current_task
29
30
  end
30
31
 
31
32
  private
@@ -33,36 +34,37 @@ module Abt
33
34
  def maybe_override_current_task
34
35
  return unless flags[:set]
35
36
  return if path.nil?
36
- return if same_args_as_config?
37
+ return if path == config.path
37
38
  return unless config.local_available?
38
39
 
39
- Current.new(path: path, cli: cli).call
40
+ config.path = path
41
+ warn 'Current task updated'
40
42
  end
41
43
 
42
44
  def update_assignee_if_needed
43
45
  current_assignee = task.dig('assignee')
44
46
 
45
47
  if current_assignee.nil?
46
- cli.warn "Assigning task to user: #{current_user['name']}"
48
+ warn "Assigning task to user: #{current_user['name']}"
47
49
  update_assignee
48
50
  elsif current_assignee['gid'] == current_user['gid']
49
- cli.warn 'You are already assigned to this task'
51
+ warn 'You are already assigned to this task'
50
52
  elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
51
- cli.warn "Reassigning task to user: #{current_user['name']}"
53
+ warn "Reassigning task to user: #{current_user['name']}"
52
54
  update_assignee
53
55
  end
54
56
  end
55
57
 
56
58
  def move_if_needed
57
- unless project_gid == config.project_gid
58
- 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'
59
61
  return
60
62
  end
61
63
 
62
64
  if task_already_in_wip_section?
63
- cli.warn "Task already in section: #{current_task_section['name']}"
65
+ warn "Task already in section: #{current_task_section['name']}"
64
66
  else
65
- cli.warn "Moving task to section: #{wip_section['name']}"
67
+ warn "Moving task to section: #{wip_section['name']}"
66
68
  move_task
67
69
  end
68
70
  end
@@ -82,7 +84,7 @@ module Abt
82
84
  end
83
85
 
84
86
  def wip_section
85
- @wip_section ||= api.get("sections/#{config.wip_section_gid}")
87
+ @wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: 'name')
86
88
  end
87
89
 
88
90
  def move_task
@@ -102,7 +104,7 @@ module Abt
102
104
  end
103
105
 
104
106
  def task
105
- @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')
106
108
  end
107
109
  end
108
110
  end
@@ -25,14 +25,15 @@ module Abt
25
25
 
26
26
  def project
27
27
  @project ||= begin
28
- api.get("projects/#{project_gid}")
28
+ api.get("projects/#{project_gid}", opt_fields: 'name')
29
29
  end
30
30
  end
31
31
 
32
32
  def tasks
33
33
  @tasks ||= begin
34
- cli.warn 'Fetching tasks...'
35
- api.get_paged('tasks', project: project['gid'], opt_fields: 'name')
34
+ warn 'Fetching tasks...'
35
+ tasks = api.get_paged('tasks', project: project['gid'], opt_fields: 'name,completed')
36
+ tasks.select { |task| !task['completed'] }
36
37
  end
37
38
  end
38
39
  end
@@ -8,24 +8,23 @@ module Abt
8
8
 
9
9
  def initialize(cli:)
10
10
  @cli = cli
11
- @git = GitConfig.new(namespace: 'abt.asana')
12
11
  end
13
12
 
14
13
  def local_available?
15
- GitConfig.local_available?
14
+ git.available?
16
15
  end
17
16
 
18
- def project_gid
19
- local_available? ? git['projectGid'] : nil
17
+ def path
18
+ Path.new(local_available? && git['path'] || '')
20
19
  end
21
20
 
22
- def task_gid
23
- local_available? ? git['taskGid'] : nil
21
+ def path=(new_path)
22
+ git['path'] = new_path
24
23
  end
25
24
 
26
25
  def workspace_gid
27
26
  @workspace_gid ||= begin
28
- current = git.global['workspaceGid']
27
+ current = git_global['workspaceGid']
29
28
  if current.nil?
30
29
  prompt_workspace['gid']
31
30
  else
@@ -46,29 +45,18 @@ module Abt
46
45
  @finalized_section_gid ||= git['finalizedSectionGid'] || prompt_finalized_section['gid']
47
46
  end
48
47
 
49
- def project_gid=(value)
50
- return if project_gid == value
51
-
52
- clear_local(verbose: false)
53
- git['projectGid'] = value unless value.nil?
54
- end
55
-
56
- def task_gid=(value)
57
- git['taskGid'] = value
58
- end
59
-
60
48
  def clear_local(verbose: true)
61
49
  git.clear(output: verbose ? cli.err_output : nil)
62
50
  end
63
51
 
64
52
  def clear_global(verbose: true)
65
- git.global.clear(output: verbose ? cli.err_output : nil)
53
+ git_global.clear(output: verbose ? cli.err_output : nil)
66
54
  end
67
55
 
68
56
  def access_token
69
- return git.global['accessToken'] unless git.global['accessToken'].nil?
57
+ return git_global['accessToken'] unless git_global['accessToken'].nil?
70
58
 
71
- git.global['accessToken'] = cli.prompt.text([
59
+ git_global['accessToken'] = cli.prompt.text([
72
60
  'Please provide your personal access token for Asana.',
73
61
  'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
74
62
  '',
@@ -78,7 +66,13 @@ module Abt
78
66
 
79
67
  private
80
68
 
81
- attr_reader :git
69
+ def git
70
+ @git ||= GitConfig.new('local', 'abt.asana')
71
+ end
72
+
73
+ def git_global
74
+ @git_global ||= GitConfig.new('global', 'abt.asana')
75
+ end
82
76
 
83
77
  def prompt_finalized_section
84
78
  section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
@@ -94,23 +88,23 @@ module Abt
94
88
 
95
89
  def prompt_section(message)
96
90
  cli.warn 'Fetching sections...'
97
- sections = api.get_paged("projects/#{project_gid}/sections")
91
+ sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: 'name')
98
92
  cli.prompt.choice(message, sections)
99
93
  end
100
94
 
101
95
  def prompt_workspace
102
96
  cli.warn 'Fetching workspaces...'
103
- workspaces = api.get_paged('workspaces')
97
+ workspaces = api.get_paged('workspaces', opt_fields: 'name')
104
98
  if workspaces.empty?
105
99
  cli.abort 'Your asana access token does not have access to any workspaces'
106
100
  elsif workspaces.one?
107
101
  workspace = workspaces.first
108
- cli.warn "Selected Asana workspace #{workspace['name']}"
102
+ cli.warn "Selected Asana workspace: #{workspace['name']}"
109
103
  else
110
104
  workspace = cli.prompt.choice('Select Asana workspace', workspaces)
111
105
  end
112
106
 
113
- git.global['workspaceGid'] = workspace['gid']
107
+ git_global['workspaceGid'] = workspace['gid']
114
108
  workspace
115
109
  end
116
110
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ class Path < String
7
+ PATH_REGEX = %r{^(?<project_gid>\d+)?(/(?<task_gid>\d+))?$}.freeze
8
+
9
+ def self.from_ids(project_gid = nil, task_gid = nil)
10
+ path = project_gid ? [project_gid, *task_gid].join('/') : ''
11
+ new path
12
+ end
13
+
14
+ def initialize(path = '')
15
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
16
+
17
+ super
18
+ end
19
+
20
+ def project_gid
21
+ match[:project_gid]
22
+ end
23
+
24
+ def task_gid
25
+ match[:task_gid]
26
+ end
27
+
28
+ private
29
+
30
+ def match
31
+ @match ||= PATH_REGEX.match(self)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end