abt-cli 0.0.26 → 0.0.27

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/abt/docs.rb +10 -6
  3. data/lib/abt/providers/asana.rb +1 -0
  4. data/lib/abt/providers/asana/base_command.rb +33 -3
  5. data/lib/abt/providers/asana/commands/add.rb +0 -4
  6. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  7. data/lib/abt/providers/asana/commands/current.rb +0 -18
  8. data/lib/abt/providers/asana/commands/pick.rb +11 -41
  9. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  10. data/lib/abt/providers/asana/path.rb +1 -1
  11. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  12. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  13. data/lib/abt/providers/devops.rb +1 -0
  14. data/lib/abt/providers/devops/api.rb +10 -0
  15. data/lib/abt/providers/devops/base_command.rb +34 -14
  16. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  17. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  18. data/lib/abt/providers/devops/commands/current.rb +0 -19
  19. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
  20. data/lib/abt/providers/devops/commands/pick.rb +14 -53
  21. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  22. data/lib/abt/providers/devops/path.rb +2 -2
  23. data/lib/abt/providers/devops/services/board_picker.rb +54 -0
  24. data/lib/abt/providers/devops/services/project_picker.rb +79 -0
  25. data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
  26. data/lib/abt/providers/harvest.rb +1 -0
  27. data/lib/abt/providers/harvest/base_command.rb +45 -3
  28. data/lib/abt/providers/harvest/commands/current.rb +0 -28
  29. data/lib/abt/providers/harvest/commands/pick.rb +12 -27
  30. data/lib/abt/providers/harvest/commands/projects.rb +0 -5
  31. data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
  32. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  33. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  34. data/lib/abt/version.rb +1 -1
  35. metadata +9 -5
  36. data/lib/abt/providers/asana/commands/init.rb +0 -42
  37. data/lib/abt/providers/devops/commands/init.rb +0 -79
  38. data/lib/abt/providers/harvest/commands/init.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed527a89749355f223b94daa60d63b91c78ae98ab6976c3470ec6ca42cbcff27
4
- data.tar.gz: aed24bd58e30bfbbdc574532eb97044cd8e108851c2586ac407c0eb42a2deb28
3
+ metadata.gz: 0bffe51f05670b4fcbf1bf2a023c354e26a387b0a74fd34751ef180b1f619c62
4
+ data.tar.gz: 7e5c476b8e1b803adfbb92590676c883e66a6ac74b5111e83aed9f4d6b9feb8a
5
5
  SHA512:
6
- metadata.gz: 6d615f241ac406df5f89cc79ad36f19b0554602f6eb3e20e2f01ba0143595ea6e748c727ccedfeecd1a826c46e031acc7160a3b0ee7462027157a357d730de35
7
- data.tar.gz: 2b9e500f77e181f4198ac17939fc1753f9a7105682783b005d780ec4da61c8af43aa3e986bba34e1df4f5173bde47bd0e97e042f293edb713a364d2a9c1c71b6
6
+ metadata.gz: 9dba1d7966f28966e5fe8d33fd3b380e823f707536b86e1f21a82bfdb74e20ba4d76b2c08e1fb9da9b7f1dc1d74ea6a787979b6ae86ca59b3ce9305964f7a063
7
+ data.tar.gz: 33b61911f3018b0e51ece32264c98f8327af60fcfb94bb59e4d210171bf8562fb885c120be565f79a827dd7f35c72d4d91acf48aa8497079107e120d8e4535bf
data/lib/abt/docs.rb CHANGED
@@ -10,11 +10,10 @@ module Abt
10
10
  def basic_examples
11
11
  {
12
12
  "Getting started:" => {
13
- "abt init asana harvest" => "Setup asana and harvest project for local git repo",
14
13
  "abt pick harvest" => "Pick harvest task. This will likely stay the same throughout the project",
15
14
  "abt pick asana | abt start harvest" => "Pick asana task and start tracking time",
16
15
  "abt stop harvest" => "Stop time tracker",
17
- "abt start asana harvest" => "Continue working, e.g., after a break",
16
+ "abt track asana harvest" => "Continue tracking time, e.g., after a break",
18
17
  "abt finalize asana" => "Finalize the selected asana task"
19
18
  }
20
19
  }
@@ -31,10 +30,15 @@ module Abt
31
30
  "abt tasks asana | grep -i <name of task> | abt start" => nil
32
31
  },
33
32
  "Sharing ARIs:" => {
34
- 'abt share asana harvest | tr "\n" " "' => "Print current asana and harvest ARIs on a single line",
35
- 'abt share asana harvest | tr "\n" " " | pbcopy' => "Copy ARIs to clipboard (mac only)",
36
- "abt start <ARIs from coworker>" => "Work on a task your coworker shared with you",
37
- "abt current <ARIs from coworker> | abt start" => "Set task as current, then start it"
33
+ "abt share" => "Print current asana and harvest ARIs on a single line",
34
+ "abt share | pbcopy" => "Copy ARIs to clipboard (mac only)",
35
+ "abt track <ARIs from coworker>" => "Start tracking on the task your coworker shared with you",
36
+ "abt current <ARIs from coworker> | abt track" => "Set task as current, then start tracking"
37
+ },
38
+ "One-off tracking on any project": {
39
+ "abt pick asana -dc -- harvest -dc | abt track" =>
40
+ "Find a track any task on any project, without reusing/affecting previous settings",
41
+ "abt pick asana harvest | abt track" => "Can be used instead of the above when outside a git repo"
38
42
  },
39
43
  "Flags:" => {
40
44
  'abt start harvest -c "comment"' => "Add command flags after ARIs",
@@ -2,6 +2,7 @@
2
2
 
3
3
  Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each { |file| require file }
4
4
  Dir.glob("#{File.expand_path(__dir__)}/asana/commands/*.rb").sort.each { |file| require file }
5
+ Dir.glob("#{File.expand_path(__dir__)}/asana/services/*.rb").sort.each { |file| require file }
5
6
 
6
7
  module Abt
7
8
  module Providers
@@ -25,12 +25,42 @@ module Abt
25
25
  end
26
26
 
27
27
  def require_project!
28
- abort("No current/specified project. Did you initialize Asana?") if project_gid.nil?
28
+ abort("No current/specified project. Did you forget to run `pick`?") if project_gid.nil?
29
29
  end
30
30
 
31
31
  def require_task!
32
- abort("No current/specified project. Did you initialize Asana and pick a task?") if project_gid.nil?
33
- abort("No current/specified task. Did you pick an Asana task?") if task_gid.nil?
32
+ require_project!
33
+ abort("No current/specified task. Did you forget to run `pick`?") if task_gid.nil?
34
+ end
35
+
36
+ def prompt_project!
37
+ result = Services::ProjectPicker.call(cli: cli, config: config)
38
+ @path = result.path
39
+ @project = result.project
40
+ end
41
+
42
+ def prompt_task!
43
+ result = Services::TaskPicker.call(cli: cli, path: path, config: config, project: project)
44
+ @path = result.path
45
+ @task = result.task
46
+ end
47
+
48
+ def task
49
+ @task ||= begin
50
+ warn("Fetching task...")
51
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
52
+ rescue Abt::HttpError::NotFoundError
53
+ nil
54
+ end
55
+ end
56
+
57
+ def project
58
+ @project ||= begin
59
+ warn("Fetching project...")
60
+ api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
61
+ rescue Abt::HttpError::NotFoundError
62
+ nil
63
+ end
34
64
  end
35
65
 
36
66
  def print_project(project)
@@ -56,10 +56,6 @@ module Abt
56
56
  @notes ||= cli.prompt.text("Enter task notes")
57
57
  end
58
58
 
59
- def project
60
- @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
61
- end
62
-
63
59
  def section
64
60
  @section ||= cli.prompt.choice("Add to section?", sections,
65
61
  nil_option: ["q", "Don't add to section"])
@@ -28,19 +28,6 @@ module Abt
28
28
 
29
29
  def ensure_current_is_valid!
30
30
  abort("Invalid task gid: #{task_gid}") if task.nil?
31
-
32
- return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
33
-
34
- abort("Invalid or unmatching project gid: #{project_gid}")
35
- end
36
-
37
- def task
38
- @task ||= begin
39
- warn("Fetching task...")
40
- api.get("tasks/#{task_gid}", opt_fields: "name,memberships.project")
41
- rescue Abt::HttpError::NotFoundError
42
- nil
43
- end
44
31
  end
45
32
  end
46
33
  end
@@ -36,24 +36,6 @@ module Abt
36
36
  abort("Invalid project: #{project_gid}") if project.nil?
37
37
  abort("Invalid task: #{task_gid}") if task_gid && task.nil?
38
38
  end
39
-
40
- def project
41
- @project ||= begin
42
- warn("Fetching project...")
43
- api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
44
- rescue Abt::HttpError::NotFoundError
45
- nil
46
- end
47
- end
48
-
49
- def task
50
- @task ||= begin
51
- warn("Fetching task...")
52
- api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
53
- rescue Abt::HttpError::NotFoundError
54
- nil
55
- end
56
- end
57
39
  end
58
40
  end
59
41
  end
@@ -10,65 +10,35 @@ module Abt
10
10
  end
11
11
 
12
12
  def self.description
13
- "Pick task for current git repository"
13
+ "Pick a task and - unless told not to - make it current"
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
+ ["-c", "--clean", "Don't reuse project configuration"]
19
20
  ]
20
21
  end
21
22
 
22
23
  def perform
23
- require_local_config!
24
- require_project!
25
-
26
- warn(project["name"])
27
- task = select_task
24
+ pick!
28
25
 
29
26
  print_task(project, task)
30
27
 
31
28
  return if flags[:"dry-run"]
32
29
 
33
- config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
34
- end
35
-
36
- private
37
-
38
- def project
39
- @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
40
- end
41
-
42
- def select_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
30
+ if config.local_available?
31
+ config.path = Path.from_gids(project_gid: project["gid"], task_gid: task["gid"])
50
32
  else
51
- cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
33
+ warn("No local configuration to update - will function as dry run")
52
34
  end
53
35
  end
54
36
 
55
- def tasks_in_section(section)
56
- tasks = api.get_paged(
57
- "tasks",
58
- section: section["gid"],
59
- opt_fields: "name,completed,permalink_url"
60
- )
61
-
62
- # The below filtering is the best we can do with Asanas api, see this:
63
- # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
64
- tasks.reject { |task| task["completed"] }
65
- end
37
+ private
66
38
 
67
- def sections
68
- @sections ||= begin
69
- warn("Fetching sections...")
70
- api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
71
- end
39
+ def pick!
40
+ prompt_project! if project_gid.nil? || flags[:clean]
41
+ prompt_task!
72
42
  end
73
43
  end
74
44
  end
@@ -14,7 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- require_project!
17
+ prompt_project! unless project_gid
18
18
 
19
19
  tasks.each do |task|
20
20
  print_task(project, task)
@@ -23,14 +23,9 @@ module Abt
23
23
 
24
24
  private
25
25
 
26
- def project
27
- @project ||= begin
28
- api.get("projects/#{project_gid}", opt_fields: "name")
29
- end
30
- end
31
-
32
26
  def tasks
33
27
  @tasks ||= begin
28
+ project
34
29
  warn("Fetching tasks...")
35
30
  tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
36
31
  tasks.reject { |task| task["completed"] }
@@ -6,7 +6,7 @@ module Abt
6
6
  class Path < String
7
7
  PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
8
8
 
9
- def self.from_ids(project_gid: nil, task_gid: nil)
9
+ def self.from_gids(project_gid: nil, task_gid: nil)
10
10
  path = project_gid ? [project_gid, *task_gid].join("/") : ""
11
11
  new(path)
12
12
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Services
7
+ class ProjectPicker
8
+ class Result
9
+ attr_reader :project, :path
10
+
11
+ def initialize(project:, path:)
12
+ @project = project
13
+ @path = path
14
+ end
15
+ end
16
+
17
+ def self.call(**args)
18
+ new(**args).call
19
+ end
20
+
21
+ attr_reader :cli, :config
22
+
23
+ def initialize(cli:, config:)
24
+ @cli = cli
25
+ @config = config
26
+ end
27
+
28
+ def call
29
+ project = cli.prompt.search("Select a project", projects)
30
+ path = Path.from_gids(project_gid: project["gid"])
31
+
32
+ Result.new(project: project, path: path)
33
+ end
34
+
35
+ private
36
+
37
+ def projects
38
+ @projects ||= begin
39
+ cli.warn("Fetching projects...")
40
+ api.get_paged("projects",
41
+ workspace: config.workspace_gid,
42
+ archived: false,
43
+ opt_fields: "name,permalink_url")
44
+ end
45
+ end
46
+
47
+ def api
48
+ Abt::Providers::Asana::Api.new(access_token: config.access_token)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Services
7
+ class TaskPicker
8
+ class Result
9
+ attr_reader :task, :path
10
+
11
+ def initialize(task:, path:)
12
+ @task = task
13
+ @path = path
14
+ end
15
+ end
16
+
17
+ def self.call(**args)
18
+ new(**args).call
19
+ end
20
+
21
+ attr_reader :cli, :path, :config, :project
22
+
23
+ def initialize(cli:, path:, config:, project:)
24
+ @cli = cli
25
+ @path = path
26
+ @config = config
27
+ @project = project
28
+ end
29
+
30
+ def call
31
+ task = select_task
32
+
33
+ path_with_task = Path.new([path, task["gid"]].join("/"))
34
+
35
+ Result.new(task: task, path: path_with_task)
36
+ end
37
+
38
+ private
39
+
40
+ def select_task
41
+ section = prompt_section
42
+ tasks = tasks_in_section(section)
43
+
44
+ if tasks.length.zero?
45
+ cli.warn("Section is empty")
46
+ select_task
47
+ else
48
+ cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
49
+ end
50
+ end
51
+
52
+ def prompt_section
53
+ cli.prompt.choice("Which section in #{project['name']}?", sections)
54
+ end
55
+
56
+ def tasks_in_section(section)
57
+ cli.warn("Fetching tasks...")
58
+ tasks = api.get_paged(
59
+ "tasks",
60
+ section: section["gid"],
61
+ opt_fields: "name,completed,permalink_url"
62
+ )
63
+
64
+ # The below filtering is the best we can do with Asanas api, see this:
65
+ # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
66
+ tasks.reject { |task| task["completed"] }
67
+ end
68
+
69
+ def sections
70
+ @sections ||= begin
71
+ cli.warn("Fetching sections...")
72
+ api.get_paged("projects/#{project['gid']}/sections", opt_fields: "name")
73
+ end
74
+ end
75
+
76
+ def api
77
+ Abt::Providers::Asana::Api.new(access_token: config.access_token)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
4
4
  Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
5
+ Dir.glob("#{File.expand_path(__dir__)}/devops/services/*.rb").sort.each { |file| require file }
5
6
 
6
7
  module Abt
7
8
  module Providers
@@ -73,6 +73,16 @@ module Abt
73
73
  "#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
74
74
  end
75
75
 
76
+ def sanitize_work_item(work_item)
77
+ return nil if work_item.nil?
78
+
79
+ work_item.merge(
80
+ "id" => work_item["id"].to_s,
81
+ "name" => work_item["fields"]["System.Title"],
82
+ "url" => url_for_work_item(work_item)
83
+ )
84
+ end
85
+
76
86
  def connection
77
87
  @connection ||= Faraday.new(api_endpoint) do |connection|
78
88
  connection.basic_auth(username, access_token)
@@ -24,29 +24,49 @@ module Abt
24
24
  end
25
25
 
26
26
  def require_board!
27
- return if organization_name && project_name && board_id
27
+ return if board_id && organization_name && project_name
28
28
 
29
- abort("No current/specified board. Did you initialize DevOps?")
29
+ abort("No current/specified board. Did you forget to `pick`?")
30
30
  end
31
31
 
32
32
  def require_work_item!
33
- unless organization_name && project_name && board_id
34
- abort("No current/specified board. Did you initialize DevOps and pick a work item?")
35
- end
36
-
33
+ require_board!
37
34
  return if work_item_id
38
35
 
39
- abort("No current/specified work item. Did you pick a DevOps work item?")
36
+ abort("No current/specified work item. Did you forget to `pick`?")
37
+ end
38
+
39
+ def prompt_project!
40
+ @path = Services::ProjectPicker.call(cli: cli).path
40
41
  end
41
42
 
42
- def sanitize_work_item(work_item)
43
- return nil if work_item.nil?
43
+ def prompt_board!
44
+ result = Services::BoardPicker.call(cli: cli, path: path, config: config)
45
+ @path = result.path
46
+ @board = result.board
47
+ end
44
48
 
45
- work_item.merge(
46
- "id" => work_item["id"].to_s,
47
- "name" => work_item["fields"]["System.Title"],
48
- "url" => api.url_for_work_item(work_item)
49
- )
49
+ def prompt_work_item!
50
+ result = Services::WorkItemPicker.call(cli: cli, path: path, config: config, board: board)
51
+ @path = result.path
52
+ @work_item = result.work_item
53
+ end
54
+
55
+ def board
56
+ @board ||= begin
57
+ api.get("work/boards/#{board_id}")
58
+ rescue HttpError::NotFoundError
59
+ nil
60
+ end
61
+ end
62
+
63
+ def work_item
64
+ @work_item ||= begin
65
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
66
+ api.sanitize_work_item(work_item)
67
+ rescue HttpError::NotFoundError
68
+ nil
69
+ end
50
70
  end
51
71
 
52
72
  def print_board(organization_name, project_name, board)