abt-cli 0.0.19 → 0.0.24

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 +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