abt-cli 0.0.26 → 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
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)