abt-cli 0.0.21 → 0.0.26

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 +11 -6
  4. data/lib/abt/ari.rb +1 -1
  5. data/lib/abt/ari_list.rb +1 -1
  6. data/lib/abt/base_command.rb +7 -7
  7. data/lib/abt/cli.rb +55 -49
  8. data/lib/abt/cli/arguments_parser.rb +5 -9
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +2 -2
  11. data/lib/abt/cli/global_commands/examples.rb +2 -2
  12. data/lib/abt/cli/global_commands/help.rb +2 -2
  13. data/lib/abt/cli/global_commands/readme.rb +2 -2
  14. data/lib/abt/cli/global_commands/share.rb +6 -6
  15. data/lib/abt/cli/global_commands/version.rb +2 -2
  16. data/lib/abt/cli/prompt.rb +71 -56
  17. data/lib/abt/directory_config.rb +25 -0
  18. data/lib/abt/docs.rb +39 -33
  19. data/lib/abt/docs/cli.rb +3 -3
  20. data/lib/abt/docs/markdown.rb +10 -7
  21. data/lib/abt/git_config.rb +4 -6
  22. data/lib/abt/helpers.rb +26 -8
  23. data/lib/abt/providers/asana/api.rb +9 -9
  24. data/lib/abt/providers/asana/base_command.rb +12 -10
  25. data/lib/abt/providers/asana/commands/add.rb +13 -12
  26. data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
  27. data/lib/abt/providers/asana/commands/clear.rb +7 -8
  28. data/lib/abt/providers/asana/commands/current.rb +14 -15
  29. data/lib/abt/providers/asana/commands/finalize.rb +17 -14
  30. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
  31. data/lib/abt/providers/asana/commands/init.rb +8 -41
  32. data/lib/abt/providers/asana/commands/pick.rb +22 -26
  33. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  34. data/lib/abt/providers/asana/commands/share.rb +5 -5
  35. data/lib/abt/providers/asana/commands/start.rb +28 -21
  36. data/lib/abt/providers/asana/commands/tasks.rb +6 -6
  37. data/lib/abt/providers/asana/configuration.rb +45 -29
  38. data/lib/abt/providers/asana/path.rb +6 -6
  39. data/lib/abt/providers/devops/api.rb +12 -12
  40. data/lib/abt/providers/devops/base_command.rb +14 -10
  41. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  42. data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
  43. data/lib/abt/providers/devops/commands/clear.rb +7 -8
  44. data/lib/abt/providers/devops/commands/current.rb +17 -18
  45. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
  46. data/lib/abt/providers/devops/commands/init.rb +21 -14
  47. data/lib/abt/providers/devops/commands/pick.rb +37 -19
  48. data/lib/abt/providers/devops/commands/share.rb +5 -5
  49. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  50. data/lib/abt/providers/devops/configuration.rb +15 -15
  51. data/lib/abt/providers/devops/path.rb +7 -6
  52. data/lib/abt/providers/git/commands/branch.rb +23 -21
  53. data/lib/abt/providers/harvest/api.rb +8 -8
  54. data/lib/abt/providers/harvest/base_command.rb +10 -8
  55. data/lib/abt/providers/harvest/commands/clear.rb +7 -8
  56. data/lib/abt/providers/harvest/commands/current.rb +13 -14
  57. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  58. data/lib/abt/providers/harvest/commands/pick.rb +15 -11
  59. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  60. data/lib/abt/providers/harvest/commands/share.rb +5 -5
  61. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  62. data/lib/abt/providers/harvest/commands/stop.rb +12 -12
  63. data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
  64. data/lib/abt/providers/harvest/commands/track.rb +52 -37
  65. data/lib/abt/providers/harvest/configuration.rb +18 -18
  66. data/lib/abt/providers/harvest/path.rb +6 -6
  67. data/lib/abt/version.rb +1 -1
  68. metadata +7 -5
@@ -6,72 +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
- abort 'Must be run inside a git repository' unless config.local_available?
23
+ require_local_config!
24
24
  require_project!
25
25
 
26
- 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.path = Path.from_ids(project_gid, task['gid'])
33
+ config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
35
34
  end
36
35
 
37
36
  private
38
37
 
39
38
  def project
40
- @project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
39
+ @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
41
40
  end
42
41
 
43
42
  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, true
55
- 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
56
52
  end
57
53
  end
58
54
 
59
55
  def tasks_in_section(section)
60
56
  tasks = api.get_paged(
61
- 'tasks',
62
- section: section['gid'],
63
- opt_fields: 'name,completed,permalink_url'
57
+ "tasks",
58
+ section: section["gid"],
59
+ opt_fields: "name,completed,permalink_url"
64
60
  )
65
61
 
66
62
  # The below filtering is the best we can do with Asanas api, see this:
67
63
  # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
68
- tasks.select { |task| !task['completed'] }
64
+ tasks.reject { |task| task["completed"] }
69
65
  end
70
66
 
71
67
  def sections
72
68
  @sections ||= begin
73
- warn 'Fetching sections...'
74
- api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
69
+ warn("Fetching sections...")
70
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
75
71
  end
76
72
  end
77
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
- 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,18 +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 ARI'
13
+ "Print project/task ARI"
14
14
  end
15
15
 
16
16
  def perform
17
- if path != ''
18
- cli.print_ari('asana', path)
17
+ if path != ""
18
+ cli.print_ari("asana", path)
19
19
  elsif cli.output.isatty
20
- warn 'No configuration for project. Did you initialize Asana?'
20
+ warn("No configuration for project. Did you initialize Asana?")
21
21
  end
22
22
  end
23
23
  end
@@ -6,16 +6,16 @@ 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
 
@@ -38,33 +38,39 @@ module Abt
38
38
  return unless config.local_available?
39
39
 
40
40
  config.path = path
41
- warn 'Current task updated'
41
+ warn("Current task updated")
42
42
  end
43
43
 
44
44
  def update_assignee_if_needed
45
- current_assignee = task.dig('assignee')
46
-
47
45
  if current_assignee.nil?
48
- warn "Assigning task to user: #{current_user['name']}"
46
+ warn("Assigning task to user: #{current_user['name']}")
49
47
  update_assignee
50
- elsif current_assignee['gid'] == current_user['gid']
51
- warn 'You are already assigned to this task'
52
- elsif cli.prompt.boolean "Task is assigned to: #{current_assignee['name']}, take over?"
53
- 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']}")
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
- warn 'Task was not moved, this is not implemented for tasks outside current project'
66
+ warn("Task was not moved, this is not implemented for tasks outside current project")
61
67
  return
62
68
  end
63
69
 
64
70
  if task_already_in_wip_section?
65
- warn "Task already in section: #{current_task_section['name']}"
71
+ warn("Task already in section: #{current_task_section['name']}")
66
72
  else
67
- warn "Moving task to section: #{wip_section['name']}"
73
+ warn("Moving task to section: #{wip_section['name']}")
68
74
  move_task
69
75
  end
70
76
  end
@@ -74,17 +80,17 @@ module Abt
74
80
  end
75
81
 
76
82
  def current_task_section
77
- task_section_membership&.dig('section')
83
+ task_section_membership&.dig("section")
78
84
  end
79
85
 
80
86
  def task_section_membership
81
- task['memberships'].find do |membership|
82
- membership.dig('section', 'gid') == config.wip_section_gid
87
+ task["memberships"].find do |membership|
88
+ membership.dig("section", "gid") == config.wip_section_gid
83
89
  end
84
90
  end
85
91
 
86
92
  def wip_section
87
- @wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: 'name')
93
+ @wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: "name")
88
94
  end
89
95
 
90
96
  def move_task
@@ -94,17 +100,18 @@ module Abt
94
100
  end
95
101
 
96
102
  def update_assignee
97
- body = { data: { assignee: current_user['gid'] } }
103
+ body = { data: { assignee: current_user["gid"] } }
98
104
  body_json = Oj.dump(body, mode: :json)
99
105
  api.put("tasks/#{task_gid}", body_json)
100
106
  end
101
107
 
102
108
  def current_user
103
- @current_user ||= api.get('users/me', opt_fields: 'name')
109
+ @current_user ||= api.get("users/me", opt_fields: "name")
104
110
  end
105
111
 
106
112
  def task
107
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name,permalink_url')
113
+ @task ||= api.get("tasks/#{task_gid}",
114
+ opt_fields: "name,memberships.section.name,assignee.name,permalink_url")
108
115
  end
109
116
  end
110
117
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class Tasks < BaseCommand
8
8
  def self.usage
9
- 'abt tasks asana'
9
+ "abt tasks asana"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'List available tasks on project - useful for piping into grep etc.'
13
+ "List available tasks on project - useful for piping into grep etc."
14
14
  end
15
15
 
16
16
  def perform
@@ -25,15 +25,15 @@ module Abt
25
25
 
26
26
  def project
27
27
  @project ||= begin
28
- api.get("projects/#{project_gid}", opt_fields: 'name')
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
- warn 'Fetching tasks...'
35
- tasks = api.get_paged('tasks', project: project['gid'], opt_fields: 'name,completed')
36
- tasks.select { |task| !task['completed'] }
34
+ warn("Fetching tasks...")
35
+ tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
36
+ tasks.reject { |task| task["completed"] }
37
37
  end
38
38
  end
39
39
  end
@@ -15,18 +15,18 @@ 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)
22
- git['path'] = new_path
22
+ git["path"] = new_path
23
23
  end
24
24
 
25
25
  def workspace_gid
26
26
  @workspace_gid ||= begin
27
- current = git_global['workspaceGid']
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)
@@ -54,58 +58,70 @@ module Abt
54
58
  end
55
59
 
56
60
  def access_token
57
- return git_global['accessToken'] unless git_global['accessToken'].nil?
61
+ return git_global["accessToken"] unless git_global["accessToken"].nil?
58
62
 
59
- git_global['accessToken'] = cli.prompt.text([
60
- 'Please provide your personal access token for Asana.',
61
- 'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
62
- '',
63
- 'Enter access token'
63
+ git_global["accessToken"] = cli.prompt.text([
64
+ "Please provide your personal access token for Asana.",
65
+ "If you don't have one, create one here: https://app.asana.com/0/developer-console",
66
+ "",
67
+ "Enter access token"
64
68
  ].join("\n"))
65
69
  end
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
- @git ||= GitConfig.new('local', 'abt.asana')
78
+ @git ||= GitConfig.new("local", "abt.asana")
71
79
  end
72
80
 
73
81
  def git_global
74
- @git_global ||= GitConfig.new('global', 'abt.asana')
82
+ @git_global ||= GitConfig.new("global", "abt.asana")
75
83
  end
76
84
 
77
85
  def prompt_finalized_section
78
86
  section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
79
- git['finalizedSectionGid'] = section['gid']
87
+ git["finalizedSectionGid"] = section["gid"]
80
88
  section
81
89
  end
82
90
 
83
91
  def prompt_wip_section
84
- section = prompt_section('Select WIP (Work In Progress) section')
85
- git['wipSectionGid'] = section['gid']
92
+ section = prompt_section("Select WIP (Work In Progress) section")
93
+ git["wipSectionGid"] = section["gid"]
86
94
  section
87
95
  end
88
96
 
89
97
  def prompt_section(message)
90
- cli.warn 'Fetching sections...'
91
- sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: 'name')
98
+ cli.warn("Fetching sections...")
99
+ sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: "name")
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
- cli.warn "Selected Asana workspace: #{workspace['name']}"
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
- git_global['workspaceGid'] = workspace['gid']
108
- workspace
113
+ git_global["workspaceGid"] = workspace["gid"]
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
@@ -4,15 +4,15 @@ module Abt
4
4
  module Providers
5
5
  module Asana
6
6
  class Path < String
7
- PATH_REGEX = %r{^(?<project_gid>\d+)?(/(?<task_gid>\d+))?$}.freeze
7
+ PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
8
8
 
9
- def self.from_ids(project_gid = nil, task_gid = nil)
10
- path = project_gid ? [project_gid, *task_gid].join('/') : ''
11
- new path
9
+ def self.from_ids(project_gid: nil, task_gid: nil)
10
+ path = project_gid ? [project_gid, *task_gid].join("/") : ""
11
+ new(path)
12
12
  end
13
13
 
14
- def initialize(path = '')
15
- raise Abt::Cli::Abort, "Invalid path: #{path}" unless path =~ PATH_REGEX
14
+ def initialize(path = "")
15
+ raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
16
16
 
17
17
  super
18
18
  end