abt-cli 0.0.20 → 0.0.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) 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 +2 -2
  5. data/lib/abt/ari_list.rb +1 -1
  6. data/lib/abt/base_command.rb +7 -7
  7. data/lib/abt/cli.rb +49 -47
  8. data/lib/abt/cli/arguments_parser.rb +6 -3
  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 +71 -56
  17. data/lib/abt/docs.rb +48 -26
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +10 -7
  20. data/lib/abt/git_config.rb +4 -6
  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 +12 -10
  24. data/lib/abt/providers/asana/commands/add.rb +13 -12
  25. data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +7 -8
  27. data/lib/abt/providers/asana/commands/current.rb +14 -15
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -14
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +22 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +7 -5
  34. data/lib/abt/providers/asana/commands/start.rb +28 -21
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -6
  36. data/lib/abt/providers/asana/configuration.rb +37 -29
  37. data/lib/abt/providers/asana/path.rb +6 -6
  38. data/lib/abt/providers/devops/api.rb +12 -12
  39. data/lib/abt/providers/devops/base_command.rb +14 -10
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
  42. data/lib/abt/providers/devops/commands/clear.rb +7 -8
  43. data/lib/abt/providers/devops/commands/current.rb +17 -18
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
  45. data/lib/abt/providers/devops/commands/init.rb +21 -14
  46. data/lib/abt/providers/devops/commands/pick.rb +37 -19
  47. data/lib/abt/providers/devops/commands/share.rb +7 -5
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +15 -15
  50. data/lib/abt/providers/devops/path.rb +7 -6
  51. data/lib/abt/providers/git/commands/branch.rb +23 -21
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +10 -8
  54. data/lib/abt/providers/harvest/commands/clear.rb +7 -8
  55. data/lib/abt/providers/harvest/commands/current.rb +13 -14
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -11
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +7 -5
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +12 -12
  62. data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
  63. data/lib/abt/providers/harvest/commands/track.rb +52 -37
  64. data/lib/abt/providers/harvest/configuration.rb +18 -18
  65. data/lib/abt/providers/harvest/path.rb +6 -6
  66. data/lib/abt/version.rb +1 -1
  67. metadata +12 -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,17 +6,19 @@ 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
- require_project!
18
-
19
- cli.print_ari('asana', path)
17
+ if path != ""
18
+ cli.print_ari("asana", path)
19
+ elsif cli.output.isatty
20
+ warn("No configuration for project. Did you initialize Asana?")
21
+ end
20
22
  end
21
23
  end
22
24
  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"] || "")
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,13 @@ 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"] || prompt_wip_section["gid"]
40
40
  end
41
41
 
42
42
  def finalized_section_gid
43
43
  return nil unless local_available?
44
44
 
45
- @finalized_section_gid ||= git['finalizedSectionGid'] || prompt_finalized_section['gid']
45
+ @finalized_section_gid ||= git["finalizedSectionGid"] || prompt_finalized_section["gid"]
46
46
  end
47
47
 
48
48
  def clear_local(verbose: true)
@@ -54,58 +54,66 @@ module Abt
54
54
  end
55
55
 
56
56
  def access_token
57
- return git_global['accessToken'] unless git_global['accessToken'].nil?
57
+ return git_global["accessToken"] unless git_global["accessToken"].nil?
58
58
 
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'
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"
64
64
  ].join("\n"))
65
65
  end
66
66
 
67
67
  private
68
68
 
69
69
  def git
70
- @git ||= GitConfig.new('local', 'abt.asana')
70
+ @git ||= GitConfig.new("local", "abt.asana")
71
71
  end
72
72
 
73
73
  def git_global
74
- @git_global ||= GitConfig.new('global', 'abt.asana')
74
+ @git_global ||= GitConfig.new("global", "abt.asana")
75
75
  end
76
76
 
77
77
  def prompt_finalized_section
78
78
  section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
79
- git['finalizedSectionGid'] = section['gid']
79
+ git["finalizedSectionGid"] = section["gid"]
80
80
  section
81
81
  end
82
82
 
83
83
  def prompt_wip_section
84
- section = prompt_section('Select WIP (Work In Progress) section')
85
- git['wipSectionGid'] = section['gid']
84
+ section = prompt_section("Select WIP (Work In Progress) section")
85
+ git["wipSectionGid"] = section["gid"]
86
86
  section
87
87
  end
88
88
 
89
89
  def prompt_section(message)
90
- cli.warn 'Fetching sections...'
91
- sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: 'name')
90
+ cli.warn("Fetching sections...")
91
+ sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: "name")
92
92
  cli.prompt.choice(message, sections)
93
93
  end
94
94
 
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?
95
+ def prompt_workspace_gid
96
+ cli.abort("Your asana access token does not have access to any workspaces") if workspaces.empty?
97
+
98
+ if workspaces.one?
101
99
  workspace = workspaces.first
102
- cli.warn "Selected Asana workspace: #{workspace['name']}"
100
+ cli.warn("Selected Asana workspace: #{workspace['name']}")
103
101
  else
104
- workspace = cli.prompt.choice('Select Asana workspace', workspaces)
102
+ workspace = pick_workspace
105
103
  end
106
104
 
107
- git_global['workspaceGid'] = workspace['gid']
108
- workspace
105
+ git_global["workspaceGid"] = workspace["gid"]
106
+ end
107
+
108
+ def pick_workspace
109
+ cli.prompt.choice("Select Asana workspace", workspaces)
110
+ end
111
+
112
+ def workspaces
113
+ @workspaces ||= begin
114
+ cli.warn("Fetching workspaces...")
115
+ api.get_paged("workspaces", opt_fields: "name")
116
+ end
109
117
  end
110
118
 
111
119
  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