linear-cli 0.7.6 → 0.8.0
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 +14 -2
- data/Readme.adoc +23 -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/changelog/0.8.0/added_containerfile_to_build_oci_image.yml +4 -0
- data/changelog/0.8.0/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/{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 -67
- 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 +5 -4
- data/lib/linear/commands/issue.rb +42 -23
- data/lib/linear/models/base_model/class_methods.rb +45 -10
- data/lib/linear/models/base_model/method_magic.rb +5 -7
- 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 +10 -4
- data/linear-cli.gemspec +1 -1
- data/oci/Containerfile +32 -0
- metadata +21 -11
- data/exe/lclose +0 -4
- data/exe/lcls +0 -4
- data/exe/lcls.sh +0 -2
- data/exe/lcreate +0 -4
- /data/{listings.cinema.gif → cinemas/listings.cinema.gif} +0 -0
- /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: f3115a9533c74319bbb8bafd878f9e60c60a26c557909b35cf4fff8b16870a14
|
|
4
|
+
data.tar.gz: a98d826dcfe32a3c569a73ec3df15e639d38511a8107b7e86b6c25238e6f3bb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b632968d2cfcfa38f9e54097c215043d7576529fe6417f44e6acbc741305ec5e8e822d59cf460f92257c556d5042b68559dbc41875a8faf5110b4d88e153c08
|
|
7
|
+
data.tar.gz: 3c853989005ac06d6e6899da1b763a6615ca8924e893e2de2dd0d9632e1397a30a2a8d535af80f8e6c4ed42ffe8260476241cd732afb14924069d753b5336827
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.0] - 2024-02-06
|
|
6
|
+
### Added
|
|
7
|
+
- Added Containerfile to build oci image (@bougyman)
|
|
8
|
+
|
|
9
|
+
## [0.7.7] - 2024-02-06
|
|
10
|
+
### Added
|
|
11
|
+
- Added ability to attach project to command (@bougyman)
|
|
12
|
+
- Added issue pr command (@bougyman)
|
|
13
|
+
- Added lcomment alias to add comments to issues (@bougyman)
|
|
14
|
+
|
|
5
15
|
## [0.7.5] - 2024-02-05
|
|
6
16
|
### Fixed
|
|
7
17
|
- Fixed problem when choosing from multiple completed states (@bougyman)
|
|
@@ -45,8 +55,10 @@
|
|
|
45
55
|
### Added
|
|
46
56
|
- Added new changelog management system (changelog-rb) (@bougyman)
|
|
47
57
|
|
|
48
|
-
[Unreleased]: https://github.com/rubyists/linear-cli/compare/0.
|
|
49
|
-
[0.
|
|
58
|
+
[Unreleased]: https://github.com/rubyists/linear-cli/compare/0.8.0...HEAD
|
|
59
|
+
[0.8.0]: https://github.com/rubyists/linear-cli/compare/v0.7.7...0.8.0
|
|
60
|
+
[0.7.7]: https://github.com/rubyists/linear-cli/compare/v0.7.5...v0.7.7
|
|
61
|
+
[0.7.5]: https://github.com/rubyists/linear-cli/compare/v0.7.3...v0.7.5
|
|
50
62
|
[0.7.3]: https://github.com/rubyists/linear-cli/compare/v0.7.2...v0.7.3
|
|
51
63
|
[0.7.2]: https://github.com/rubyists/linear-cli/compare/v0.7.1...v0.7.2
|
|
52
64
|
[0.7.1]: https://github.com/rubyists/linear-cli/compare/v0.7.0...v0.7.1
|
data/Readme.adoc
CHANGED
|
@@ -11,7 +11,21 @@ A command line interface to https://linear.app.
|
|
|
11
11
|
|
|
12
12
|
== Installation
|
|
13
13
|
|
|
14
|
-
===
|
|
14
|
+
=== I don't want to install
|
|
15
|
+
|
|
16
|
+
You can use the OCI container image to run the CLI without installing it.
|
|
17
|
+
|
|
18
|
+
[source,sh]
|
|
19
|
+
----
|
|
20
|
+
$ podman run -it --rm -e LINEAR_API_KEY=your-api-key ghcr.io/rubyists/linear-cli:stable lcls <1>
|
|
21
|
+
$ docker run -it --rm -e LINEAR_API_KEY=your-api-key ghcr.io/rubyists/linear-cli:stable lcls <2>
|
|
22
|
+
----
|
|
23
|
+
<1> Podman Usage
|
|
24
|
+
<2> Docker Usage
|
|
25
|
+
|
|
26
|
+
=== Gem install (Most should use this)
|
|
27
|
+
|
|
28
|
+
Requires ruby 3.2 or later
|
|
15
29
|
|
|
16
30
|
[source,sh]
|
|
17
31
|
----
|
|
@@ -80,7 +94,7 @@ $ lc w --teams
|
|
|
80
94
|
|
|
81
95
|
`lcls` is a helper provided to list issues. It's an alias for `lc issues list`.
|
|
82
96
|
|
|
83
|
-
image::listings.cinema.gif[]
|
|
97
|
+
image::cinemas/listings.cinema.gif[]
|
|
84
98
|
|
|
85
99
|
[source,sh]
|
|
86
100
|
----
|
|
@@ -133,8 +147,13 @@ at a time. You can also use the 'u' alias for 'update', and as always, the 'i' a
|
|
|
133
147
|
|
|
134
148
|
[source,sh]
|
|
135
149
|
----
|
|
136
|
-
$ lc issue update --comment "Here is a comment" CRY-1234
|
|
150
|
+
$ lc issue update --comment "Here is a comment" CRY-1234 <1>
|
|
151
|
+
$ lc issue update --comment - CRY-14 CRY-15 <2>
|
|
152
|
+
$ lcomment CRY-1234 CRY-3 <3>
|
|
137
153
|
----
|
|
154
|
+
<1> This will use the provided comment
|
|
155
|
+
<2> This will prompt for a comment (use '-' to prompt)
|
|
156
|
+
<3> This will always prompt you for a comment ('lcomment' is an alias for 'lc issue update --comment -')
|
|
138
157
|
|
|
139
158
|
===== Close one or many issues
|
|
140
159
|
|
|
@@ -152,4 +171,5 @@ Some command aliases are available to make things easier to type.
|
|
|
152
171
|
$ lcls
|
|
153
172
|
$ lcreate --description "This is a new issue" --labels Bug,Feature --team CRY
|
|
154
173
|
$ lclose --reason "This issue sucks" CRY-1234 CRY-456
|
|
174
|
+
$ lcancel --reason "These should never have been here" --trash CRY-1234 CRY-456
|
|
155
175
|
----
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
date: 2024-02-06
|
|
@@ -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/{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,73 +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 cancelled_state_for(thingy)
|
|
58
|
-
states = thingy.cancelled_states
|
|
59
|
-
return states.first if states.size == 1
|
|
60
|
-
|
|
61
|
-
selection = prompt.select('Choose a cancelled state', states.to_h { |s| [s.name, s.id] })
|
|
62
|
-
Rubyists::Linear::WorkflowState.find selection
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def completed_state_for(thingy)
|
|
66
|
-
states = thingy.completed_states
|
|
67
|
-
return states.first if states.size == 1
|
|
68
|
-
|
|
69
|
-
selection = prompt.select('Choose a completed state', states.to_h { |s| [s.name, s.id] })
|
|
70
|
-
Rubyists::Linear::WorkflowState.find selection
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def description_for(description = nil)
|
|
74
|
-
return description if description
|
|
75
|
-
|
|
76
|
-
prompt.multiline('Description:').map(&:chomp).join('\\n')
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def title_for(title = nil)
|
|
80
|
-
return title if title
|
|
81
|
-
|
|
82
|
-
prompt.ask('Title:')
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def labels_for(team, labels = nil)
|
|
86
|
-
return Rubyists::Linear::Label.find_all_by_name(labels.map(&:strip)) if labels
|
|
87
|
-
|
|
88
|
-
prompt.on(:keypress) do |event|
|
|
89
|
-
prompt.trigger(:keydown) if event.value == 'j'
|
|
90
|
-
prompt.trigger(:keyup) if event.value == 'k'
|
|
91
|
-
end
|
|
92
|
-
prompt.multi_select('Labels:', team.labels.to_h { |t| [t.name, t] })
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def cut_branch!(branch_name)
|
|
96
|
-
if current_branch != default_branch
|
|
97
|
-
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
|
|
98
|
-
end
|
|
99
|
-
git.branch(branch_name)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def branch_for(branch_name)
|
|
103
|
-
logger.trace('Looking for branch', branch_name:)
|
|
104
|
-
existing = git.branches[branch_name]
|
|
105
|
-
return cut_branch!(branch_name) unless existing
|
|
106
|
-
|
|
107
|
-
logger.trace('Branch found', branch: existing&.name)
|
|
108
|
-
existing
|
|
109
|
-
end
|
|
110
|
-
|
|
111
49
|
def current_branch
|
|
112
50
|
git.current_branch
|
|
113
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
|
|
@@ -19,11 +19,11 @@ module Rubyists
|
|
|
19
19
|
include Rubyists::Linear::CLI::Issue # for #gimme_da_issue! and other Issue methods
|
|
20
20
|
desc 'Update an issue'
|
|
21
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'
|
|
23
|
-
option :
|
|
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
24
|
option :cancel, type: :boolean, default: false, desc: 'Cancel the issue'
|
|
25
25
|
option :close, type: :boolean, default: false, desc: 'Close the issue'
|
|
26
|
-
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
27
|
option :trash,
|
|
28
28
|
type: :boolean,
|
|
29
29
|
default: false,
|
|
@@ -31,7 +31,8 @@ module Rubyists
|
|
|
31
31
|
|
|
32
32
|
example [
|
|
33
33
|
'--comment "This is a comment" CRY-1 CRY2 # Add a comment to multiple issues',
|
|
34
|
-
'--
|
|
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',
|
|
35
36
|
'--close CRY-2 # Close an issue. Will be prompted for a reason',
|
|
36
37
|
'--close --reason "Done" CRY-1 CRY-2 # Close multiple issues with a reason',
|
|
37
38
|
'--cancel --trash --reason "Garbage" CRY-2 # Cancel an issue, and throw it in the trash'
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# as well as other helpers which are used in multiple commands and subcommands
|
|
5
5
|
# This is also where the #prompt method is defined, which is used to display messages to the user and get input
|
|
6
6
|
require_relative '../cli/sub_commands'
|
|
7
|
+
require 'tty-editor'
|
|
8
|
+
require 'git'
|
|
7
9
|
|
|
8
10
|
module Rubyists
|
|
9
11
|
module Linear
|
|
@@ -15,6 +17,7 @@ module Rubyists
|
|
|
15
17
|
include CLI::SubCommands
|
|
16
18
|
|
|
17
19
|
DESCRIPTION = 'Manage issues'
|
|
20
|
+
ALLOWED_PR_TYPES = 'bug|fix|sec(urity)|feat(ure)|chore|refactor|test|docs|style|ci|perf'
|
|
18
21
|
|
|
19
22
|
# Aliases for Issue commands
|
|
20
23
|
ALIASES = {
|
|
@@ -22,20 +25,21 @@ module Rubyists
|
|
|
22
25
|
develop: %w[d dev], # aliases for the develop command
|
|
23
26
|
list: %w[l ls], # aliases for the list command
|
|
24
27
|
update: %w[u], # aliases for the close command
|
|
28
|
+
pr: %w[pull-request], # aliases for the pr command
|
|
25
29
|
issue: %w[i issues] # aliases for the main issue command itself
|
|
26
30
|
}.freeze
|
|
27
31
|
|
|
28
32
|
def issue_comment(issue, comment)
|
|
29
|
-
issue.add_comment(comment)
|
|
30
|
-
prompt.ok
|
|
33
|
+
issue.add_comment comment_for(issue, comment)
|
|
34
|
+
prompt.ok "Comment added to #{issue.identifier}"
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
def cancel_issue(issue, **options)
|
|
34
38
|
reason = reason_for(options[:reason], four: "cancelling #{issue.identifier} - #{issue.title}")
|
|
35
|
-
issue_comment
|
|
39
|
+
issue_comment issue, reason
|
|
36
40
|
cancel_state = cancel_state_for(issue)
|
|
37
|
-
issue.close!
|
|
38
|
-
prompt.ok
|
|
41
|
+
issue.close! state: cancel_state, trash: options[:trash]
|
|
42
|
+
prompt.ok "#{issue.identifier} was cancelled"
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def close_issue(issue, **options)
|
|
@@ -43,46 +47,61 @@ module Rubyists
|
|
|
43
47
|
doing = cancelled ? 'cancelling' : 'closing'
|
|
44
48
|
done = cancelled ? 'cancelled' : 'closed'
|
|
45
49
|
workflow_state = cancelled ? cancelled_state_for(issue) : completed_state_for(issue)
|
|
46
|
-
reason = reason_for(options[:reason], four: "#{doing}
|
|
47
|
-
issue_comment
|
|
48
|
-
issue.close!
|
|
49
|
-
prompt.ok
|
|
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}"
|
|
50
54
|
end
|
|
51
55
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
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:)
|
|
66
|
+
end
|
|
67
|
+
|
|
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}"
|
|
55
72
|
end
|
|
56
73
|
|
|
57
74
|
def update_issue(issue, **options)
|
|
58
75
|
issue_comment(issue, options[:comment]) if options[:comment]
|
|
59
76
|
return close_issue(issue, **options) if options[:close]
|
|
60
77
|
return issue_pr(issue) if options[:pr]
|
|
78
|
+
return attach_project(issue, options[:project]) if options[:project]
|
|
61
79
|
return if options[:comment]
|
|
62
80
|
|
|
63
|
-
prompt.warn
|
|
64
|
-
prompt.ok
|
|
81
|
+
prompt.warn 'No action taken, no options specified'
|
|
82
|
+
prompt.ok 'Issue was not updated'
|
|
65
83
|
end
|
|
66
84
|
|
|
67
85
|
def make_da_issue!(**options)
|
|
68
86
|
# These *_for methods are defined in Rubyists::Linear::CLI::SubCommands
|
|
69
|
-
title = title_for
|
|
70
|
-
description = description_for
|
|
71
|
-
team = team_for
|
|
72
|
-
labels = labels_for
|
|
73
|
-
|
|
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:)
|
|
74
93
|
end
|
|
75
94
|
|
|
76
95
|
def gimme_da_issue!(issue_id, me: Rubyists::Linear::User.me) # rubocop:disable Naming/MethodParameterName
|
|
77
96
|
logger.trace('Looking up issue', issue_id:, me:)
|
|
78
97
|
issue = Rubyists::Linear::Issue.find(issue_id)
|
|
79
|
-
if issue.assignee && issue.assignee
|
|
80
|
-
prompt.say
|
|
98
|
+
if issue.assignee && issue.assignee.id == me.id
|
|
99
|
+
prompt.say "You are already assigned #{issue_id}"
|
|
81
100
|
return issue
|
|
82
101
|
end
|
|
83
102
|
|
|
84
|
-
prompt.say
|
|
85
|
-
updated = issue.assign!
|
|
103
|
+
prompt.say "Assigning issue #{issue_id} to ya"
|
|
104
|
+
updated = issue.assign!(me)
|
|
86
105
|
logger.trace 'Issue taken', issue: updated
|
|
87
106
|
updated
|
|
88
107
|
end
|
|
@@ -5,28 +5,59 @@ module Rubyists
|
|
|
5
5
|
class BaseModel
|
|
6
6
|
# Class methods for Linear models.
|
|
7
7
|
module ClassMethods
|
|
8
|
-
def
|
|
8
|
+
def setter!(relation, klass)
|
|
9
|
+
define_method "#{relation}=" do |val|
|
|
10
|
+
hash = val.is_a?(Hash) ? val : val.updated_data
|
|
11
|
+
updated_data[relation] = hash
|
|
12
|
+
instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(hash))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def getter!(relation)
|
|
9
17
|
define_method relation do
|
|
10
18
|
return instance_variable_get("@#{relation}") if instance_variable_defined?("@#{relation}")
|
|
11
|
-
return unless (val = data[relation])
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
return unless (val = updated_data[relation])
|
|
21
|
+
|
|
22
|
+
send("#{relation}=", val)
|
|
14
23
|
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def many_to_one(relation, klass = nil)
|
|
27
|
+
klass ||= relation.to_s.camelize.to_sym
|
|
28
|
+
getter! relation
|
|
29
|
+
setter! relation, klass
|
|
30
|
+
end
|
|
15
31
|
|
|
32
|
+
alias one_to_one many_to_one
|
|
33
|
+
|
|
34
|
+
def many_setter!(relation, klass)
|
|
16
35
|
define_method "#{relation}=" do |val|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
vals = if val&.key?(:nodes)
|
|
37
|
+
val[:nodes]
|
|
38
|
+
else
|
|
39
|
+
Array(val)
|
|
40
|
+
end
|
|
41
|
+
updated_data[relation] = vals.map { |v| v.is_a?(Hash) ? v : v.updated_data }
|
|
42
|
+
new_relations = vals.map { |v| v.is_a?(Hash) ? Rubyists::Linear.const_get(klass).new(v) : v }
|
|
43
|
+
instance_variable_set("@#{relation}", new_relations)
|
|
20
44
|
end
|
|
21
45
|
end
|
|
22
46
|
|
|
23
|
-
|
|
47
|
+
def one_to_many(relation, klass = nil)
|
|
48
|
+
klass ||= relation.to_s.singularize.camelize.to_sym
|
|
49
|
+
getter! relation
|
|
50
|
+
many_setter! relation, klass
|
|
51
|
+
end
|
|
24
52
|
|
|
25
53
|
def find(id_val)
|
|
26
54
|
camel_name = just_name.camelize :lower
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
sym = camel_name.to_sym
|
|
56
|
+
ff = full_fragment
|
|
57
|
+
query_data = Api.query(query { __node(camel_name, id: id_val) { ___ ff } })
|
|
58
|
+
raise NotFoundError, "No #{just_name} found with id #{id_val}" if query_data[sym].nil?
|
|
59
|
+
|
|
60
|
+
new query_data[sym]
|
|
30
61
|
end
|
|
31
62
|
|
|
32
63
|
def const_added(const)
|
|
@@ -63,6 +94,10 @@ module Rubyists
|
|
|
63
94
|
const_get(:Base)
|
|
64
95
|
end
|
|
65
96
|
|
|
97
|
+
def full_fragment
|
|
98
|
+
base_fragment
|
|
99
|
+
end
|
|
100
|
+
|
|
66
101
|
def basic_filter
|
|
67
102
|
return const_get(:BASIC_FILTER) if const_defined?(:BASIC_FILTER)
|
|
68
103
|
|
|
@@ -5,17 +5,15 @@ module Rubyists
|
|
|
5
5
|
class BaseModel
|
|
6
6
|
# Methods for Linear models.
|
|
7
7
|
module MethodMagic
|
|
8
|
-
def self.included(base) # rubocop:disable Metrics/
|
|
8
|
+
def self.included(base) # rubocop:disable Metrics/AbcSize
|
|
9
9
|
base.instance_eval do
|
|
10
10
|
base.base_fragment.__nodes.each do |node|
|
|
11
11
|
sym = node.__name.to_sym
|
|
12
|
-
define_method
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
define_method(sym) { updated_data[sym] } unless instance_methods.include? sym
|
|
13
|
+
esym = :"#{sym}="
|
|
14
|
+
next if instance_methods.include? esym
|
|
15
15
|
|
|
16
|
-
define_method
|
|
17
|
-
updated_data[sym] = value
|
|
18
|
-
end
|
|
16
|
+
define_method(esym) { |value| updated_data[sym] = value }
|
|
19
17
|
end
|
|
20
18
|
end
|
|
21
19
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyists
|
|
4
|
+
# Namespace for Linear
|
|
5
|
+
module Linear
|
|
6
|
+
M :user, :team
|
|
7
|
+
# The Issue class represents a Linear issue.
|
|
8
|
+
class Issue
|
|
9
|
+
# Class methods for Issue
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def base_fragment
|
|
12
|
+
@base_fragment ||= fragment('BaseIssue', 'Issue') do
|
|
13
|
+
___ Base
|
|
14
|
+
assignee { ___ User.base_fragment }
|
|
15
|
+
team { ___ Team.base_fragment }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def full_fragment
|
|
20
|
+
@full_fragment ||= fragment('FullIssue', 'Issue') do
|
|
21
|
+
___ Base
|
|
22
|
+
assignee { ___ User.full_fragment }
|
|
23
|
+
team { ___ Team.full_fragment }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find_all(*slugs)
|
|
28
|
+
slugs.flatten.map { |slug| find(slug) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create(title:, description:, team:, project:, labels: [])
|
|
32
|
+
team_id = team.id
|
|
33
|
+
label_ids = labels.map(&:id)
|
|
34
|
+
input = { title:, description:, teamId: team_id }
|
|
35
|
+
input[:labelIds] = label_ids unless label_ids.empty?
|
|
36
|
+
input[:projectId] = project.id if project
|
|
37
|
+
m = mutation { issueCreate(input:) { issue { ___ Issue.base_fragment } } }
|
|
38
|
+
query_data = Api.query(m)
|
|
39
|
+
new query_data.dig(:issueCreate, :issue)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/linear/models/issue.rb
CHANGED
|
@@ -5,13 +5,15 @@ require 'gqli'
|
|
|
5
5
|
module Rubyists
|
|
6
6
|
# Namespace for Linear
|
|
7
7
|
module Linear
|
|
8
|
-
M :base_model
|
|
8
|
+
M :base_model
|
|
9
9
|
Issue = Class.new(BaseModel)
|
|
10
|
+
M 'issue/class_methods'
|
|
10
11
|
# The Issue class represents a Linear issue.
|
|
11
|
-
class Issue
|
|
12
|
+
class Issue
|
|
12
13
|
include SemanticLogger::Loggable
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
extend ClassMethods
|
|
15
|
+
many_to_one :assignee, :User
|
|
16
|
+
many_to_one :team, :Team
|
|
15
17
|
|
|
16
18
|
BASIC_FILTER = { completedAt: { null: true } }.freeze
|
|
17
19
|
|
|
@@ -25,38 +27,6 @@ module Rubyists
|
|
|
25
27
|
updatedAt
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
class << self
|
|
29
|
-
def base_fragment
|
|
30
|
-
@base_fragment ||= fragment('IssueWithTeams', 'Issue') do
|
|
31
|
-
___ Base
|
|
32
|
-
assignee { ___ User.base_fragment }
|
|
33
|
-
team { ___ Team.base_fragment }
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def find(slug)
|
|
38
|
-
q = query { issue(id: slug) { ___ Issue.base_fragment } }
|
|
39
|
-
data = Api.query(q)
|
|
40
|
-
raise NotFoundError, "Issue not found: #{slug}" if data.nil?
|
|
41
|
-
|
|
42
|
-
new(data[:issue])
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def find_all(*slugs)
|
|
46
|
-
slugs.flatten.map { |slug| find(slug) }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def create(title:, description:, team:, labels: [])
|
|
50
|
-
team_id = team.id
|
|
51
|
-
label_ids = labels.map(&:id)
|
|
52
|
-
input = { title:, description:, teamId: team_id }
|
|
53
|
-
input[:labelIds] = label_ids unless label_ids.empty?
|
|
54
|
-
m = mutation { issueCreate(input:) { issue { ___ Issue.base_fragment } } }
|
|
55
|
-
query_data = Api.query(m)
|
|
56
|
-
new query_data.dig(:issueCreate, :issue)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
30
|
def comment_fragment
|
|
61
31
|
@comment_fragment ||= fragment('Comment', 'Comment') do
|
|
62
32
|
id
|
|
@@ -65,6 +35,21 @@ module Rubyists
|
|
|
65
35
|
end
|
|
66
36
|
end
|
|
67
37
|
|
|
38
|
+
def update!(input)
|
|
39
|
+
id_for_this = identifier
|
|
40
|
+
m = mutation { issueUpdate(id: id_for_this, input:) { issue { ___ Issue.full_fragment } } }
|
|
41
|
+
query_data = Api.query(m)
|
|
42
|
+
updated = query_data.dig(:issueUpdate, :issue)
|
|
43
|
+
raise SmellsBad, "Unknown response for issue update: #{data} (should have :issueUpdate key)" if updated.nil?
|
|
44
|
+
|
|
45
|
+
@data = @updated_data = updated
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def attach_to_project(project)
|
|
50
|
+
update!({ projectId: project.id })
|
|
51
|
+
end
|
|
52
|
+
|
|
68
53
|
# Reference for this mutation:
|
|
69
54
|
# https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/inputs/CommentCreateInput
|
|
70
55
|
def add_comment(comment)
|
|
@@ -81,7 +66,7 @@ module Rubyists
|
|
|
81
66
|
id_for_this = identifier
|
|
82
67
|
input = { stateId: close_state.id }
|
|
83
68
|
input[:trash] = true if trash
|
|
84
|
-
mutation { issueUpdate(id: id_for_this, input:) { issue { ___ Issue.
|
|
69
|
+
mutation { issueUpdate(id: id_for_this, input:) { issue { ___ Issue.full_fragment } } }
|
|
85
70
|
end
|
|
86
71
|
|
|
87
72
|
def close!(state: nil, trash: false)
|
|
@@ -97,7 +82,7 @@ module Rubyists
|
|
|
97
82
|
|
|
98
83
|
def assign!(user)
|
|
99
84
|
this_id = identifier
|
|
100
|
-
m = mutation { issueUpdate(id: this_id, input: { assigneeId: user.id }) { issue { ___ Issue.
|
|
85
|
+
m = mutation { issueUpdate(id: this_id, input: { assigneeId: user.id }) { issue { ___ Issue.full_fragment } } }
|
|
101
86
|
query_data = Api.query(m)
|
|
102
87
|
updated = query_data.dig(:issueUpdate, :issue)
|
|
103
88
|
raise SmellsBad, "Unknown response for issue update: #{data} (should have :issueUpdate key)" if updated.nil?
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'gqli'
|
|
4
|
+
|
|
5
|
+
module Rubyists
|
|
6
|
+
# Namespace for Linear
|
|
7
|
+
module Linear
|
|
8
|
+
M :base_model
|
|
9
|
+
Project = Class.new(BaseModel)
|
|
10
|
+
# The Project class represents a Linear workflow state.
|
|
11
|
+
class Project
|
|
12
|
+
include SemanticLogger::Loggable
|
|
13
|
+
|
|
14
|
+
Base = fragment('BaseProject', 'Project') do
|
|
15
|
+
id
|
|
16
|
+
name
|
|
17
|
+
content
|
|
18
|
+
slugId
|
|
19
|
+
description
|
|
20
|
+
url
|
|
21
|
+
createdAt
|
|
22
|
+
updatedAt
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def slug
|
|
26
|
+
File.basename(url).sub("-#{slugId}", '')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def match_score?(string)
|
|
30
|
+
downed = string.downcase
|
|
31
|
+
return 100 if downed.split.join('-') == slug || downed == name.downcase
|
|
32
|
+
return 75 if name.include?(string) || slug.include?(downed)
|
|
33
|
+
return 50 if description.downcase.include?(downed)
|
|
34
|
+
|
|
35
|
+
0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_s
|
|
39
|
+
format('%<name>-12s %<url>s', name:, url:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inspection
|
|
43
|
+
format('name: "%<name>s" type: "%<url>s"', name:, url:)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/linear/models/team.rb
CHANGED
|
@@ -5,11 +5,12 @@ require 'gqli'
|
|
|
5
5
|
module Rubyists
|
|
6
6
|
# Namespace for Linear
|
|
7
7
|
module Linear
|
|
8
|
-
M :base_model, :issue, :
|
|
8
|
+
M :base_model, :issue, :project, :workflow_state, :user
|
|
9
9
|
Team = Class.new(BaseModel)
|
|
10
10
|
# The Issue class represents a Linear issue.
|
|
11
11
|
class Team
|
|
12
12
|
include SemanticLogger::Loggable
|
|
13
|
+
one_to_many :projects
|
|
13
14
|
|
|
14
15
|
# TODO: Make this configurable
|
|
15
16
|
BaseFilter = { # rubocop:disable Naming/ConstantName
|
|
@@ -29,15 +30,11 @@ module Rubyists
|
|
|
29
30
|
updatedAt
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
def self.
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
def self.full_fragment
|
|
34
|
+
@full_fragment ||= fragment('WholeTeam', 'Team') do
|
|
35
|
+
___ Base
|
|
36
|
+
projects { nodes { ___ Project.base_fragment } }
|
|
35
37
|
end
|
|
36
|
-
data = Api.query(q)
|
|
37
|
-
hash = data[:team]
|
|
38
|
-
raise NotFoundError, "Team not found: #{key}" unless hash
|
|
39
|
-
|
|
40
|
-
new hash
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
def self.mine
|
data/lib/linear.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'pathname'
|
|
4
|
-
require 'semantic_logger'
|
|
5
|
-
SemanticLogger.default_level = :info
|
|
6
|
-
SemanticLogger.add_appender(io: $stderr, formatter: :color)
|
|
7
4
|
|
|
8
5
|
# Add the / operator for path separation
|
|
9
6
|
class Pathname
|
|
@@ -15,7 +12,6 @@ end
|
|
|
15
12
|
module Rubyists
|
|
16
13
|
# Namespace for Linear classes
|
|
17
14
|
module Linear
|
|
18
|
-
include SemanticLogger::Loggable
|
|
19
15
|
# rubocop:disable Layout/SpaceAroundOperators
|
|
20
16
|
ROOT = (Pathname(__FILE__)/'../..').expand_path
|
|
21
17
|
LIBROOT = ROOT/:lib/:linear
|
|
@@ -46,6 +42,16 @@ module Rubyists
|
|
|
46
42
|
@verbosity ||= 0
|
|
47
43
|
end
|
|
48
44
|
|
|
45
|
+
def self.logger
|
|
46
|
+
return @logger if @logger
|
|
47
|
+
|
|
48
|
+
require 'semantic_logger'
|
|
49
|
+
|
|
50
|
+
SemanticLogger.default_level = :info
|
|
51
|
+
SemanticLogger.add_appender(io: $stderr, formatter: :color)
|
|
52
|
+
@logger = SemanticLogger['Rubyists::Linear']
|
|
53
|
+
end
|
|
54
|
+
|
|
49
55
|
def self.verbosity=(debug)
|
|
50
56
|
return verbosity unless debug
|
|
51
57
|
|
data/linear-cli.gemspec
CHANGED
|
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
spec.bindir = 'exe'
|
|
32
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }.reject { |f| f.end_with?('.sh') }
|
|
33
33
|
spec.require_paths = ['lib']
|
|
34
34
|
|
|
35
35
|
# Uncomment to register a new dependency of your gem
|
data/oci/Containerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
FROM docker.io/ruby:3.3.0-alpine3.19 AS build-env
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Setting env up
|
|
5
|
+
ARG APP_ROOT=/app
|
|
6
|
+
ENV LANG C.UTF-8
|
|
7
|
+
ENV BUNDLE_SILENCE_ROOT_WARNING=1
|
|
8
|
+
|
|
9
|
+
#Install dependencies needed for compilation
|
|
10
|
+
RUN apk --update add bash ruby-dev build-base git sqlite-dev
|
|
11
|
+
|
|
12
|
+
WORKDIR $APP_ROOT
|
|
13
|
+
|
|
14
|
+
COPY . $APP_ROOT
|
|
15
|
+
RUN bundle install && bundle exec rake build
|
|
16
|
+
|
|
17
|
+
CMD %w[bundle exec lc]
|
|
18
|
+
|
|
19
|
+
# Remove folders not needed in resulting image
|
|
20
|
+
RUN rm -rf node_modules tmp/cache app/assets vendor/assets spec
|
|
21
|
+
|
|
22
|
+
############### Build step done ###############
|
|
23
|
+
FROM docker.io/ruby:3.3.0-alpine3.19
|
|
24
|
+
ARG PACKAGES="bash sqlite sqlite-dev ruby-dev build-base github-cli"
|
|
25
|
+
|
|
26
|
+
# install packages
|
|
27
|
+
RUN apk --update --no-cache add $PACKAGES
|
|
28
|
+
COPY --from=build-env /app/pkg /tmp
|
|
29
|
+
RUN ls /tmp/*.gem && gem install /tmp/*.gem
|
|
30
|
+
|
|
31
|
+
CMD %w[bundle exec lc]
|
|
32
|
+
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: linear-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tj (bougyman) Vanderpoel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-02-
|
|
11
|
+
date: 2024-02-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -197,13 +197,10 @@ email:
|
|
|
197
197
|
- tj@rubyists.com
|
|
198
198
|
executables:
|
|
199
199
|
- lc
|
|
200
|
-
- lc.sh
|
|
201
200
|
- lclose
|
|
202
|
-
- lclose.sh
|
|
203
201
|
- lcls
|
|
204
|
-
-
|
|
202
|
+
- lcomment
|
|
205
203
|
- lcreate
|
|
206
|
-
- lcreate.sh
|
|
207
204
|
- linear-cli
|
|
208
205
|
extensions: []
|
|
209
206
|
extra_rdoc_files: []
|
|
@@ -233,17 +230,26 @@ files:
|
|
|
233
230
|
- changelog/0.7.3/tag.yml
|
|
234
231
|
- changelog/0.7.5/fixed_problem_when_choosing_from_multiple_completed_states.yml
|
|
235
232
|
- changelog/0.7.5/tag.yml
|
|
233
|
+
- changelog/0.7.7/added_ability_to_attach_project_to_command.yml
|
|
234
|
+
- changelog/0.7.7/added_issue_pr_command.yml
|
|
235
|
+
- changelog/0.7.7/added_lcomment_alias_to_add_comments_to_issues.yml
|
|
236
|
+
- changelog/0.7.7/tag.yml
|
|
237
|
+
- changelog/0.8.0/added_containerfile_to_build_oci_image.yml
|
|
238
|
+
- changelog/0.8.0/tag.yml
|
|
236
239
|
- changelog/unreleased/.gitkeep
|
|
237
240
|
- cinemas/listings.cinema
|
|
241
|
+
- cinemas/listings.cinema.gif
|
|
238
242
|
- exe/lc
|
|
239
|
-
- exe/lc.sh
|
|
240
243
|
- exe/lclose
|
|
241
|
-
- exe/lclose.sh
|
|
242
244
|
- exe/lcls
|
|
243
|
-
- exe/
|
|
245
|
+
- exe/lcomment
|
|
244
246
|
- exe/lcreate
|
|
245
|
-
- exe/lcreate.sh
|
|
246
247
|
- exe/linear-cli
|
|
248
|
+
- exe/scripts/lc.sh
|
|
249
|
+
- exe/scripts/lclose.sh
|
|
250
|
+
- exe/scripts/lcls.sh
|
|
251
|
+
- exe/scripts/lcomment.sh
|
|
252
|
+
- exe/scripts/lcreate.sh
|
|
247
253
|
- lib/linear.rb
|
|
248
254
|
- lib/linear/api.rb
|
|
249
255
|
- lib/linear/cli.rb
|
|
@@ -252,10 +258,12 @@ files:
|
|
|
252
258
|
- lib/linear/cli/sub_commands.rb
|
|
253
259
|
- lib/linear/cli/version.rb
|
|
254
260
|
- lib/linear/cli/watcher.rb
|
|
261
|
+
- lib/linear/cli/what_for.rb
|
|
255
262
|
- lib/linear/commands/issue.rb
|
|
256
263
|
- lib/linear/commands/issue/create.rb
|
|
257
264
|
- lib/linear/commands/issue/develop.rb
|
|
258
265
|
- lib/linear/commands/issue/list.rb
|
|
266
|
+
- lib/linear/commands/issue/pr.rb
|
|
259
267
|
- lib/linear/commands/issue/take.rb
|
|
260
268
|
- lib/linear/commands/issue/update.rb
|
|
261
269
|
- lib/linear/commands/team.rb
|
|
@@ -267,13 +275,15 @@ files:
|
|
|
267
275
|
- lib/linear/models/base_model/class_methods.rb
|
|
268
276
|
- lib/linear/models/base_model/method_magic.rb
|
|
269
277
|
- lib/linear/models/issue.rb
|
|
278
|
+
- lib/linear/models/issue/class_methods.rb
|
|
270
279
|
- lib/linear/models/label.rb
|
|
280
|
+
- lib/linear/models/project.rb
|
|
271
281
|
- lib/linear/models/team.rb
|
|
272
282
|
- lib/linear/models/user.rb
|
|
273
283
|
- lib/linear/models/workflow_state.rb
|
|
274
284
|
- lib/linear/version.rb
|
|
275
285
|
- linear-cli.gemspec
|
|
276
|
-
-
|
|
286
|
+
- oci/Containerfile
|
|
277
287
|
homepage: https://github.com/rubyists/linear-cli
|
|
278
288
|
licenses:
|
|
279
289
|
- MIT
|
data/exe/lclose
DELETED
data/exe/lcls
DELETED
data/exe/lcls.sh
DELETED
data/exe/lcreate
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|