abt-cli 0.0.18 → 0.0.23

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -3
  3. data/lib/abt.rb +6 -6
  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 +51 -52
  8. data/lib/abt/cli/arguments_parser.rb +7 -26
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +23 -0
  11. data/lib/abt/cli/global_commands/examples.rb +23 -0
  12. data/lib/abt/cli/global_commands/help.rb +23 -0
  13. data/lib/abt/cli/global_commands/readme.rb +23 -0
  14. data/lib/abt/cli/global_commands/share.rb +36 -0
  15. data/lib/abt/cli/global_commands/version.rb +23 -0
  16. data/lib/abt/cli/prompt.rb +64 -51
  17. data/lib/abt/docs.rb +48 -25
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +11 -8
  20. data/lib/abt/git_config.rb +21 -39
  21. data/lib/abt/helpers.rb +26 -8
  22. data/lib/abt/providers/asana/api.rb +9 -9
  23. data/lib/abt/providers/asana/base_command.rb +20 -38
  24. data/lib/abt/providers/asana/commands/add.rb +18 -15
  25. data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +8 -7
  27. data/lib/abt/providers/asana/commands/current.rb +22 -38
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -18
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +20 -13
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +27 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +6 -8
  34. data/lib/abt/providers/asana/commands/start.rb +33 -24
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -5
  36. data/lib/abt/providers/asana/configuration.rb +46 -44
  37. data/lib/abt/providers/asana/path.rb +36 -0
  38. data/lib/abt/providers/devops/api.rb +23 -11
  39. data/lib/abt/providers/devops/base_command.rb +22 -43
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
  42. data/lib/abt/providers/devops/commands/clear.rb +8 -7
  43. data/lib/abt/providers/devops/commands/current.rb +24 -49
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +26 -16
  45. data/lib/abt/providers/devops/commands/init.rb +33 -26
  46. data/lib/abt/providers/devops/commands/pick.rb +23 -24
  47. data/lib/abt/providers/devops/commands/share.rb +7 -6
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +27 -56
  50. data/lib/abt/providers/devops/path.rb +51 -0
  51. data/lib/abt/providers/git/commands/branch.rb +25 -19
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +20 -36
  54. data/lib/abt/providers/harvest/commands/clear.rb +8 -7
  55. data/lib/abt/providers/harvest/commands/current.rb +27 -35
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -40
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -12
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +6 -8
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +13 -13
  62. data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
  63. data/lib/abt/providers/harvest/commands/track.rb +60 -38
  64. data/lib/abt/providers/harvest/configuration.rb +28 -37
  65. data/lib/abt/providers/harvest/path.rb +36 -0
  66. data/lib/abt/version.rb +1 -1
  67. metadata +18 -6
  68. data/lib/abt/cli/base_command.rb +0 -61
@@ -6,47 +6,49 @@ module Abt
6
6
  module Commands
7
7
  class Finalize < BaseCommand
8
8
  def self.usage
9
- 'abt finalize asana[:<project-gid>/<task-gid>]'
9
+ "abt finalize asana[:<project-gid>/<task-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Move current/specified task to section (column) for finalized tasks'
13
+ "Move current/specified task to section (column) for finalized tasks"
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'
19
- end
17
+ abort("This is a no-op for tasks outside the current project") unless project_gid == config.path.project_gid
20
18
  require_task!
21
19
  print_task(project_gid, task)
22
20
 
21
+ maybe_move_task
22
+ end
23
+
24
+ private
25
+
26
+ def maybe_move_task
23
27
  if task_already_in_finalized_section?
24
- cli.warn "Task already in section: #{current_task_section['name']}"
28
+ warn("Task already in section: #{current_task_section['name']}")
25
29
  else
26
- cli.warn "Moving task to section: #{finalized_section['name']}"
30
+ warn("Moving task to section: #{finalized_section['name']}")
27
31
  move_task
28
32
  end
29
33
  end
30
34
 
31
- private
32
-
33
35
  def task_already_in_finalized_section?
34
36
  !task_section_membership.nil?
35
37
  end
36
38
 
37
39
  def current_task_section
38
- task_section_membership&.dig('section')
40
+ task_section_membership&.dig("section")
39
41
  end
40
42
 
41
43
  def task_section_membership
42
- task['memberships'].find do |membership|
43
- membership.dig('section', 'gid') == config.finalized_section_gid
44
+ task["memberships"].find do |membership|
45
+ membership.dig("section", "gid") == config.finalized_section_gid
44
46
  end
45
47
  end
46
48
 
47
49
  def finalized_section
48
50
  @finalized_section ||= api.get("sections/#{config.finalized_section_gid}",
49
- opt_fields: 'name')
51
+ opt_fields: "name")
50
52
  end
51
53
 
52
54
  def move_task
@@ -57,11 +59,8 @@ module Abt
57
59
 
58
60
  def task
59
61
  @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
62
+ api.get("tasks/#{task_gid}",
63
+ opt_fields: "name,memberships.section.name,permalink_url")
65
64
  end
66
65
  end
67
66
  end
@@ -6,41 +6,48 @@ module Abt
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
8
  def self.usage
9
- 'abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
9
+ "abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
13
+ "Print Harvest time entry data for Asana task as json. Used by harvest start script."
14
14
  end
15
15
 
16
16
  def perform
17
17
  require_task!
18
18
  ensure_current_is_valid!
19
19
 
20
- body = {
21
- notes: task['name'],
20
+ puts Oj.dump(body, mode: :json)
21
+ end
22
+
23
+ private
24
+
25
+ def body
26
+ {
27
+ notes: task["name"],
22
28
  external_reference: {
23
29
  id: task_gid.to_i,
24
30
  group_id: project_gid.to_i,
25
- permalink: task['permalink_url']
31
+ permalink: task["permalink_url"]
26
32
  }
27
33
  }
28
-
29
- cli.puts Oj.dump(body, mode: :json)
30
34
  end
31
35
 
32
- private
33
-
34
36
  def ensure_current_is_valid!
35
- cli.abort "Invalid task gid: #{task_gid}" if task.nil?
37
+ abort("Invalid task gid: #{task_gid}") if task.nil?
36
38
 
37
- return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
39
+ return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
38
40
 
39
- cli.abort "Invalid project gid: #{project_gid}"
41
+ abort("Invalid or unmatching project gid: #{project_gid}")
40
42
  end
41
43
 
42
44
  def task
43
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
45
+ @task ||= begin
46
+ warn("Fetching task...")
47
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url,memberships.project")
48
+ rescue Abt::HttpError::NotFoundError
49
+ nil
50
+ end
44
51
  end
45
52
  end
46
53
  end
@@ -6,66 +6,33 @@ module Abt
6
6
  module Commands
7
7
  class Init < BaseCommand
8
8
  def self.usage
9
- 'abt init asana'
9
+ "abt init asana"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Pick Asana project for current git repository'
14
- end
15
-
16
- def initialize(cli:, **)
17
- @config = Configuration.new(cli: cli)
18
- @cli = cli
13
+ "Pick Asana project for current git repository"
19
14
  end
20
15
 
21
16
  def perform
22
- cli.abort 'Must be run inside a git repository' unless config.local_available?
17
+ require_local_config!
23
18
 
24
19
  projects # Load projects up front to make it obvious that searches are instant
25
- project = find_search_result
20
+ project = cli.prompt.search("Select a project", projects)
26
21
 
27
- config.project_gid = project['gid']
22
+ config.path = Path.from_ids(project_gid: project["gid"])
28
23
 
29
24
  print_project(project)
30
25
  end
31
26
 
32
27
  private
33
28
 
34
- def find_search_result
35
- cli.warn 'Select a project'
36
-
37
- loop do
38
- matches = matches_for_string cli.prompt.text('Enter search')
39
- if matches.empty?
40
- cli.warn 'No matches'
41
- next
42
- end
43
-
44
- cli.warn 'Showing the 10 first matches' if matches.size > 10
45
- choice = cli.prompt.choice 'Select a project', matches[0...10], true
46
- break choice unless choice.nil?
47
- end
48
- end
49
-
50
- def matches_for_string(string)
51
- search_string = sanitize_string(string)
52
-
53
- projects.select do |project|
54
- sanitize_string(project['name']).include?(search_string)
55
- end
56
- end
57
-
58
- def sanitize_string(string)
59
- string.downcase.gsub(/[^\w]/, '')
60
- end
61
-
62
29
  def projects
63
30
  @projects ||= begin
64
- cli.warn 'Fetching projects...'
65
- api.get_paged('projects',
31
+ warn("Fetching projects...")
32
+ api.get_paged("projects",
66
33
  workspace: config.workspace_gid,
67
34
  archived: false,
68
- opt_fields: 'name,permalink_url')
35
+ opt_fields: "name,permalink_url")
69
36
  end
70
37
  end
71
38
  end
@@ -6,67 +6,68 @@ module Abt
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
8
  def self.usage
9
- 'abt pick asana[:<project-gid>]'
9
+ "abt pick asana[:<project-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Pick task for current git repository'
13
+ "Pick task for current git repository"
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
19
  ]
20
20
  end
21
21
 
22
22
  def perform
23
- cli.abort 'Must be run inside a git repository' unless config.local_available?
23
+ require_local_config!
24
24
  require_project!
25
25
 
26
- cli.warn project['name']
27
-
26
+ warn(project["name"])
28
27
  task = select_task
29
28
 
30
29
  print_task(project, task)
31
30
 
32
31
  return if flags[:"dry-run"]
33
32
 
34
- config.project_gid = project_gid # We might have gotten the project ID from a path
35
- config.task_gid = task['gid']
33
+ config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
36
34
  end
37
35
 
38
36
  private
39
37
 
40
38
  def project
41
- @project ||= api.get("projects/#{project_gid}")
39
+ @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
42
40
  end
43
41
 
44
42
  def select_task
45
- loop do
46
- section = cli.prompt.choice 'Which section?', sections
47
- cli.warn 'Fetching tasks...'
48
- tasks = tasks_in_section(section)
49
-
50
- if tasks.length.zero?
51
- cli.warn 'Section is empty'
52
- next
53
- end
54
-
55
- task = cli.prompt.choice 'Select a task', tasks, true
56
- return task if task
43
+ section = cli.prompt.choice("Which section?", sections)
44
+ warn("Fetching tasks...")
45
+ tasks = tasks_in_section(section)
46
+
47
+ if tasks.length.zero?
48
+ warn("Section is empty")
49
+ select_task
50
+ else
51
+ cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
57
52
  end
58
53
  end
59
54
 
60
55
  def tasks_in_section(section)
61
- api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
56
+ tasks = api.get_paged(
57
+ "tasks",
58
+ section: section["gid"],
59
+ opt_fields: "name,completed,permalink_url"
60
+ )
61
+
62
+ # The below filtering is the best we can do with Asanas api, see this:
63
+ # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
64
+ tasks.reject { |task| task["completed"] }
62
65
  end
63
66
 
64
67
  def sections
65
68
  @sections ||= begin
66
- cli.warn 'Fetching sections...'
67
- api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
68
- rescue Abt::HttpError::HttpError
69
- []
69
+ warn("Fetching sections...")
70
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
70
71
  end
71
72
  end
72
73
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class Projects < BaseCommand
8
8
  def self.usage
9
- 'abt projects asana'
9
+ "abt projects asana"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'List all available projects - useful for piping into grep etc.'
13
+ "List all available projects - useful for piping into grep etc."
14
14
  end
15
15
 
16
16
  def perform
@@ -23,12 +23,12 @@ 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
- 'projects',
28
+ "projects",
29
29
  workspace: config.workspace_gid,
30
30
  archived: false,
31
- opt_fields: 'name'
31
+ opt_fields: "name"
32
32
  )
33
33
  end
34
34
  end
@@ -6,20 +6,18 @@ module Abt
6
6
  module Commands
7
7
  class Share < BaseCommand
8
8
  def self.usage
9
- 'abt share asana[:<project-gid>[/<task-gid>]]'
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
16
  def perform
17
- require_project!
18
-
19
- if task_gid.nil?
20
- cli.print_ari('asana', project_gid)
21
- else
22
- cli.print_ari('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
@@ -6,26 +6,27 @@ module Abt
6
6
  module Commands
7
7
  class Start < BaseCommand
8
8
  def self.usage
9
- 'abt start asana[:<project-gid>/<task-gid>]'
9
+ "abt start asana[:<project-gid>/<task-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Move current or specified task to WIP section (column) and assign it to you'
13
+ "Move current or specified task to WIP section (column) and assign it to you"
14
14
  end
15
15
 
16
16
  def self.flags
17
17
  [
18
- ['-s', '--set', 'Set specified task as current']
18
+ ["-s", "--set", "Set specified task as current"]
19
19
  ]
20
20
  end
21
21
 
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,43 @@ 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
- current_assignee = task.dig('assignee')
44
-
45
45
  if current_assignee.nil?
46
- cli.warn "Assigning task to user: #{current_user['name']}"
46
+ warn("Assigning task to user: #{current_user['name']}")
47
47
  update_assignee
48
- elsif current_assignee['gid'] == current_user['gid']
49
- cli.warn 'You are already assigned to this task'
50
- elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
51
- cli.warn "Reassigning task to user: #{current_user['name']}"
48
+ elsif current_assignee["gid"] == current_user["gid"]
49
+ warn("You are already assigned to this task")
50
+ elsif should_reassign?
51
+ warn("Reassigning task to user: #{current_user['name']}")
52
52
  update_assignee
53
53
  end
54
54
  end
55
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
+
56
64
  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'
65
+ unless project_gid == config.path.project_gid
66
+ warn("Task was not moved, this is not implemented for tasks outside current project")
59
67
  return
60
68
  end
61
69
 
62
70
  if task_already_in_wip_section?
63
- cli.warn "Task already in section: #{current_task_section['name']}"
71
+ warn("Task already in section: #{current_task_section['name']}")
64
72
  else
65
- cli.warn "Moving task to section: #{wip_section['name']}"
73
+ warn("Moving task to section: #{wip_section['name']}")
66
74
  move_task
67
75
  end
68
76
  end
@@ -72,17 +80,17 @@ module Abt
72
80
  end
73
81
 
74
82
  def current_task_section
75
- task_section_membership&.dig('section')
83
+ task_section_membership&.dig("section")
76
84
  end
77
85
 
78
86
  def task_section_membership
79
- task['memberships'].find do |membership|
80
- membership.dig('section', 'gid') == config.wip_section_gid
87
+ task["memberships"].find do |membership|
88
+ membership.dig("section", "gid") == config.wip_section_gid
81
89
  end
82
90
  end
83
91
 
84
92
  def wip_section
85
- @wip_section ||= api.get("sections/#{config.wip_section_gid}")
93
+ @wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: "name")
86
94
  end
87
95
 
88
96
  def move_task
@@ -92,17 +100,18 @@ module Abt
92
100
  end
93
101
 
94
102
  def update_assignee
95
- body = { data: { assignee: current_user['gid'] } }
103
+ body = { data: { assignee: current_user["gid"] } }
96
104
  body_json = Oj.dump(body, mode: :json)
97
105
  api.put("tasks/#{task_gid}", body_json)
98
106
  end
99
107
 
100
108
  def current_user
101
- @current_user ||= api.get('users/me', opt_fields: 'name')
109
+ @current_user ||= api.get("users/me", opt_fields: "name")
102
110
  end
103
111
 
104
112
  def task
105
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name')
113
+ @task ||= api.get("tasks/#{task_gid}",
114
+ opt_fields: "name,memberships.section.name,assignee.name,permalink_url")
106
115
  end
107
116
  end
108
117
  end