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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33c1343c813451d0c0d39f4d883a61856364dba86459c7fe6c1c784771998431
4
- data.tar.gz: d40871678beca4fb383c3b569d367dd1434c0df81711b046ac9f970cf1bd8366
3
+ metadata.gz: ed24311870484d37b861be6bfe48af2edfc58b2c248764dd31f3f82a4ba282a5
4
+ data.tar.gz: c0404fc83e9157c97b75c1afa3fe0766a1d10838ea33e9284d380de6dde1d547
5
5
  SHA512:
6
- metadata.gz: 73c3de969e83cfa59685ab2a264bce91c746dc110630a763a3e56f2e2b052fe0a4f0679ad4bba48d4ae23749c75f89db46fc9d45ce704abae793811db3db1000
7
- data.tar.gz: 5e770e36384c62ef81f483cddd926b5cd7423ce71366146310eefb134e0675a4f349cdba22decb473e5bb0ff449be7372f2e66c4c3ce6681e9506bea5dfa6c6c
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 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
 
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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rubyists
4
4
  module Linear
5
- VERSION = '0.7.5'
5
+ VERSION = '0.7.6'
6
6
  end
7
7
  end
@@ -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. ISS-1)'
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
- prompt.error('You should provide at least one issue ID') && raise(SmellsBad) if issue_ids.empty?
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
- reason = reason_for(options[:reason], four: "closing #{issue.identifier} - #{issue.title}")
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
- close_state = completed_state_for(issue)
34
- issue.close!(state: close_state, trash: options[:trash])
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
- # Module which provides a base model for Linear models.
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.5
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