linear-cli 0.5.4 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3178678b69a93bd4717bb28038fc549ed8cd3724ee1d987eab4be8d486025f04
4
- data.tar.gz: ad68d009c3068e5787ac4b2a24520768954e7a648533c355a04702c3496a7c48
3
+ metadata.gz: bc273e12a3dd9e864894c2484bd08a203531da62873505b33d5706f13349e2c6
4
+ data.tar.gz: ffb38d1c66023c229bc98812fa55e4a58e29342adb46e96f63cf0054560a3964
5
5
  SHA512:
6
- metadata.gz: 5257b5c0e526ac9a51c974382769e9a0602793132f8b0ec7b8f35e7ba1c6e6ab6032b988d4c89e3af9b7959e09a9642e6d66a07f7c9e626444979517a016da0b
7
- data.tar.gz: 49682ab5cf46469c720584fe72887581451a17f66edab356e1643fd391dd6fb441f06dbbfeb7d5decd96d4e76096ee51f11cd64a1c64cc90cfd44832d1d93d7a
6
+ metadata.gz: 2323c788c7095f385320006a7be3b167b7dc9fc731c96a13ea5af48dcdec0dc013f8e6f55c71e9631022ae744a5faecbc34c8bda668c172cfb98d935c1af40a0
7
+ data.tar.gz: c8422224df18d500f92d1d8e0d90cd27f41cc8d0c148943b6f53f6e6269d67aae676f2fd006e9a4210199e5aae572e39e9b3bf46ddce2206f2880d88c3a78fac
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.0] - 2024-02-04
6
+
7
+ ## [0.5.5] - 2024-02-04
8
+ ### Added
9
+ - Added lclose alias and 'issue update' subcommand (@bougyman)
10
+
5
11
  ## [0.5.4] - 2024-02-04
6
12
 
7
13
  ## [0.5.3] - 2024-02-03
@@ -15,6 +21,8 @@
15
21
  ### Added
16
22
  - Added new changelog management system (changelog-rb) (@bougyman)
17
23
 
18
- [Unreleased]: https://github.com/rubyists/linear-cli/compare/0.5.4...HEAD
19
- [0.5.4]: https://github.com/rubyists/linear-cli/compare/v0.5.3...0.5.4
24
+ [Unreleased]: https://github.com/rubyists/linear-cli/compare/0.6.0...HEAD
25
+ [0.6.0]: https://github.com/rubyists/linear-cli/compare/v0.5.5...0.6.0
26
+ [0.5.5]: https://github.com/rubyists/linear-cli/compare/v0.5.4...v0.5.5
27
+ [0.5.4]: https://github.com/rubyists/linear-cli/compare/v0.5.3...v0.5.4
20
28
  [0.5.3]: https://github.com/rubyists/linear-cli/compare/v0.5.2...v0.5.3
data/Readme.adoc CHANGED
@@ -119,3 +119,23 @@ $ lc i dev CRY-1234
119
119
  ----
120
120
 
121
121
  TIP: You may pass the --dev option to the create subcommand to immediately develop the creted issue.
122
+
123
+ ==== Update an issue
124
+
125
+ All of the update options can work on multiple issues, so long as it's not more than 50
126
+ at a time. You can also use the 'u' alias for 'update', and as always, the 'i' alias for 'issue'.
127
+
128
+ ===== Add a comment to one or more issues
129
+
130
+ [source,sh]
131
+ ----
132
+ $ lc issue update --comment "Here is a comment" CRY-1234
133
+ ----
134
+
135
+ ===== Close one or many issues
136
+
137
+ [source,sh]
138
+ ----
139
+ $ lc i u --close --reason "These were closable" CRY-1234 CRY-2
140
+ ----
141
+
@@ -0,0 +1,4 @@
1
+ type: Added
2
+ title: >
3
+ Added lclose alias and 'issue update' subcommand
4
+ author: bougyman
@@ -0,0 +1 @@
1
+ date: 2024-02-04
@@ -0,0 +1 @@
1
+ date: 2024-02-04
data/exe/lclose ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ exec File.join(__dir__, 'lclose.sh'), *ARGV
data/exe/lclose.sh ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ if [[ "$*" =~ "--help" ]]
3
+ then
4
+ printf "This wrapper adds the --close option to the 'issue update' command.\n" >&2
5
+ printf "It is used to close one or many issues. The issues are specified by their ID/slugs.\n" >&2
6
+ printf "For closing multiple issues, you really want to pass --reason so you do not get prompted for each issue.\n\n" >&2
7
+ exec lc issue update --help
8
+ fi
9
+ exec lc issue update --close "$@"
@@ -47,6 +47,20 @@ module Rubyists
47
47
  ask_for_team
48
48
  end
49
49
 
50
+ def reason_for(reason = nil, four: nil)
51
+ return reason if reason
52
+
53
+ question = four ? "Reason for #{four}:" : 'Reason:'
54
+ prompt.ask(question)
55
+ end
56
+
57
+ def completed_state_for(thingy)
58
+ states = thingy.completed_states
59
+ return states.first if states.size == 1
60
+
61
+ prompt.select('Choose a completed state', states.to_h { |s| [s.name, s.id] })
62
+ end
63
+
50
64
  def description_for(description = nil)
51
65
  return description if description
52
66
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rubyists
4
4
  module Linear
5
- VERSION = '0.5.4'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -0,0 +1,37 @@
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
+ Update = Class.new Dry::CLI::Command
15
+ # The Update class is a Dry::CLI::Command to update an issue
16
+ class Update
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 'Update an issue'
21
+ argument :issue_ids, type: :array, required: true, desc: 'Issue IDs (i.e. ISS-1)'
22
+ option :comment, type: :string, aliases: ['--message'], desc: 'Comment to add to the issue'
23
+ option :pr, type: :boolean, aliases: ['--pull-request'], default: false, desc: 'Create a pull request'
24
+ option :close, type: :boolean, default: false, desc: 'Close the issue'
25
+ option :reason, type: :string, aliases: ['--close-reason'], desc: 'Reason for closing the issue'
26
+
27
+ def call(issue_ids:, **options)
28
+ logger.debug('Updating issues', issue_ids:, options:)
29
+ Rubyists::Linear::Issue.find_all(issue_ids).each do |issue|
30
+ update_issue(issue, **options)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -12,13 +12,40 @@ module Rubyists
12
12
  include CLI::SubCommands
13
13
  # Aliases for Issue commands
14
14
  ALIASES = {
15
- create: %w[c new add], # aliases for the create command
16
- develop: %w[d dev], # aliases for the create command
17
- list: %w[l ls], # aliases for the list command
18
- show: %w[s view v], # aliases for the show command
19
- issue: %w[i issues] # aliases for the main issue command itself
15
+ create: %w[c new add], # aliases for the create command
16
+ develop: %w[d dev], # aliases for the develop command
17
+ list: %w[l ls], # aliases for the list command
18
+ update: %w[u], # aliases for the close command
19
+ issue: %w[i issues] # aliases for the main issue command itself
20
20
  }.freeze
21
21
 
22
+ def issue_comment(issue, comment)
23
+ issue.add_comment(comment)
24
+ prompt.ok("Comment added to #{issue.identifier}")
25
+ end
26
+
27
+ def close_issue(issue, **options)
28
+ reason = reason_for(options[:reason], four: "closing #{issue.identifier} - #{issue.title}")
29
+ issue_comment(issue, reason)
30
+ close_state = completed_state_for(issue)
31
+ issue.close!(state: close_state, trash: options[:trash])
32
+ prompt.ok("#{issue.identifier} was closed")
33
+ end
34
+
35
+ def issue_pr(issue)
36
+ issue.create_pr!
37
+ prompt.ok("Pull request created for #{issue.identifier}")
38
+ end
39
+
40
+ def update_issue(issue, **options)
41
+ issue_comment(issue, options[:comment]) if options[:comment]
42
+ return close_issue(issue, **options) if options[:close]
43
+ return issue_pr(issue) if options[:pr]
44
+
45
+ prompt.warn('No action taken, no options specified')
46
+ prompt.ok('Issue was not updated')
47
+ end
48
+
22
49
  def make_da_issue!(**options)
23
50
  # These *_for methods are defined in Rubyists::Linear::CLI::SubCommands
24
51
  title = title_for options[:title]
@@ -28,7 +55,7 @@ module Rubyists
28
55
  Rubyists::Linear::Issue.create(title:, description:, team:, labels:)
29
56
  end
30
57
 
31
- def gimme_da_issue!(issue_id, me) # rubocop:disable Naming/MethodParameterName
58
+ def gimme_da_issue!(issue_id, me: Rubyists::Linear::User.me) # rubocop:disable Naming/MethodParameterName
32
59
  logger.trace('Looking up issue', issue_id:, me:)
33
60
  issue = Rubyists::Linear::Issue.find(issue_id)
34
61
  if issue.assignee && issue.assignee[:id] == me.id
@@ -5,7 +5,9 @@ require 'semantic_logger'
5
5
  require 'sequel/extensions/inflector'
6
6
 
7
7
  module Rubyists
8
+ # Namespace for Linear
8
9
  module Linear
10
+ L :api, :fragments
9
11
  # Module which provides a base model for Linear models.
10
12
  class BaseModel
11
13
  extend GQLi::DSL
@@ -32,6 +34,20 @@ module Rubyists
32
34
 
33
35
  # Class methods for Linear models.
34
36
  class << self
37
+ def has_one(relation, klass) # rubocop:disable Naming/PredicateName
38
+ define_method relation do
39
+ return instance_variable_get("@#{relation}") if instance_variable_defined?("@#{relation}")
40
+
41
+ instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(data[relation]))
42
+ end
43
+
44
+ define_method "#{relation}=" do |val|
45
+ hash = val.is_a?(Hash) ? val : val.data
46
+ updated_data[relation] = hash
47
+ instance_variable_set("@#{relation}", Rubyists::Linear.const_get(klass).new(hash))
48
+ end
49
+ end
50
+
35
51
  def const_added(const)
36
52
  return unless const == :Base
37
53
 
@@ -108,6 +124,10 @@ module Rubyists
108
124
  data != updated_data
109
125
  end
110
126
 
127
+ def completed_states
128
+ workflow_states.select { |ws| ws.type == 'completed' }
129
+ end
130
+
111
131
  def to_h
112
132
  updated_data
113
133
  end
@@ -5,14 +5,13 @@ require 'gqli'
5
5
  module Rubyists
6
6
  # Namespace for Linear
7
7
  module Linear
8
- L :api
9
- L :fragments
10
- M :base_model
11
- M :user
8
+ M :base_model, :user
12
9
  Issue = Class.new(BaseModel)
13
10
  # The Issue class represents a Linear issue.
14
- class Issue
11
+ class Issue # rubocop:disable Metrics/ClassLength
15
12
  include SemanticLogger::Loggable
13
+ has_one :assignee, :User
14
+ has_one :team, :Team
16
15
 
17
16
  BASIC_FILTER = { completedAt: { null: true } }.freeze
18
17
 
@@ -20,7 +19,6 @@ module Rubyists
20
19
  id
21
20
  identifier
22
21
  title
23
- assignee { ___ User::Base }
24
22
  branchName
25
23
  description
26
24
  createdAt
@@ -28,33 +26,88 @@ module Rubyists
28
26
  end
29
27
 
30
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
+
31
37
  def find(slug)
32
- q = query { issue(id: slug) { ___ Base } }
38
+ q = query { issue(id: slug) { ___ Issue.base_fragment } }
33
39
  data = Api.query(q)
34
40
  raise NotFoundError, "Issue not found: #{slug}" if data.nil?
35
41
 
36
42
  new(data[:issue])
37
43
  end
38
44
 
45
+ def find_all(*slugs)
46
+ slugs.flatten.map { |slug| find(slug) }
47
+ end
48
+
39
49
  def create(title:, description:, team:, labels: [])
40
50
  team_id = team.id
41
51
  label_ids = labels.map(&:id)
42
52
  input = { title:, description:, teamId: team_id }
43
- input.merge!(labelIds: label_ids) unless label_ids.empty?
44
- m = mutation { issueCreate(input:) { issue { ___ Base } } }
45
- data = Api.query(m)
46
- new(data[:issueCreate][:issue])
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)
47
57
  end
48
58
  end
49
59
 
50
- def assign!(user)
60
+ def comment_fragment
61
+ @comment_fragment ||= fragment('Comment', 'Comment') do
62
+ id
63
+ body
64
+ url
65
+ end
66
+ end
67
+
68
+ # Reference for this mutation:
69
+ # https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/inputs/CommentCreateInput
70
+ def add_comment(comment)
71
+ id_for_this = identifier
72
+ comment_frag = comment_fragment
73
+ m = mutation { commentCreate(input: { issueId: id_for_this, body: comment }) { comment { ___ comment_frag } } }
74
+
75
+ query_data = Api.query(m)
76
+ query_data.dig(:commentCreate, :comment)
77
+ self
78
+ end
79
+
80
+ def close_mutation(close_state, trash: false)
51
81
  id_for_this = identifier
52
- m = mutation { issueUpdate(id: id_for_this, input: { assigneeId: user.id }) { issue { ___ Base } } }
53
- data = Api.query(m)
54
- updated = data.dig(:issueUpdate, :issue)
82
+ input = { stateId: close_state.id }
83
+ input[:trash] = true if trash
84
+ mutation { issueUpdate(id: id_for_this, input:) { issue { ___ Issue.base_fragment } } }
85
+ end
86
+
87
+ def close!(state: nil, trash: false)
88
+ logger.warn "Using first completed state found: #{completed_states.first}" if state.nil?
89
+ state ||= completed_states.first
90
+ query_data = Api.query close_mutation(state, trash:)
91
+ updated = query_data.dig(:issueUpdate, :issue)
92
+ raise SmellsBad, "Unknown response for issue close: #{data} (should have :issueUpdate key)" if updated.nil?
93
+
94
+ @data = @updated_data = updated
95
+ self
96
+ end
97
+
98
+ def assign!(user)
99
+ this_id = identifier
100
+ m = mutation { issueUpdate(id: this_id, input: { assigneeId: user.id }) { issue { ___ Issue.base_fragment } } }
101
+ query_data = Api.query(m)
102
+ updated = query_data.dig(:issueUpdate, :issue)
55
103
  raise SmellsBad, "Unknown response for issue update: #{data} (should have :issueUpdate key)" if updated.nil?
56
104
 
57
- Issue.new updated
105
+ @data = @updated_data = updated
106
+ self
107
+ end
108
+
109
+ def workflow_states
110
+ @workflow_states ||= team.workflow_states
58
111
  end
59
112
 
60
113
  def inspection
@@ -62,18 +115,26 @@ module Rubyists
62
115
  end
63
116
 
64
117
  def to_s
65
- basic = format('%<id>-12s %<title>s', id: data[:identifier], title: data[:title])
118
+ basic = format('%<id>-12s %<title>s', id: identifier, title:)
66
119
  return basic unless (name = data.dig(:assignee, :name))
67
120
 
68
121
  format('%<basic>s (%<name>s)', basic:, name:)
69
122
  end
70
123
 
124
+ def parsed_description
125
+ return TTY::Markdown.parse(description) if description && !description.empty?
126
+
127
+ TTY::Markdown.parse(['# No Description For this issue??',
128
+ 'Issues really need description',
129
+ "## What's up with that?"].join("\n"))
130
+ rescue StandardError => e
131
+ logger.error 'Error parsing description', e
132
+ "Description was unparsable: #{description}\n"
133
+ end
134
+
71
135
  def full
72
136
  sep = '-' * to_s.length
73
- format("%<to_s>s\n%<sep>s\n%<description>s\n",
74
- sep:,
75
- to_s:,
76
- description: (TTY::Markdown.parse(data[:description]) rescue 'No Description?')) # rubocop:disable Style/RescueModifier
137
+ format("%<to_s>s\n%<sep>s\n%<description>s\n", sep:, to_s:, description: parsed_description)
77
138
  end
78
139
 
79
140
  def display(options)
@@ -31,14 +31,14 @@ module Rubyists
31
31
  fragment('LabelWithTeams', 'IssueLabel') do
32
32
  ___ Base
33
33
  parent { ___ Base }
34
- team { ___ Team::Base }
34
+ team { ___ Team.base_fragment }
35
35
  end
36
36
  end
37
37
 
38
38
  def self.find_all_by_name(names)
39
39
  q = query do
40
40
  issueLabels(filter: { name: { in: names } }) do
41
- edges { node { ___ Base } }
41
+ edges { node { ___ base_fragment } }
42
42
  end
43
43
  end
44
44
  data = Api.query(q)
@@ -5,8 +5,7 @@ require 'gqli'
5
5
  module Rubyists
6
6
  # Namespace for Linear
7
7
  module Linear
8
- L :api, :fragments
9
- M :base_model, :issue, :user
8
+ M :base_model, :issue, :user, :workflow_state
10
9
  Team = Class.new(BaseModel)
11
10
  # The Issue class represents a Linear issue.
12
11
  class Team
@@ -96,6 +95,26 @@ module Rubyists
96
95
  def display(_options)
97
96
  printf "%s\n", full
98
97
  end
98
+
99
+ def workflow_states_query
100
+ team_id = id
101
+ query do
102
+ team(id: team_id) do
103
+ states do
104
+ nodes { ___ WorkflowState.base_fragment }
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def workflow_states
111
+ return @workflow_states if @workflow_states
112
+
113
+ data = Api.query(workflow_states_query)
114
+ @workflow_states = data.dig(:team, :states, :nodes)&.map do |state|
115
+ WorkflowState.new state
116
+ end
117
+ end
99
118
  end
100
119
  end
101
120
  end
@@ -0,0 +1,33 @@
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
+ WorkflowState = Class.new(BaseModel)
10
+ # The WorkflowState class represents a Linear workflow state.
11
+ class WorkflowState
12
+ include SemanticLogger::Loggable
13
+
14
+ Base = fragment('BaseWorkflowState', 'WorkflowState') do
15
+ id
16
+ name
17
+ position
18
+ type
19
+ description
20
+ createdAt
21
+ updatedAt
22
+ end
23
+
24
+ def to_s
25
+ format('%<name>-12s %<type>s', name:, type:)
26
+ end
27
+
28
+ def inspection
29
+ format('name: "%<name>s" type: "%<type>s"', name:, type:)
30
+ end
31
+ end
32
+ end
33
+ end
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.5.4
4
+ version: 0.6.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-04 00:00:00.000000000 Z
11
+ date: 2024-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -183,6 +183,8 @@ email:
183
183
  - tj@rubyists.com
184
184
  executables:
185
185
  - lc
186
+ - lclose
187
+ - lclose.sh
186
188
  - lcls
187
189
  - lcls.sh
188
190
  extensions: []
@@ -198,8 +200,13 @@ files:
198
200
  - changelog/0.5.3/changed_default_branch_to_use_upstream_default_branch_name.yml
199
201
  - changelog/0.5.3/tag.yml
200
202
  - changelog/0.5.4/tag.yml
203
+ - changelog/0.5.5/added_lclose_alias_and_issue_update_subcommand.yml
204
+ - changelog/0.5.5/tag.yml
205
+ - changelog/0.6.0/tag.yml
201
206
  - changelog/unreleased/.gitkeep
202
207
  - exe/lc
208
+ - exe/lclose
209
+ - exe/lclose.sh
203
210
  - exe/lcls
204
211
  - exe/lcls.sh
205
212
  - lib/linear.rb
@@ -215,6 +222,7 @@ files:
215
222
  - lib/linear/commands/issue/develop.rb
216
223
  - lib/linear/commands/issue/list.rb
217
224
  - lib/linear/commands/issue/take.rb
225
+ - lib/linear/commands/issue/update.rb
218
226
  - lib/linear/commands/team.rb
219
227
  - lib/linear/commands/team/list.rb
220
228
  - lib/linear/commands/whoami.rb
@@ -225,6 +233,7 @@ files:
225
233
  - lib/linear/models/label.rb
226
234
  - lib/linear/models/team.rb
227
235
  - lib/linear/models/user.rb
236
+ - lib/linear/models/workflow_state.rb
228
237
  - lib/linear/version.rb
229
238
  - linear-cli.gemspec
230
239
  - sig/linear/cli.rbs