linear-cli 0.7.5 → 0.7.6

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