abt-cli 0.0.18 → 0.0.23

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