linear-cli 0.7.5 → 0.7.7

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -2
  3. data/Readme.adoc +11 -3
  4. data/changelog/0.7.7/added_ability_to_attach_project_to_command.yml +4 -0
  5. data/changelog/0.7.7/added_issue_pr_command.yml +4 -0
  6. data/changelog/0.7.7/added_lcomment_alias_to_add_comments_to_issues.yml +4 -0
  7. data/changelog/0.7.7/tag.yml +1 -0
  8. data/exe/lc +5 -1
  9. data/exe/lclose +1 -0
  10. data/exe/lcls +1 -0
  11. data/exe/lcomment +1 -0
  12. data/exe/lcreate +1 -0
  13. data/exe/linear-cli +8 -1
  14. data/exe/{lc.sh → scripts/lc.sh} +4 -3
  15. data/exe/scripts/lcls.sh +2 -0
  16. data/exe/scripts/lcomment.sh +2 -0
  17. data/lib/linear/api.rb +1 -1
  18. data/lib/linear/cli/caller.rb +6 -1
  19. data/lib/linear/cli/sub_commands.rb +5 -59
  20. data/lib/linear/cli/version.rb +1 -1
  21. data/lib/linear/cli/what_for.rb +143 -0
  22. data/lib/linear/cli.rb +8 -1
  23. data/lib/linear/commands/issue/create.rb +1 -0
  24. data/lib/linear/commands/issue/pr.rb +38 -0
  25. data/lib/linear/commands/issue/update.rb +21 -6
  26. data/lib/linear/commands/issue.rb +54 -21
  27. data/lib/linear/models/base_model/class_methods.rb +129 -0
  28. data/lib/linear/models/base_model/method_magic.rb +23 -0
  29. data/lib/linear/models/base_model.rb +9 -108
  30. data/lib/linear/models/issue/class_methods.rb +44 -0
  31. data/lib/linear/models/issue.rb +23 -38
  32. data/lib/linear/models/project.rb +47 -0
  33. data/lib/linear/models/team.rb +6 -9
  34. data/lib/linear.rb +8 -0
  35. data/linear-cli.gemspec +2 -1
  36. metadata +33 -10
  37. data/exe/lclose +0 -4
  38. data/exe/lcls +0 -4
  39. data/exe/lcls.sh +0 -2
  40. data/exe/lcreate +0 -4
  41. /data/exe/{lclose.sh → scripts/lclose.sh} +0 -0
  42. /data/exe/{lcreate.sh → scripts/lcreate.sh} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33c1343c813451d0c0d39f4d883a61856364dba86459c7fe6c1c784771998431
4
- data.tar.gz: d40871678beca4fb383c3b569d367dd1434c0df81711b046ac9f970cf1bd8366
3
+ metadata.gz: 040dce1ff59ddd000240b8a03105928d775841be91d1fe50f974249e2ddc21fc
4
+ data.tar.gz: 0de33b286614b981e63ad75b14d1728e825d44e6ec2265c625a00888cdb32cf2
5
5
  SHA512:
6
- metadata.gz: 73c3de969e83cfa59685ab2a264bce91c746dc110630a763a3e56f2e2b052fe0a4f0679ad4bba48d4ae23749c75f89db46fc9d45ce704abae793811db3db1000
7
- data.tar.gz: 5e770e36384c62ef81f483cddd926b5cd7423ce71366146310eefb134e0675a4f349cdba22decb473e5bb0ff449be7372f2e66c4c3ce6681e9506bea5dfa6c6c
6
+ metadata.gz: 1f45d0fc3280fda121430d10078e49a5a870753a8fa4788943934a586134101b58c40cacbd8fb3d83bd140d36db70bf19e82a65e3a87770bcf6a6ab48aa8bbbe
7
+ data.tar.gz: 9333ae8a1cb9bab180035dba641c23a5315783bf3d6bd0e790f0b21eb3da0a8ae149c1264ff4619ef75caaff1ec51bf8fdf4cd7c1f36659552d8cb685b7f4332
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.7] - 2024-02-06
6
+ ### Added
7
+ - Added ability to attach project to command (@bougyman)
8
+ - Added issue pr command (@bougyman)
9
+ - Added lcomment alias to add comments to issues (@bougyman)
10
+
5
11
  ## [0.7.5] - 2024-02-05
6
12
  ### Fixed
7
13
  - Fixed problem when choosing from multiple completed states (@bougyman)
@@ -45,8 +51,9 @@
45
51
  ### Added
46
52
  - Added new changelog management system (changelog-rb) (@bougyman)
47
53
 
48
- [Unreleased]: https://github.com/rubyists/linear-cli/compare/0.7.5...HEAD
49
- [0.7.5]: https://github.com/rubyists/linear-cli/compare/v0.7.3...0.7.5
54
+ [Unreleased]: https://github.com/rubyists/linear-cli/compare/0.7.7...HEAD
55
+ [0.7.7]: https://github.com/rubyists/linear-cli/compare/v0.7.5...0.7.7
56
+ [0.7.5]: https://github.com/rubyists/linear-cli/compare/v0.7.3...v0.7.5
50
57
  [0.7.3]: https://github.com/rubyists/linear-cli/compare/v0.7.2...v0.7.3
51
58
  [0.7.2]: https://github.com/rubyists/linear-cli/compare/v0.7.1...v0.7.2
52
59
  [0.7.1]: https://github.com/rubyists/linear-cli/compare/v0.7.0...v0.7.1
data/Readme.adoc CHANGED
@@ -3,6 +3,8 @@
3
3
  :toclevels: 3
4
4
  :sectanchors:
5
5
  :icons: font
6
+ :tip-caption: 💡
7
+ :note-caption: 📝
6
8
  :experimental:
7
9
 
8
10
  A command line interface to https://linear.app.
@@ -104,7 +106,7 @@ $ lc issue take CRY-456 CRY-789
104
106
  [source,sh]
105
107
  ----
106
108
  $ lc i c --title "My new issue" --description "This is a new issue" --labels Bug,Feature --team CRY
107
- $ lc i c -t "My new issue" -T CRY -l Improvment,Feature
109
+ $ lc i c -t "My new issue" -T CRY -l Improvement,Feature
108
110
  ----
109
111
 
110
112
  NOTE: If you don't provide a title, team, labels or description, you will be prompted to enter them.
@@ -120,7 +122,7 @@ This will switch to the branch for the issue, creating the branch if it doesn't
120
122
  $ lc i dev CRY-1234
121
123
  ----
122
124
 
123
- TIP: You may pass the --dev option to the create subcommand to immediately develop the creted issue.
125
+ TIP: You may pass the --dev option to the create subcommand to immediately develop the created issue.
124
126
 
125
127
  ==== Update an issue
126
128
 
@@ -131,8 +133,13 @@ at a time. You can also use the 'u' alias for 'update', and as always, the 'i' a
131
133
 
132
134
  [source,sh]
133
135
  ----
134
- $ lc issue update --comment "Here is a comment" CRY-1234
136
+ $ lc issue update --comment "Here is a comment" CRY-1234 <1>
137
+ $ lc issue update --comment - CRY-14 CRY-15 <2>
138
+ $ lcomment CRY-1234 CRY-3 <3>
135
139
  ----
140
+ <1> This will use the provided comment
141
+ <2> This will prompt for a comment (use '-' to prompt)
142
+ <3> This will always prompt you for a comment ('lcomment' is an alias for 'lc issue update --comment -')
136
143
 
137
144
  ===== Close one or many issues
138
145
 
@@ -150,4 +157,5 @@ Some command aliases are available to make things easier to type.
150
157
  $ lcls
151
158
  $ lcreate --description "This is a new issue" --labels Bug,Feature --team CRY
152
159
  $ lclose --reason "This issue sucks" CRY-1234 CRY-456
160
+ $ lcancel --reason "These should never have been here" --trash CRY-1234 CRY-456
153
161
  ----
@@ -0,0 +1,4 @@
1
+ type: Added
2
+ title: >
3
+ Added ability to attach project to command
4
+ author: bougyman
@@ -0,0 +1,4 @@
1
+ type: Added
2
+ title: >
3
+ Added issue pr command
4
+ author: bougyman
@@ -0,0 +1,4 @@
1
+ type: Added
2
+ title: >
3
+ Added lcomment alias to add comments to issues
4
+ author: bougyman
@@ -0,0 +1 @@
1
+ date: 2024-02-06
data/exe/lc CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- exec File.join(__dir__, 'lc.sh'), *ARGV
4
+ require 'pathname'
5
+ script_dir = Pathname(__dir__).join('scripts')
6
+ basename = File.basename(__FILE__)
7
+ script = script_dir.join(basename).exist? ? script_dir.join(basename) : script_dir.join("#{basename}.sh")
8
+ exec script.to_s, *ARGV
data/exe/lclose ADDED
@@ -0,0 +1 @@
1
+ lc
data/exe/lcls ADDED
@@ -0,0 +1 @@
1
+ lc
data/exe/lcomment ADDED
@@ -0,0 +1 @@
1
+ lc
data/exe/lcreate ADDED
@@ -0,0 +1 @@
1
+ lc
data/exe/linear-cli CHANGED
@@ -3,4 +3,11 @@
3
3
 
4
4
  require 'linear'
5
5
  Rubyists::Linear::L :cli
6
- Dry::CLI.new(Rubyists::Linear::CLI).call
6
+ begin
7
+ Dir.mktmpdir(Process.pid.to_s) do |dir|
8
+ Rubyists::Linear.tmpdir = dir
9
+ Dry::CLI.new(Rubyists::Linear::CLI).call
10
+ end
11
+ ensure
12
+ FileUtils.rm_rf(Rubyists::Linear.tmpdir) if Rubyists::Linear.tmpdir.exist?
13
+ end
@@ -9,9 +9,10 @@ then
9
9
  linear-cli "$@" 2>&1|sed 's/linear-cli/lc/g'
10
10
  exit 0
11
11
  fi
12
- if ! linear-cli "$@"
13
- then
14
- printf "lc: linear-cli failed\n" >&2
12
+ linear-cli "$@"
13
+ result=$?
14
+ if [ $result -gt 1 ]; then
15
+ printf "lc: linear-cli failed %s\n" $result >&2
15
16
  lc "$@" --help 2>&1
16
17
  exit 1
17
18
  fi
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec lc issue list "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec linear-cli issue update --comment - "$@"
data/lib/linear/api.rb CHANGED
@@ -42,7 +42,7 @@ module Rubyists
42
42
 
43
43
  def query(query)
44
44
  call format('{ "query": %s }', query.to_s.to_json)
45
- rescue StandardError => e
45
+ rescue SmellsBad => e
46
46
  logger.error('Error in query', query:, error: e)
47
47
  raise e unless Rubyists::Linear.verbosity > 2
48
48
 
@@ -9,8 +9,13 @@ module Rubyists
9
9
  # Global options for all commands
10
10
  mod.instance_eval do
11
11
  option :output, type: :string, default: 'text', values: %w[text json], desc: 'Output format'
12
- option :debug, type: :integer, default: 0, desc: 'Debug level'
12
+ option :debug,
13
+ type: :integer,
14
+ aliases: ['-D'],
15
+ default: 0,
16
+ desc: 'Debug level (greater than 0 to see backtraces)'
13
17
  end
18
+
14
19
  Caller.class_eval do
15
20
  # Wraps the :call method so the debug option is honored, and we can trace the call
16
21
  # as well as handle any exceptions that are raised
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is where all the _for methods live
4
+ require_relative 'what_for'
5
+
3
6
  module Rubyists
4
7
  module Linear
5
8
  module CLI
6
9
  # The SubCommands module should be included in all commands with subcommands
7
10
  module SubCommands
11
+ include CLI::WhatFor
12
+
8
13
  def self.included(mod)
9
14
  mod.instance_eval do
10
15
  def const_added(const)
@@ -41,65 +46,6 @@ module Rubyists
41
46
  @prompt ||= CLI.prompt
42
47
  end
43
48
 
44
- def team_for(key = nil)
45
- return Rubyists::Linear::Team.find(key) if key
46
-
47
- ask_for_team
48
- end
49
-
50
- def reason_for(reason = nil, four: nil)
51
- return reason if reason
52
-
53
- question = four ? "Reason for #{four}:" : 'Reason:'
54
- prompt.ask(question)
55
- end
56
-
57
- def completed_state_for(thingy)
58
- states = thingy.completed_states
59
- return states.first if states.size == 1
60
-
61
- selection = prompt.select('Choose a completed state', states.to_h { |s| [s.name, s.id] })
62
- Rubyists::Linear::WorkflowState.find selection
63
- end
64
-
65
- def description_for(description = nil)
66
- return description if description
67
-
68
- prompt.multiline('Description:').map(&:chomp).join('\\n')
69
- end
70
-
71
- def title_for(title = nil)
72
- return title if title
73
-
74
- prompt.ask('Title:')
75
- end
76
-
77
- def labels_for(team, labels = nil)
78
- return Rubyists::Linear::Label.find_all_by_name(labels.map(&:strip)) if labels
79
-
80
- prompt.on(:keypress) do |event|
81
- prompt.trigger(:keydown) if event.value == 'j'
82
- prompt.trigger(:keyup) if event.value == 'k'
83
- end
84
- prompt.multi_select('Labels:', team.labels.to_h { |t| [t.name, t] })
85
- end
86
-
87
- def cut_branch!(branch_name)
88
- if current_branch != default_branch
89
- prompt.yes?("You are not on the default branch (#{default_branch}). Do you want to checkout #{default_branch} and create a new branch?") && git.checkout(default_branch) # rubocop:disable Layout/LineLength
90
- end
91
- git.branch(branch_name)
92
- end
93
-
94
- def branch_for(branch_name)
95
- logger.trace('Looking for branch', branch_name:)
96
- existing = git.branches[branch_name]
97
- return cut_branch!(branch_name) unless existing
98
-
99
- logger.trace('Branch found', branch: existing&.name)
100
- existing
101
- end
102
-
103
49
  def current_branch
104
50
  git.current_branch
105
51
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rubyists
4
4
  module Linear
5
- VERSION = '0.7.5'
5
+ VERSION = '0.7.7'
6
6
  end
7
7
  end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyists
4
+ module Linear
5
+ module CLI
6
+ # Module for the _for methods
7
+ module WhatFor
8
+ def editor_for(prefix)
9
+ file = Tempfile.open(prefix, Rubyists::Linear.tmpdir)
10
+ TTY::Editor.open(file.path)
11
+ file.close
12
+ File.readlines(file.path).map(&:chomp).join('\\n')
13
+ ensure
14
+ file&.close
15
+ end
16
+
17
+ def comment_for(issue, comment)
18
+ return comment unless comment.nil? || comment == '-'
19
+
20
+ comment = prompt.ask("Comment for #{issue.identifier} - #{issue.title} (- to open an editor)", default: '-')
21
+ return comment unless comment == '-'
22
+
23
+ editor_for %w[comment .md]
24
+ end
25
+
26
+ def team_for(key = nil)
27
+ return Rubyists::Linear::Team.find(key) if key
28
+
29
+ ask_for_team
30
+ end
31
+
32
+ def reason_for(reason = nil, four: nil)
33
+ return reason if reason && reason != '-'
34
+
35
+ question = four ? "Reason for #{TTY::Markdown.parse(four)}" : 'Reason'
36
+ answer = prompt.ask("#{question} (- to open an editor):", default: '-')
37
+ return answer unless answer == '-'
38
+
39
+ editor_for %w[reason .md]
40
+ end
41
+
42
+ def cancelled_state_for(thingy)
43
+ states = thingy.cancelled_states
44
+ return states.first if states.size == 1
45
+
46
+ selection = prompt.select('Choose a cancelled state', states.to_h { |s| [s.name, s.id] })
47
+ Rubyists::Linear::WorkflowState.find selection
48
+ end
49
+
50
+ def completed_state_for(thingy)
51
+ states = thingy.completed_states
52
+ return states.first if states.size == 1
53
+
54
+ selection = prompt.select('Choose a completed state', states.to_h { |s| [s.name, s.id] })
55
+ Rubyists::Linear::WorkflowState.find selection
56
+ end
57
+
58
+ def description_for(description = nil)
59
+ return description if description
60
+
61
+ prompt.multiline('Description:').map(&:chomp).join('\\n')
62
+ end
63
+
64
+ def title_for(title = nil)
65
+ return title if title
66
+
67
+ prompt.ask('Title:')
68
+ end
69
+
70
+ def ask_for_projects(projects, search: true)
71
+ prompt.warn("No project found matching #{search}.") if search
72
+ return projects.first if projects.size == 1
73
+
74
+ prompt.select('Project:', projects.to_h { |p| [p.name, p] })
75
+ end
76
+
77
+ def project_scores(projects, search_term)
78
+ projects.select { |p| p.match_score?(search_term).positive? }.sort_by { |p| p.match_score?(search_term) }
79
+ end
80
+
81
+ def project_for(team, project = nil)
82
+ projects = team.projects
83
+ return nil if projects.empty?
84
+
85
+ possibles = project_scores(projects, project)
86
+ return ask_for_projects(projects, search: project) if possibles.empty?
87
+
88
+ first = possibles.first
89
+ return first if first.match_score?(project) == 100
90
+
91
+ selections = possibles + (projects - possibles)
92
+ prompt.select('Project:', selections.to_h { |p| [p.name, p] }) if possibles.size.positive?
93
+ end
94
+
95
+ def pr_title_for(issue)
96
+ proposed = [pr_type_for(issue)]
97
+ proposed_scope = pr_scope_for(issue.title)
98
+ proposed << "(#{proposed_scope})" if proposed_scope
99
+ summary = issue.title.sub(/(?:#{ALLOWED_PR_TYPES})(\([^)]+\))? /, '')
100
+ proposed << ": #{issue.identifier} - #{summary}"
101
+ prompt.ask("Title for PR for #{issue.identifier} - #{summary}", default: proposed.join)
102
+ end
103
+
104
+ def pr_description_for(issue)
105
+ tmpfile = Tempfile.new([issue.identifier, '.md'], Rubyists::Linear.tmpdir)
106
+ # TODO: Look up templates
107
+ proposed = "# Context\n\n#{issue.description}\n\n## Issue\n\n#{issue.identifier}\n\n# Solution\n\n# Testing\n\n# Notes\n\n" # rubocop:disable Layout/LineLength
108
+ tmpfile.write(proposed) && tmpfile.close
109
+ desc = TTY::Editor.open(tmpfile.path)
110
+ return tmpfile if desc
111
+
112
+ File.open(tmpfile.path, 'w+') do |file|
113
+ file.puts prompt.ask("Description for PR for #{issue.identifier} - #{issue.title}", default: proposed)
114
+ end
115
+ tmpfile
116
+ end
117
+
118
+ def pr_type_for(issue)
119
+ proposed_type = issue.title.match(/^(#{ALLOWED_PR_TYPES})/i)
120
+ return proposed_type[1].downcase if proposed_type
121
+
122
+ prompt.select('What type of PR is this?', %w[fix feature chore refactor test docs style ci perf security])
123
+ end
124
+
125
+ def pr_scope_for(title)
126
+ proposed_scope = title.match(/^\w+\(([^\)]+)\)/)
127
+ return proposed_scope[1].downcase if proposed_scope
128
+
129
+ scope = prompt.ask('What is the scope of this PR?', default: 'none')
130
+ return nil if scope.empty? && scope == 'none'
131
+
132
+ scope
133
+ end
134
+
135
+ def labels_for(team, labels = nil)
136
+ return Rubyists::Linear::Label.find_all_by_name(labels.map(&:strip)) if labels
137
+
138
+ prompt.multi_select('Labels:', team.labels.to_h { |t| [t.name, t] })
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
data/lib/linear/cli.rb CHANGED
@@ -16,7 +16,14 @@ module Rubyists
16
16
  extend Dry::CLI::Registry
17
17
 
18
18
  def self.prompt
19
- @prompt ||= TTY::Prompt.new
19
+ return @prompt if @prompt
20
+
21
+ @prompt = TTY::Prompt.new
22
+ @prompt.on(:keypress) do |event|
23
+ @prompt.trigger(:keydown) if event.value == 'j'
24
+ @prompt.trigger(:keyup) if event.value == 'k'
25
+ end
26
+ @prompt
20
27
  end
21
28
 
22
29
  def self.register_sub!(command, sub_file, klass)
@@ -21,6 +21,7 @@ module Rubyists
21
21
  option :description, type: :string, aliases: ['-d'], desc: 'Issue Description'
22
22
  option :team, type: :string, aliases: ['-T'], desc: 'Team Identifier'
23
23
  option :labels, type: :array, aliases: ['-l'], desc: 'Labels for the issue (Comma separated list)'
24
+ option :project, type: :string, aliases: ['-p'], desc: 'Project Identifier'
24
25
  option :develop, type: :boolean, aliases: ['-D', '--dev'], desc: 'Start development after creating the issue'
25
26
 
26
27
  def call(**options)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'semantic_logger'
4
+ require 'git'
5
+ require_relative '../issue'
6
+
7
+ module Rubyists
8
+ # Namespace for Linear
9
+ module Linear
10
+ M :issue, :user, :label
11
+ # Namespace for CLI
12
+ module CLI
13
+ module Issue
14
+ Pr = Class.new Dry::CLI::Command
15
+ # The Develop class is a Dry::CLI::Command to start/update development status of an issue
16
+ class Pr
17
+ include SemanticLogger::Loggable
18
+ include Rubyists::Linear::CLI::CommonOptions
19
+ include Rubyists::Linear::CLI::Issue # for #gimme_da_issue! and other Issue methods
20
+ desc 'Create a PR for an issue and push it to the remote'
21
+ argument :issue_id, required: true, desc: 'The Issue (i.e. CRY-1)'
22
+ option :title, required: false, desc: 'The title of the PR'
23
+ option :description, required: false, desc: 'The description of the PR'
24
+
25
+ def call(issue_id:, **options)
26
+ logger.debug('Creating PR for issue issue', options:)
27
+ issue = gimme_da_issue!(issue_id, me: Rubyists::Linear::User.me)
28
+ branch_name = issue.branchName
29
+ branch = branch_for(branch_name)
30
+ branch.checkout
31
+ prompt.ok "Checked out branch #{branch_name}"
32
+ issue_pr(issue, **options)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -18,18 +18,33 @@ module Rubyists
18
18
  include Rubyists::Linear::CLI::CommonOptions
19
19
  include Rubyists::Linear::CLI::Issue # for #gimme_da_issue! and other Issue methods
20
20
  desc 'Update an issue'
21
- argument :issue_ids, type: :array, required: true, desc: 'Issue IDs (i.e. ISS-1)'
22
- option :comment, type: :string, aliases: ['-m'], desc: 'Comment to add to the issue'
23
- option :pr, type: :boolean, aliases: ['--pull-request'], default: false, desc: 'Create a pull request'
21
+ argument :issue_ids, type: :array, required: true, desc: 'Issue IDs (i.e. CRY-1)'
22
+ option :comment, type: :string, aliases: ['-m'], desc: 'Comment to add to the issue. - open an editor'
23
+ option :project, type: :string, aliases: ['-p'], desc: 'Project to move the issue to. - select from a list'
24
+ option :cancel, type: :boolean, default: false, desc: 'Cancel the issue'
24
25
  option :close, type: :boolean, default: false, desc: 'Close the issue'
25
- option :reason, type: :string, aliases: ['--butwhy'], desc: 'Reason for closing the issue'
26
+ option :reason, type: :string, aliases: ['--butwhy'], desc: 'Reason for closing the issue. - open an editor'
27
+ option :trash,
28
+ type: :boolean,
29
+ default: false,
30
+ desc: 'Also trash the issue (--close and --cancel support this option)'
31
+
32
+ example [
33
+ '--comment "This is a comment" CRY-1 CRY2 # Add a comment to multiple issues',
34
+ '--comment - CRY-1 CRY2 # Add a comment to multiple issues, open an editor',
35
+ '--project "Manhattan" CRY-3 CRY-4 # Move tickets to a different project',
36
+ '--close CRY-2 # Close an issue. Will be prompted for a reason',
37
+ '--close --reason "Done" CRY-1 CRY-2 # Close multiple issues with a reason',
38
+ '--cancel --trash --reason "Garbage" CRY-2 # Cancel an issue, and throw it in the trash'
39
+ ]
26
40
 
27
41
  def call(issue_ids:, **options)
28
- prompt.error('You should provide at least one issue ID') && raise(SmellsBad) if issue_ids.empty?
42
+ raise SmellsBad, 'No issue IDs provided!' if issue_ids.empty?
43
+ raise SmellsBad, 'You may only open a PR against a single issue' if options[:pr] && issue_ids.size > 1
29
44
 
30
45
  logger.debug('Updating issues', issue_ids:, options:)
31
46
  Rubyists::Linear::Issue.find_all(issue_ids).each do |issue|
32
- update_issue(issue, **options)
47
+ update_issue(issue, **options) # defined in lib/linear/commands/issue.rb
33
48
  end
34
49
  end
35
50
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This is where the #reason_for, #title_for, #description_for, #team_for, and #labels_for methods are defined
4
+ # as well as other helpers which are used in multiple commands and subcommands
5
+ # This is also where the #prompt method is defined, which is used to display messages to the user and get input
3
6
  require_relative '../cli/sub_commands'
7
+ require 'tty-editor'
8
+ require 'git'
4
9
 
5
10
  module Rubyists
6
11
  module Linear
@@ -12,6 +17,7 @@ module Rubyists
12
17
  include CLI::SubCommands
13
18
 
14
19
  DESCRIPTION = 'Manage issues'
20
+ ALLOWED_PR_TYPES = 'bug|fix|sec(urity)|feat(ure)|chore|refactor|test|docs|style|ci|perf'
15
21
 
16
22
  # Aliases for Issue commands
17
23
  ALIASES = {
@@ -19,56 +25,83 @@ module Rubyists
19
25
  develop: %w[d dev], # aliases for the develop command
20
26
  list: %w[l ls], # aliases for the list command
21
27
  update: %w[u], # aliases for the close command
28
+ pr: %w[pull-request], # aliases for the pr command
22
29
  issue: %w[i issues] # aliases for the main issue command itself
23
30
  }.freeze
24
31
 
25
32
  def issue_comment(issue, comment)
26
- issue.add_comment(comment)
27
- prompt.ok("Comment added to #{issue.identifier}")
33
+ issue.add_comment comment_for(issue, comment)
34
+ prompt.ok "Comment added to #{issue.identifier}"
35
+ end
36
+
37
+ def cancel_issue(issue, **options)
38
+ reason = reason_for(options[:reason], four: "cancelling #{issue.identifier} - #{issue.title}")
39
+ issue_comment issue, reason
40
+ cancel_state = cancel_state_for(issue)
41
+ issue.close! state: cancel_state, trash: options[:trash]
42
+ prompt.ok "#{issue.identifier} was cancelled"
28
43
  end
29
44
 
30
45
  def close_issue(issue, **options)
31
- reason = reason_for(options[:reason], four: "closing #{issue.identifier} - #{issue.title}")
32
- issue_comment(issue, reason)
33
- close_state = completed_state_for(issue)
34
- issue.close!(state: close_state, trash: options[:trash])
35
- prompt.ok("#{issue.identifier} was closed")
46
+ cancelled = options[:cancel]
47
+ doing = cancelled ? 'cancelling' : 'closing'
48
+ done = cancelled ? 'cancelled' : 'closed'
49
+ workflow_state = cancelled ? cancelled_state_for(issue) : completed_state_for(issue)
50
+ reason = reason_for(options[:reason], four: "#{doing} *#{issue.identifier} - #{issue.title}*")
51
+ issue_comment issue, reason
52
+ issue.close! state: workflow_state, trash: options[:trash]
53
+ prompt.ok "#{issue.identifier} was #{done}"
54
+ end
55
+
56
+ def create_pr!(title:, body:)
57
+ return `gh pr create -a @me --title "#{title}" --body-file "#{body.path}"` if body.respond_to?(:path)
58
+
59
+ `gh pr create -a @me --title "#{title}" --body "#{body}"`
60
+ end
61
+
62
+ def issue_pr(issue, **options)
63
+ title = options[:title] || pr_title_for(issue)
64
+ body = options[:description] || pr_description_for(issue)
65
+ create_pr!(title:, body:)
36
66
  end
37
67
 
38
- def issue_pr(issue)
39
- issue.create_pr!
40
- prompt.ok("Pull request created for #{issue.identifier}")
68
+ def attach_project(issue, project_search)
69
+ project = project_for(issue.team, project_search)
70
+ issue.attach_to_project project
71
+ prompt.ok "#{issue.identifier} was attached to #{project.name}"
41
72
  end
42
73
 
43
74
  def update_issue(issue, **options)
44
75
  issue_comment(issue, options[:comment]) if options[:comment]
45
76
  return close_issue(issue, **options) if options[:close]
46
77
  return issue_pr(issue) if options[:pr]
78
+ return attach_project(issue, options[:project]) if options[:project]
47
79
  return if options[:comment]
48
80
 
49
- prompt.warn('No action taken, no options specified')
50
- prompt.ok('Issue was not updated')
81
+ prompt.warn 'No action taken, no options specified'
82
+ prompt.ok 'Issue was not updated'
51
83
  end
52
84
 
53
85
  def make_da_issue!(**options)
54
86
  # These *_for methods are defined in Rubyists::Linear::CLI::SubCommands
55
- title = title_for options[:title]
56
- description = description_for options[:description]
57
- team = team_for options[:team]
58
- labels = labels_for team, options[:labels]
59
- Rubyists::Linear::Issue.create(title:, description:, team:, labels:)
87
+ title = title_for(options[:title])
88
+ description = description_for(options[:description])
89
+ team = team_for(options[:team])
90
+ labels = labels_for(team, options[:labels])
91
+ project = project_for(team, options[:project])
92
+ Rubyists::Linear::Issue.create(title:, description:, team:, labels:, project:)
60
93
  end
61
94
 
62
95
  def gimme_da_issue!(issue_id, me: Rubyists::Linear::User.me) # rubocop:disable Naming/MethodParameterName
63
96
  logger.trace('Looking up issue', issue_id:, me:)
64
97
  issue = Rubyists::Linear::Issue.find(issue_id)
65
- if issue.assignee && issue.assignee[:id] == me.id
66
- prompt.say("You are already assigned #{issue_id}")
98
+ if issue.assignee && issue.assignee.id == me.id
99
+ prompt.say "You are already assigned #{issue_id}"
67
100
  return issue
68
101
  end
69
102
 
70
- prompt.say("Assigning issue #{issue_id} to ya")
71
- updated = issue.assign! me
103
+ prompt.say "Assigning issue #{issue_id} to ya"
104
+ updated = issue.assign!(me)
72
105
  logger.trace 'Issue taken', issue: updated
73
106
  updated
74
107
  end