abt-cli 0.0.19 → 0.0.24

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 (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 +64 -52
  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 -18
  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 +25 -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,74 +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
- 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.path = Path.from_ids(project['id'])
21
+ config.path = Path.from_ids(project_id: project["id"])
23
22
 
24
23
  print_project(project)
25
24
  end
26
25
 
27
26
  private
28
27
 
29
- def find_search_result
30
- warn 'Select a project'
31
-
32
- loop do
33
- matches = matches_for_string cli.prompt.text('Enter search')
34
- if matches.empty?
35
- warn 'No matches'
36
- next
37
- end
38
-
39
- warn 'Showing the 10 first matches' if matches.size > 10
40
- choice = cli.prompt.choice 'Select a project', matches[0...10], true
41
- break choice['project'] unless choice.nil?
42
- end
43
- end
44
-
45
- def matches_for_string(string)
46
- search_string = sanitize_string(string)
47
-
48
- searchable_projects.select do |project|
49
- sanitize_string(project['name']).include?(search_string)
50
- end
51
- end
52
-
53
- def sanitize_string(string)
54
- string.downcase.gsub(/[^\w]/, '')
55
- end
56
-
57
28
  def searchable_projects
58
29
  @searchable_projects ||= projects.map do |project|
59
30
  {
60
- 'name' => "#{project['client']['name']} > #{project['name']}",
61
- 'project' => project
31
+ "name" => "#{project['client']['name']} > #{project['name']}",
32
+ "project" => project
62
33
  }
63
34
  end
64
35
  end
65
36
 
66
37
  def projects
67
38
  @projects ||= begin
68
- warn 'Fetching projects...'
39
+ warn("Fetching projects...")
69
40
  project_assignments.map do |project_assignment|
70
- project_assignment['project'].merge('client' => project_assignment['client'])
41
+ project_assignment["project"].merge("client" => project_assignment["client"])
71
42
  end
72
43
  end
73
44
  end
74
45
 
75
46
  def project_assignments
76
- @project_assignments ||= api.get_paged('users/me/project_assignments')
47
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
77
48
  end
78
49
  end
79
50
  end
@@ -6,51 +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
- 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
- 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.path = Path.from_ids(project_id, task['id'])
33
+ config.path = Path.from_ids(project_id: project_id, task_id: task["id"])
34
34
  end
35
35
 
36
36
  private
37
37
 
38
38
  def project
39
- project_assignment['project']
39
+ project_assignment["project"]
40
+ end
41
+
42
+ def pick_task
43
+ cli.prompt.choice("Select a task", tasks)
40
44
  end
41
45
 
42
46
  def tasks
43
- @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
47
+ @tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
44
48
  end
45
49
 
46
50
  def project_assignment
47
51
  @project_assignment ||= begin
48
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
52
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
49
53
  end
50
54
  end
51
55
 
52
56
  def project_assignments
53
- @project_assignments ||= api.get_paged('users/me/project_assignments')
57
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
54
58
  end
55
59
  end
56
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
- 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,17 +6,19 @@ 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 ARI'
13
+ "Print project/task ARI"
14
14
  end
15
15
 
16
16
  def perform
17
- require_project!
18
-
19
- cli.print_ari('harvest', path)
17
+ if path != ""
18
+ cli.print_ari("harvest", path)
19
+ elsif cli.output.isatty
20
+ warn("No configuration for project. Did you initialize Harvest?")
21
+ end
20
22
  end
21
23
  end
22
24
  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,19 +6,19 @@ 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
- 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
- warn 'Harvest time entry stopped'
21
+ warn("Harvest time entry stopped")
22
22
  print_task(project, task)
23
23
  end
24
24
 
@@ -27,28 +27,28 @@ module Abt
27
27
  def stop_time_entry
28
28
  api.patch("time_entries/#{time_entry['id']}/stop")
29
29
  rescue Abt::HttpError::HttpError => e
30
- warn e
31
- abort 'Unable to stop time entry'
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
- warn e
51
- 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,24 +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
31
  @tasks ||= begin
32
- warn 'Fetching tasks...'
33
- project_assignment['task_assignments'].map { |ta| ta['task'] }
32
+ warn("Fetching tasks...")
33
+ project_assignment["task_assignments"].map { |ta| ta["task"] }
34
34
  end
35
35
  end
36
36
 
37
37
  def project_assignment
38
38
  @project_assignment ||= begin
39
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
39
+ project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
40
40
  end
41
41
  end
42
42
 
43
43
  def project_assignments
44
- @project_assignments ||= api.get_paged('users/me/project_assignments')
44
+ @project_assignments ||= api.get_paged("users/me/project_assignments")
45
45
  end
46
46
  end
47
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
- abort 'Invalid task'
35
+ abort("Invalid task")
33
36
  end
34
37
 
35
38
  private
@@ -39,65 +42,77 @@ 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
- 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
- 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)")
89
+ end
90
+
91
+ def maybe_add_time(body)
92
+ body[:hours] = flags[:time] if flags.key?(:time)
76
93
  end
77
94
 
78
95
  def external_link_data
79
- @external_link_data ||= begin
80
- lines = call_harvest_time_entry_data_for_other_aris
81
-
82
- if lines.empty?
83
- nil
84
- else
85
- if lines.length > 1
86
- abort('Got reference data from multiple scheme providers, only one is supported at a time')
87
- end
88
-
89
- Oj.load(lines.first, symbol_keys: true)
90
- end
96
+ return @external_link_data if instance_variable_defined?(:@external_link_data)
97
+
98
+ lines = fetch_link_data_lines
99
+
100
+ return @external_link_data = nil if lines.empty?
101
+
102
+ if lines.length > 1
103
+ abort("Got reference data from multiple scheme providers, only one is supported at a time")
91
104
  end
105
+
106
+ @external_link_data = Oj.load(lines.first, symbol_keys: true)
92
107
  end
93
108
 
94
- def call_harvest_time_entry_data_for_other_aris
109
+ def fetch_link_data_lines
95
110
  other_aris = cli.aris - [ari]
96
111
  return [] if other_aris.empty?
97
112
 
98
113
  input = StringIO.new(other_aris.to_s)
99
114
  output = StringIO.new
100
- Abt::Cli.new(argv: ['harvest-time-entry-data'], output: output, input: input).perform
115
+ Abt::Cli.new(argv: ["harvest-time-entry-data"], output: output, input: input).perform
101
116
 
102
117
  output.string.strip.lines
103
118
  end
@@ -108,7 +123,7 @@ module Abt
108
123
  return unless config.local_available?
109
124
 
110
125
  config.path = path
111
- warn 'Current task updated'
126
+ warn("Current task updated")
112
127
  end
113
128
  end
114
129
  end