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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -2
- data/Readme.adoc +11 -3
- data/changelog/0.7.7/added_ability_to_attach_project_to_command.yml +4 -0
- data/changelog/0.7.7/added_issue_pr_command.yml +4 -0
- data/changelog/0.7.7/added_lcomment_alias_to_add_comments_to_issues.yml +4 -0
- data/changelog/0.7.7/tag.yml +1 -0
- data/exe/lc +5 -1
- data/exe/lclose +1 -0
- data/exe/lcls +1 -0
- data/exe/lcomment +1 -0
- data/exe/lcreate +1 -0
- data/exe/linear-cli +8 -1
- data/exe/{lc.sh → scripts/lc.sh} +4 -3
- data/exe/scripts/lcls.sh +2 -0
- data/exe/scripts/lcomment.sh +2 -0
- data/lib/linear/api.rb +1 -1
- data/lib/linear/cli/caller.rb +6 -1
- data/lib/linear/cli/sub_commands.rb +5 -59
- data/lib/linear/cli/version.rb +1 -1
- data/lib/linear/cli/what_for.rb +143 -0
- data/lib/linear/cli.rb +8 -1
- data/lib/linear/commands/issue/create.rb +1 -0
- data/lib/linear/commands/issue/pr.rb +38 -0
- data/lib/linear/commands/issue/update.rb +21 -6
- data/lib/linear/commands/issue.rb +54 -21
- data/lib/linear/models/base_model/class_methods.rb +129 -0
- data/lib/linear/models/base_model/method_magic.rb +23 -0
- data/lib/linear/models/base_model.rb +9 -108
- data/lib/linear/models/issue/class_methods.rb +44 -0
- data/lib/linear/models/issue.rb +23 -38
- data/lib/linear/models/project.rb +47 -0
- data/lib/linear/models/team.rb +6 -9
- data/lib/linear.rb +8 -0
- data/linear-cli.gemspec +2 -1
- metadata +33 -10
- data/exe/lclose +0 -4
- data/exe/lcls +0 -4
- data/exe/lcls.sh +0 -2
- data/exe/lcreate +0 -4
- /data/exe/{lclose.sh → scripts/lclose.sh} +0 -0
- /data/exe/{lcreate.sh → scripts/lcreate.sh} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 040dce1ff59ddd000240b8a03105928d775841be91d1fe50f974249e2ddc21fc
|
4
|
+
data.tar.gz: 0de33b286614b981e63ad75b14d1728e825d44e6ec2265c625a00888cdb32cf2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
49
|
-
[0.7.
|
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
|
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
|
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 @@
|
|
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
|
-
|
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
|
-
|
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
|
data/exe/{lc.sh → scripts/lc.sh}
RENAMED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
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
|
data/exe/scripts/lcls.sh
ADDED
data/lib/linear/api.rb
CHANGED
data/lib/linear/cli/caller.rb
CHANGED
@@ -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,
|
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
|
data/lib/linear/cli/version.rb
CHANGED
@@ -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
|
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.
|
22
|
-
option :comment, type: :string, aliases: ['-m'], desc: 'Comment to add to the issue'
|
23
|
-
option :
|
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
|
-
|
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
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
issue
|
35
|
-
|
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
|
39
|
-
issue.
|
40
|
-
|
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
|
50
|
-
prompt.ok
|
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
|
56
|
-
description = description_for
|
57
|
-
team = team_for
|
58
|
-
labels = labels_for
|
59
|
-
|
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
|
66
|
-
prompt.say
|
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
|
71
|
-
updated = issue.assign!
|
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
|