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,75 +6,45 @@ module Abt
6
6
  module Commands
7
7
  class Init < BaseCommand
8
8
  def self.usage
9
- 'abt init harvest'
9
+ "abt init harvest"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Pick Harvest project for current git repository'
13
+ "Pick Harvest project for current git repository"
14
14
  end
15
15
 
16
16
  def perform
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
18
-
17
+ require_local_config!
19
18
  projects # Load projects up front to make it obvious that searches are instant
20
- project = find_search_result
19
+ project = cli.prompt.search("Select a project", searchable_projects)["project"]
21
20
 
22
- config.project_id = project['id']
23
- config.task_id = nil
21
+ config.path = Path.from_ids(project_id: project["id"])
24
22
 
25
23
  print_project(project)
26
24
  end
27
25
 
28
26
  private
29
27
 
30
- def find_search_result
31
- cli.warn 'Select a project'
32
-
33
- loop do
34
- matches = matches_for_string cli.prompt.text('Enter search')
35
- if matches.empty?
36
- warn 'No matches'
37
- next
38
- end
39
-
40
- cli.warn 'Showing the 10 first matches' if matches.size > 10
41
- choice = cli.prompt.choice 'Select a project', matches[0...10], true
42
- break choice['project'] unless choice.nil?
43
- end
44
- end
45
-
46
- def matches_for_string(string)
47
- search_string = sanitize_string(string)
48
-
49
- searchable_projects.select do |project|
50
- sanitize_string(project['name']).include?(search_string)
51
- end
52
- end
53
-
54
- def sanitize_string(string)
55
- string.downcase.gsub(/[^\w]/, '')
56
- end
57
-
58
28
  def searchable_projects
59
29
  @searchable_projects ||= projects.map do |project|
60
30
  {
61
- 'name' => "#{project['client']['name']} > #{project['name']}",
62
- 'project' => project
31
+ "name" => "#{project['client']['name']} > #{project['name']}",
32
+ "project" => project
63
33
  }
64
34
  end
65
35
  end
66
36
 
67
37
  def projects
68
38
  @projects ||= begin
69
- cli.warn 'Fetching projects...'
39
+ warn("Fetching projects...")
70
40
  project_assignments.map do |project_assignment|
71
- project_assignment['project'].merge('client' => project_assignment['client'])
41
+ project_assignment["project"].merge("client" => project_assignment["client"])
72
42
  end
73
43
  end
74
44
  end
75
45
 
76
46
  def project_assignments
77
- @project_assignments ||= api.get_paged('users/me/project_assignments')
47
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
78
48
  end
79
49
  end
80
50
  end
@@ -6,52 +6,55 @@ module Abt
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
8
  def self.usage
9
- 'abt pick harvest[:<project-id>]'
9
+ "abt pick harvest[:<project-id>]"
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
- task = cli.prompt.choice 'Select a task', tasks
26
+ warn(project["name"])
27
+ task = pick_task
28
28
 
29
29
  print_task(project, task)
30
30
 
31
31
  return if flags[:"dry-run"]
32
32
 
33
- config.project_id = project_id # We might have gotten the project ID as a path
34
- config.task_id = task['id']
33
+ config.path = Path.from_ids(project_id: project_id, task_id: task["id"])
35
34
  end
36
35
 
37
36
  private
38
37
 
39
38
  def project
40
- project_assignment['project']
39
+ project_assignment["project"]
40
+ end
41
+
42
+ def pick_task
43
+ cli.prompt.choice("Select a task", tasks)
41
44
  end
42
45
 
43
46
  def tasks
44
- @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
47
+ @tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
45
48
  end
46
49
 
47
50
  def project_assignment
48
51
  @project_assignment ||= begin
49
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
52
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
50
53
  end
51
54
  end
52
55
 
53
56
  def project_assignments
54
- @project_assignments ||= api.get_paged('users/me/project_assignments')
57
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
55
58
  end
56
59
  end
57
60
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class Projects < BaseCommand
8
8
  def self.usage
9
- 'abt projects harvest'
9
+ "abt projects harvest"
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,15 +23,15 @@ module Abt
23
23
 
24
24
  def projects
25
25
  @projects ||= begin
26
- cli.warn 'Fetching projects...'
26
+ warn("Fetching projects...")
27
27
  project_assignments.map do |project_assignment|
28
- project_assignment['project'].merge('client' => project_assignment['client'])
28
+ project_assignment["project"].merge("client" => project_assignment["client"])
29
29
  end
30
30
  end
31
31
  end
32
32
 
33
33
  def project_assignments
34
- @project_assignments ||= api.get_paged('users/me/project_assignments')
34
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
35
35
  end
36
36
  end
37
37
  end
@@ -6,20 +6,18 @@ module Abt
6
6
  module Commands
7
7
  class Share < BaseCommand
8
8
  def self.usage
9
- 'abt share harvest[:<project-id>[/<task-id>]]'
9
+ "abt share harvest[:<project-id>[/<task-id>]]"
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
- if project_id.nil?
18
- cli.warn 'No project selected'
19
- elsif task_id.nil?
20
- cli.print_ari('harvest', project_id)
21
- else
22
- cli.print_ari('harvest', "#{project_id}/#{task_id}")
17
+ if path != ""
18
+ cli.print_ari("harvest", path)
19
+ elsif cli.output.isatty
20
+ warn("No configuration for project. Did you initialize Harvest?")
23
21
  end
24
22
  end
25
23
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'track'
3
+ require_relative "track"
4
4
 
5
5
  module Abt
6
6
  module Providers
@@ -8,11 +8,13 @@ module Abt
8
8
  module Commands
9
9
  class Start < Track
10
10
  def self.usage
11
- 'abt start harvest[:<project-id>/<task-id>] [options]'
11
+ "abt start harvest[:<project-id>/<task-id>] [options]"
12
12
  end
13
13
 
14
14
  def self.description
15
- 'Alias for: `abt track harvest`. Meant to used in combination with other ARIs, e.g. `abt start harvest asana`'
15
+ <<~TXT
16
+ Alias for: `abt track harvest`. Meant to used in combination with other ARIs, e.g. `abt start harvest asana`
17
+ TXT
16
18
  end
17
19
  end
18
20
  end
@@ -6,49 +6,49 @@ module Abt
6
6
  module Commands
7
7
  class Stop < BaseCommand
8
8
  def self.usage
9
- 'abt stop harvest'
9
+ "abt stop harvest"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Stop running harvest tracker'
13
+ "Stop running harvest tracker"
14
14
  end
15
15
 
16
16
  def perform
17
- cli.abort 'No running time entry' if time_entry.nil?
17
+ abort("No running time entry") if time_entry.nil?
18
18
 
19
19
  stop_time_entry
20
20
 
21
- cli.warn 'Harvest time entry stopped'
21
+ warn("Harvest time entry stopped")
22
22
  print_task(project, task)
23
- rescue Abt::HttpError::HttpError => e
24
- cli.warn e
25
- cli.abort 'Unable to stop time entry'
26
23
  end
27
24
 
28
25
  private
29
26
 
30
27
  def stop_time_entry
31
28
  api.patch("time_entries/#{time_entry['id']}/stop")
29
+ rescue Abt::HttpError::HttpError => e
30
+ warn(e)
31
+ abort("Unable to stop time entry")
32
32
  end
33
33
 
34
34
  def project
35
- time_entry['project']
35
+ time_entry["project"]
36
36
  end
37
37
 
38
38
  def task
39
- time_entry['task']
39
+ time_entry["task"]
40
40
  end
41
41
 
42
42
  def time_entry
43
43
  @time_entry ||= begin
44
44
  api.get_paged(
45
- 'time_entries',
45
+ "time_entries",
46
46
  is_running: true,
47
47
  user_id: config.user_id
48
48
  ).first
49
- rescue Abt::HttpError::HttpError => e # rubocop:disable Layout/RescueEnsureAlignment
50
- cli.warn e
51
- cli.abort 'Unable to fetch running time entry'
49
+ rescue Abt::HttpError::HttpError => e
50
+ warn(e)
51
+ abort("Unable to fetch running time entry")
52
52
  end
53
53
  end
54
54
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class Tasks < BaseCommand
8
8
  def self.usage
9
- 'abt tasks harvest'
9
+ "abt tasks harvest"
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
@@ -24,21 +24,24 @@ module Abt
24
24
  private
25
25
 
26
26
  def project
27
- project_assignment['project']
27
+ project_assignment["project"]
28
28
  end
29
29
 
30
30
  def tasks
31
- @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
31
+ @tasks ||= begin
32
+ warn("Fetching tasks...")
33
+ project_assignment["task_assignments"].map { |ta| ta["task"] }
34
+ end
32
35
  end
33
36
 
34
37
  def project_assignment
35
38
  @project_assignment ||= begin
36
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
39
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
37
40
  end
38
41
  end
39
42
 
40
43
  def project_assignments
41
- @project_assignments ||= api.get_paged('users/me/project_assignments')
44
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
42
45
  end
43
46
  end
44
47
  end
@@ -6,30 +6,33 @@ module Abt
6
6
  module Commands
7
7
  class Track < BaseCommand
8
8
  def self.usage
9
- 'abt track harvest[:<project-id>/<task-id>] [options]'
9
+ "abt track harvest[:<project-id>/<task-id>] [options]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Start tracker for current or specified task. Add a relevant ARI to link the time entry, e.g. `abt track harvest asana`'
13
+ <<~TXT
14
+ Start tracker for current or specified task. Add a relevant ARI to link the time entry, e.g. `abt track harvest asana`
15
+ TXT
14
16
  end
15
17
 
16
18
  def self.flags
17
19
  [
18
- ['-s', '--set', 'Set specified task as current'],
19
- ['-c', '--comment COMMENT', 'Override comment'],
20
- ['-t', '--time HOURS', 'Set hours. Creates a stopped entry unless used with --running'],
21
- ['-r', '--running', 'Used with --time, starts the created time entry']
20
+ ["-s", "--set", "Set specified task as current"],
21
+ ["-c", "--comment COMMENT", "Override comment"],
22
+ ["-t", "--time HOURS",
23
+ "Set hours. Creates a stopped entry unless used with --running"],
24
+ ["-r", "--running", "Used with --time, starts the created time entry"]
22
25
  ]
23
26
  end
24
27
 
25
28
  def perform
26
29
  require_task!
27
30
 
28
- print_task(created_time_entry['project'], created_time_entry['task'])
31
+ print_task(created_time_entry["project"], created_time_entry["task"])
29
32
 
30
33
  maybe_override_current_task
31
34
  rescue Abt::HttpError::HttpError => _e
32
- cli.abort 'Invalid task'
35
+ abort("Invalid task")
33
36
  end
34
37
 
35
38
  private
@@ -39,69 +42,88 @@ module Abt
39
42
  end
40
43
 
41
44
  def create_time_entry
42
- body = time_entry_base_data
43
- body.merge!(hours: flags[:time]) if flags.key? :time
45
+ body = time_entry_data
44
46
 
45
- result = api.post('time_entries', Oj.dump(body, mode: :json))
47
+ result = api.post("time_entries", Oj.dump(body, mode: :json))
46
48
 
47
- if flags.key?(:time) && flags[:running]
48
- api.patch("time_entries/#{result['id']}/restart")
49
- end
49
+ api.patch("time_entries/#{result['id']}/restart") if flags.key?(:time) && flags[:running]
50
50
 
51
51
  result
52
52
  end
53
53
 
54
+ def time_entry_data
55
+ body = time_entry_base_data
56
+
57
+ maybe_add_external_link(body)
58
+ maybe_add_comment(body)
59
+ maybe_add_time(body)
60
+
61
+ body
62
+ end
63
+
54
64
  def time_entry_base_data
55
- body = {
65
+ {
56
66
  project_id: project_id,
57
67
  task_id: task_id,
58
68
  user_id: config.user_id,
59
69
  spent_date: Date.today.iso8601
60
70
  }
71
+ end
61
72
 
73
+ def maybe_add_external_link(body)
62
74
  if external_link_data
63
- cli.warn <<~TXT
75
+ warn(<<~TXT)
64
76
  Linking to:
65
- #{external_link_data[:notes]}
66
- #{external_link_data[:external_reference][:permalink]}
77
+ #{external_link_data[:notes]}
78
+ #{external_link_data[:external_reference][:permalink]}
67
79
  TXT
68
- body.merge! external_link_data
80
+ body.merge!(external_link_data)
69
81
  else
70
- cli.warn 'No external link provided'
82
+ warn("No external link provided")
71
83
  end
84
+ end
72
85
 
86
+ def maybe_add_comment(body)
73
87
  body[:notes] = flags[:comment] if flags.key?(:comment)
74
- body[:notes] ||= cli.prompt.text('Fill in comment (optional)')
75
- body
88
+ body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
76
89
  end
77
90
 
78
- def external_link_data
79
- @external_link_data ||= begin
80
- input = StringIO.new(cli.aris.to_s)
81
- output = StringIO.new
82
- Abt::Cli.new(argv: ['harvest-time-entry-data'], output: output, input: input).perform
91
+ def maybe_add_time(body)
92
+ body[:hours] = flags[:time] if flags.key?(:time)
93
+ end
83
94
 
84
- lines = output.string.strip.lines
95
+ def external_link_data
96
+ return @external_link_data if instance_variable_defined?(:@external_link_data)
85
97
 
86
- return if lines.empty?
98
+ lines = fetch_link_data_lines
87
99
 
88
- # TODO: Make user choose which reference to use by printing the urls
89
- if lines.length > 1
90
- cli.abort('Got reference data from multiple scheme providers, only one is supported at a time')
91
- end
100
+ return @external_link_data = nil if lines.empty?
92
101
 
93
- Oj.load(lines.first, symbol_keys: true)
102
+ if lines.length > 1
103
+ abort("Got reference data from multiple scheme providers, only one is supported at a time")
94
104
  end
105
+
106
+ @external_link_data = Oj.load(lines.first, symbol_keys: true)
107
+ end
108
+
109
+ def fetch_link_data_lines
110
+ other_aris = cli.aris - [ari]
111
+ return [] if other_aris.empty?
112
+
113
+ input = StringIO.new(other_aris.to_s)
114
+ output = StringIO.new
115
+ Abt::Cli.new(argv: ["harvest-time-entry-data"], output: output, input: input).perform
116
+
117
+ output.string.strip.lines
95
118
  end
96
119
 
97
120
  def maybe_override_current_task
98
121
  return unless flags[:set]
99
- return if same_args_as_config?
122
+ return if path == config.path
100
123
  return unless config.local_available?
101
124
 
102
- input = StringIO.new("harvest:#{project_id}/#{task_id}")
103
- output = StringIO.new
104
- Abt::Cli.new(argv: ['current'], output: output, input: input).perform
125
+ config.path = path
126
+ warn("Current task updated")
105
127
  end
106
128
  end
107
129
  end