linear-cli 0.7.5 → 0.7.7

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