linear-cli 0.7.5 → 0.7.6
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/Readme.adoc +4 -2
- data/exe/linear-cli +8 -1
- data/lib/linear/cli/sub_commands.rb +8 -0
- data/lib/linear/cli/version.rb +1 -1
- data/lib/linear/commands/issue/update.rb +17 -3
- data/lib/linear/commands/issue.rb +18 -4
- data/lib/linear/models/base_model/class_methods.rb +97 -0
- data/lib/linear/models/base_model/method_magic.rb +25 -0
- data/lib/linear/models/base_model.rb +9 -108
- data/lib/linear.rb +8 -0
- data/linear-cli.gemspec +1 -0
- metadata +17 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed24311870484d37b861be6bfe48af2edfc58b2c248764dd31f3f82a4ba282a5
|
4
|
+
data.tar.gz: c0404fc83e9157c97b75c1afa3fe0766a1d10838ea33e9284d380de6dde1d547
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 529c58e8cfa1a550e23de0ea5f3c9bb43d559f34b8c6648aa80bdc0238ee63be00282bf6166da82bf4c5be909dd5a8212e74cd74b92e42a6d1e58bc0de98e0f8
|
7
|
+
data.tar.gz: ee09439dfb8d39fbec79ec0206822a9261b340b4e9c0bc15cfb529dcc68a0e3138810833250972cbbd4a30bee1149f5e280b544193dfc414cb825aefe1c20218
|
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
|
|
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
|
@@ -54,6 +54,14 @@ module Rubyists
|
|
54
54
|
prompt.ask(question)
|
55
55
|
end
|
56
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
|
+
|
57
65
|
def completed_state_for(thingy)
|
58
66
|
states = thingy.completed_states
|
59
67
|
return states.first if states.size == 1
|
data/lib/linear/cli/version.rb
CHANGED
@@ -18,18 +18,32 @@ 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.
|
21
|
+
argument :issue_ids, type: :array, required: true, desc: 'Issue IDs (i.e. CRY-1)'
|
22
22
|
option :comment, type: :string, aliases: ['-m'], desc: 'Comment to add to the issue'
|
23
23
|
option :pr, type: :boolean, aliases: ['--pull-request'], default: false, desc: 'Create a pull request'
|
24
|
+
option :cancel, type: :boolean, default: false, desc: 'Cancel the issue'
|
24
25
|
option :close, type: :boolean, default: false, desc: 'Close the issue'
|
25
26
|
option :reason, type: :string, aliases: ['--butwhy'], desc: 'Reason for closing the issue'
|
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
|
+
'--pr CRY-10 # Create a pull request for the issue',
|
35
|
+
'--close CRY-2 # Close an issue. Will be prompted for a reason',
|
36
|
+
'--close --reason "Done" CRY-1 CRY-2 # Close multiple issues with a reason',
|
37
|
+
'--cancel --trash --reason "Garbage" CRY-2 # Cancel an issue, and throw it in the trash'
|
38
|
+
]
|
26
39
|
|
27
40
|
def call(issue_ids:, **options)
|
28
|
-
|
41
|
+
raise SmellsBad, 'No issue IDs provided!' if issue_ids.empty?
|
42
|
+
raise SmellsBad, 'You may only open a PR against a single issue' if options[:pr] && issue_ids.size > 1
|
29
43
|
|
30
44
|
logger.debug('Updating issues', issue_ids:, options:)
|
31
45
|
Rubyists::Linear::Issue.find_all(issue_ids).each do |issue|
|
32
|
-
update_issue(issue, **options)
|
46
|
+
update_issue(issue, **options) # defined in lib/linear/commands/issue.rb
|
33
47
|
end
|
34
48
|
end
|
35
49
|
end
|
@@ -1,5 +1,8 @@
|
|
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'
|
4
7
|
|
5
8
|
module Rubyists
|
@@ -27,12 +30,23 @@ module Rubyists
|
|
27
30
|
prompt.ok("Comment added to #{issue.identifier}")
|
28
31
|
end
|
29
32
|
|
33
|
+
def cancel_issue(issue, **options)
|
34
|
+
reason = reason_for(options[:reason], four: "cancelling #{issue.identifier} - #{issue.title}")
|
35
|
+
issue_comment(issue, reason)
|
36
|
+
cancel_state = cancel_state_for(issue)
|
37
|
+
issue.close!(state: cancel_state, trash: options[:trash])
|
38
|
+
prompt.ok("#{issue.identifier} was cancelled")
|
39
|
+
end
|
40
|
+
|
30
41
|
def close_issue(issue, **options)
|
31
|
-
|
42
|
+
cancelled = options[:cancel]
|
43
|
+
doing = cancelled ? 'cancelling' : 'closing'
|
44
|
+
done = cancelled ? 'cancelled' : 'closed'
|
45
|
+
workflow_state = cancelled ? cancelled_state_for(issue) : completed_state_for(issue)
|
46
|
+
reason = reason_for(options[:reason], four: "#{doing} #{issue.identifier} - #{issue.title}")
|
32
47
|
issue_comment(issue, reason)
|
33
|
-
|
34
|
-
issue.
|
35
|
-
prompt.ok("#{issue.identifier} was closed")
|
48
|
+
issue.close!(state: workflow_state, trash: options[:trash])
|
49
|
+
prompt.ok("#{issue.identifier} was #{done}")
|
36
50
|
end
|
37
51
|
|
38
52
|
def issue_pr(issue)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubyists
|
4
|
+
module Linear
|
5
|
+
class BaseModel
|
6
|
+
# Class methods for Linear models.
|
7
|
+
module ClassMethods
|
8
|
+
def many_to_one(relation, klass)
|
9
|
+
define_method relation do
|
10
|
+
return instance_variable_get("@#{relation}") if instance_variable_defined?("@#{relation}")
|
11
|
+
return unless (val = data[relation])
|
12
|
+
|
13
|
+
instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(val))
|
14
|
+
end
|
15
|
+
|
16
|
+
define_method "#{relation}=" do |val|
|
17
|
+
hash = val.is_a?(Hash) ? val : val.data
|
18
|
+
updated_data[relation] = hash
|
19
|
+
instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(hash))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
alias one_to_one many_to_one
|
24
|
+
|
25
|
+
def find(id_val)
|
26
|
+
camel_name = just_name.camelize :lower
|
27
|
+
bf = base_fragment
|
28
|
+
query_data = Api.query(query { __node(camel_name, id: id_val) { ___ bf } })
|
29
|
+
new query_data[camel_name.to_sym]
|
30
|
+
end
|
31
|
+
|
32
|
+
def const_added(const)
|
33
|
+
return unless const == :Base
|
34
|
+
|
35
|
+
include MethodMagic
|
36
|
+
end
|
37
|
+
|
38
|
+
def allq(filter: nil, limit: 50, after: nil)
|
39
|
+
args = { first: limit }
|
40
|
+
args[:filter] = filter ? basic_filter.merge(filter) : basic_filter
|
41
|
+
args.delete(:filter) if args[:filter].empty?
|
42
|
+
args[:after] = after if after
|
43
|
+
all_query args, plural.to_s, base_fragment
|
44
|
+
end
|
45
|
+
|
46
|
+
def all_query(args, subject, base_fragment)
|
47
|
+
query do
|
48
|
+
__node(subject, args) do
|
49
|
+
edges do
|
50
|
+
node { ___ base_fragment }
|
51
|
+
cursor
|
52
|
+
end
|
53
|
+
___ Fragments::PageInfo
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def just_name
|
59
|
+
name.split('::').last
|
60
|
+
end
|
61
|
+
|
62
|
+
def base_fragment
|
63
|
+
const_get(:Base)
|
64
|
+
end
|
65
|
+
|
66
|
+
def basic_filter
|
67
|
+
return const_get(:BASIC_FILTER) if const_defined?(:BASIC_FILTER)
|
68
|
+
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
|
72
|
+
def plural
|
73
|
+
return const_get(:PLURAL) if const_defined?(:PLURAL)
|
74
|
+
|
75
|
+
just_name.downcase.pluralize.to_sym
|
76
|
+
end
|
77
|
+
|
78
|
+
def gql_query(filter: nil, after: nil)
|
79
|
+
Api.query(allq(filter:, after:))
|
80
|
+
end
|
81
|
+
|
82
|
+
def all(after: nil, filter: nil, max: 100)
|
83
|
+
edges = []
|
84
|
+
moar = true
|
85
|
+
while moar
|
86
|
+
data = gql_query(filter:, after:)
|
87
|
+
subjects = data[plural]
|
88
|
+
edges += subjects[:edges]
|
89
|
+
moar = false if edges.size >= max || !subjects[:pageInfo][:hasNextPage]
|
90
|
+
after = subjects[:pageInfo][:endCursor]
|
91
|
+
end
|
92
|
+
edges.map { |edge| new edge[:node] }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubyists
|
4
|
+
module Linear
|
5
|
+
class BaseModel
|
6
|
+
# Methods for Linear models.
|
7
|
+
module MethodMagic
|
8
|
+
def self.included(base) # rubocop:disable Metrics/MethodLength
|
9
|
+
base.instance_eval do
|
10
|
+
base.base_fragment.__nodes.each do |node|
|
11
|
+
sym = node.__name.to_sym
|
12
|
+
define_method node.__name do
|
13
|
+
updated_data[sym]
|
14
|
+
end
|
15
|
+
|
16
|
+
define_method "#{node.__name}=" do |value|
|
17
|
+
updated_data[sym] = value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -8,120 +8,17 @@ module Rubyists
|
|
8
8
|
# Namespace for Linear
|
9
9
|
module Linear
|
10
10
|
L :api, :fragments
|
11
|
-
|
11
|
+
M 'base_model/method_magic', 'base_model/class_methods'
|
12
|
+
# The base model for all Linear models
|
12
13
|
class BaseModel
|
13
14
|
extend GQLi::DSL
|
14
15
|
include GQLi::DSL
|
15
16
|
include SemanticLogger::Loggable
|
16
|
-
|
17
|
-
# Methods for Linear models.
|
18
|
-
module MethodMagic
|
19
|
-
def self.included(base) # rubocop:disable Metrics/MethodLength
|
20
|
-
base.instance_eval do
|
21
|
-
base.base_fragment.__nodes.each do |node|
|
22
|
-
sym = node.__name.to_sym
|
23
|
-
define_method node.__name do
|
24
|
-
updated_data[sym]
|
25
|
-
end
|
26
|
-
|
27
|
-
define_method "#{node.__name}=" do |value|
|
28
|
-
updated_data[sym] = value
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# Class methods for Linear models.
|
36
|
-
class << self
|
37
|
-
def one_to_one(relation, klass)
|
38
|
-
define_method relation do
|
39
|
-
return instance_variable_get("@#{relation}") if instance_variable_defined?("@#{relation}")
|
40
|
-
return unless (val = data[relation])
|
41
|
-
|
42
|
-
instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(val))
|
43
|
-
end
|
44
|
-
|
45
|
-
define_method "#{relation}=" do |val|
|
46
|
-
hash = val.is_a?(Hash) ? val : val.data
|
47
|
-
updated_data[relation] = hash
|
48
|
-
instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(hash))
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def find(id_val)
|
53
|
-
camel_name = just_name.camelize :lower
|
54
|
-
bf = base_fragment
|
55
|
-
query_data = Api.query(query { __node(camel_name, id: id_val) { ___ bf } })
|
56
|
-
new query_data[camel_name.to_sym]
|
57
|
-
end
|
58
|
-
|
59
|
-
def const_added(const)
|
60
|
-
return unless const == :Base
|
61
|
-
|
62
|
-
include MethodMagic
|
63
|
-
end
|
64
|
-
|
65
|
-
def allq(filter: nil, limit: 50, after: nil)
|
66
|
-
args = { first: limit }
|
67
|
-
args[:filter] = filter ? basic_filter.merge(filter) : basic_filter
|
68
|
-
args.delete(:filter) if args[:filter].empty?
|
69
|
-
args[:after] = after if after
|
70
|
-
all_query args, plural.to_s, base_fragment
|
71
|
-
end
|
72
|
-
|
73
|
-
def all_query(args, subject, base_fragment)
|
74
|
-
query do
|
75
|
-
__node(subject, args) do
|
76
|
-
edges do
|
77
|
-
node { ___ base_fragment }
|
78
|
-
cursor
|
79
|
-
end
|
80
|
-
___ Fragments::PageInfo
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def just_name
|
86
|
-
name.split('::').last
|
87
|
-
end
|
88
|
-
|
89
|
-
def base_fragment
|
90
|
-
const_get(:Base)
|
91
|
-
end
|
92
|
-
|
93
|
-
def basic_filter
|
94
|
-
return const_get(:BASIC_FILTER) if const_defined?(:BASIC_FILTER)
|
95
|
-
|
96
|
-
{}
|
97
|
-
end
|
98
|
-
|
99
|
-
def plural
|
100
|
-
return const_get(:PLURAL) if const_defined?(:PLURAL)
|
101
|
-
|
102
|
-
just_name.downcase.pluralize.to_sym
|
103
|
-
end
|
104
|
-
|
105
|
-
def gql_query(filter: nil, after: nil)
|
106
|
-
Api.query(allq(filter:, after:))
|
107
|
-
end
|
108
|
-
|
109
|
-
def all(after: nil, filter: nil, max: 100)
|
110
|
-
edges = []
|
111
|
-
moar = true
|
112
|
-
while moar
|
113
|
-
data = gql_query(filter:, after:)
|
114
|
-
subjects = data[plural]
|
115
|
-
edges += subjects[:edges]
|
116
|
-
moar = false if edges.size >= max || !subjects[:pageInfo][:hasNextPage]
|
117
|
-
after = subjects[:pageInfo][:endCursor]
|
118
|
-
end
|
119
|
-
edges.map { |edge| new edge[:node] }
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
17
|
+
extend ClassMethods
|
123
18
|
attr_reader :data, :updated_data
|
124
19
|
|
20
|
+
CANCELLED_STATES = %w[cancelled canceled].freeze
|
21
|
+
|
125
22
|
def initialize(data)
|
126
23
|
data.each_key { |k| raise SmellsBad, "Unknown key #{k}" unless respond_to? "#{k}=" }
|
127
24
|
@data = data
|
@@ -136,6 +33,10 @@ module Rubyists
|
|
136
33
|
workflow_states.select { |ws| ws.type == 'completed' }
|
137
34
|
end
|
138
35
|
|
36
|
+
def cancelled_states
|
37
|
+
workflow_states.select { |ws| CANCELLED_STATES.include? ws.type }
|
38
|
+
end
|
39
|
+
|
139
40
|
def to_h
|
140
41
|
updated_data
|
141
42
|
end
|
data/lib/linear.rb
CHANGED
@@ -24,6 +24,14 @@ module Rubyists
|
|
24
24
|
FEATURE_ROOT = ROOT/:features
|
25
25
|
DEBUG_LEVELS = %i[warn info debug trace].freeze
|
26
26
|
|
27
|
+
def self.tmpdir=(other)
|
28
|
+
@tmpdir = other.is_a?(Pathname) ? other : Pathname(other)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.tmpdir
|
32
|
+
@tmpdir || raise('tmpdir not set')
|
33
|
+
end
|
34
|
+
|
27
35
|
def self.L(*libraries) # rubocop:disable Naming/MethodName
|
28
36
|
Array(libraries).each { |library| require LIBROOT/library }
|
29
37
|
end
|
data/linear-cli.gemspec
CHANGED
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
|
|
43
43
|
spec.add_dependency 'semantic_logger', '~> 4.0'
|
44
44
|
spec.add_dependency 'sequel', '~> 5.0'
|
45
45
|
spec.add_dependency 'sqlite3', '~> 1.7'
|
46
|
+
spec.add_dependency 'tty-editor', '~> 0.7'
|
46
47
|
spec.add_dependency 'tty-markdown', '~> 0.7'
|
47
48
|
spec.add_dependency 'tty-prompt', '~> 0.23'
|
48
49
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: linear-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tj (bougyman) Vanderpoel
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '1.7'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: tty-editor
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.7'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.7'
|
153
167
|
- !ruby/object:Gem::Dependency
|
154
168
|
name: tty-markdown
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -250,6 +264,8 @@ files:
|
|
250
264
|
- lib/linear/exceptions.rb
|
251
265
|
- lib/linear/fragments.rb
|
252
266
|
- lib/linear/models/base_model.rb
|
267
|
+
- lib/linear/models/base_model/class_methods.rb
|
268
|
+
- lib/linear/models/base_model/method_magic.rb
|
253
269
|
- lib/linear/models/issue.rb
|
254
270
|
- lib/linear/models/label.rb
|
255
271
|
- lib/linear/models/team.rb
|