linear-cli 0.4.5 → 0.5.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: cb92c9146c83ed3475e22b56f91f6a9c99fe3728a1c93578e63cccaa901b874d
4
- data.tar.gz: 6fb9075b1306defe1282f011a788ae88542b3684c466afb40367bc2a25eb3a61
3
+ metadata.gz: 4d1133e666c4896922694d8dd9a8f3c9eeb99de63da400ae14ba6c06eaea6df1
4
+ data.tar.gz: a73559f2a87113d05cc089cac0b8ddb48b124da991b9e2fac8bed68d11537501
5
5
  SHA512:
6
- metadata.gz: 7f3c6a39b2e046aa35a16790715ab34dfd79ac08c7696b25b634d9a2a8e6068b477d50baabd91000f51c713eda3eb8b36311d55a649e9e15d76448bcc1128e7d
7
- data.tar.gz: 22ac8fa5fa283eaa2f22a7f8c22a3e7dcad62031a3421062511afef00148e8c7307d7a249700e14d966ec2964f279de97475ea1afdad98ce996da1a49b48cff8
6
+ metadata.gz: 3df7cfa7fccf9cefc7a2a9bf3204f22c55265612380baadf01df2115305bf5737c5a777259a2ad154cb60890f4078ab67a85aac84f9e2dcf185f2d0d2cf6d529
7
+ data.tar.gz: 41ddafcb546b3760a93b7880ebc877d4d6813d9f93545e1052063f97042df62d5d5427f96151d2e374f934ec266c283b862072181486a51b6112d751ad61a72b
data/Readme.adoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = Linear Command line interface
2
2
  :toc: right
3
- :toclevels: 2
3
+ :toclevels: 3
4
4
  :sectanchors:
5
5
  :icons: font
6
6
  :experimental:
@@ -15,21 +15,25 @@ module Rubyists
15
15
  end
16
16
  end
17
17
 
18
- def ask_for_team # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
18
+ def choose_a_team!(teams)
19
+ prompt.on(:keypress) do |event|
20
+ prompt.trigger(:keydown) if event.value == 'j'
21
+ prompt.trigger(:keyup) if event.value == 'k'
22
+ end
23
+ key = prompt.select('Choose a team', teams.to_h { |t| [t.name, t.key] })
24
+ Rubyists::Linear::Team.find key
25
+ end
26
+
27
+ def ask_for_team
19
28
  teams = Rubyists::Linear::Team.mine
20
29
  if teams.size == 1
21
30
  logger.info('Only one team found, using it', team: teams.first.name)
22
31
  teams.first
23
32
  elsif teams.empty?
24
- logger.error('No teams found for you. Please join a team or pass an existing team name.')
25
- raise SmellsBad, 'No team given and none found for you'
33
+ logger.error('No teams found for you. Please join a team or pass an existing team ID.')
34
+ raise SmellsBad, 'No team given and none found for you (try joining a team or use a team id from `lc teams --no-mine`)' # rubocop:disable Layout/LineLength
26
35
  else
27
- prompt.on(:keypress) do |event|
28
- prompt.trigger(:keydown) if event.value == 'j'
29
- prompt.trigger(:keyup) if event.value == 'k'
30
- end
31
- key = prompt.select('Choose a team', teams.to_h { |t| [t.name, t.key] })
32
- Rubyists::Linear::Team.find key
36
+ choose_a_team! teams
33
37
  end
34
38
  end
35
39
 
@@ -58,8 +62,53 @@ module Rubyists
58
62
  def labels_for(team, labels = nil)
59
63
  return Rubyists::Linear::Label.find_all_by_name(labels.map(&:strip)) if labels
60
64
 
65
+ prompt.on(:keypress) do |event|
66
+ prompt.trigger(:keydown) if event.value == 'j'
67
+ prompt.trigger(:keyup) if event.value == 'k'
68
+ end
61
69
  prompt.multi_select('Labels:', team.labels.to_h { |t| [t.name, t] })
62
70
  end
71
+
72
+ def cut_branch!(branch_name)
73
+ if current_branch != default_branch
74
+ 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
75
+ end
76
+ git.branch(branch_name)
77
+ end
78
+
79
+ def branch_for(branch_name)
80
+ logger.trace('Looking for branch', branch_name:)
81
+ existing = git.branches[branch_name]
82
+ return cut_branch!(branch_name) unless existing
83
+
84
+ logger.trace('Branch found', branch: existing&.name)
85
+ existing
86
+ end
87
+
88
+ def current_branch
89
+ git.current_branch
90
+ end
91
+
92
+ # Horrible way to do this, but it is working for now
93
+ def pull_or_push_new_branch!(branch_name)
94
+ git.pull
95
+ rescue Git::FailedError
96
+ prompt.warn("Upstream branch not found, pushing local #{branch_name} to origin")
97
+ git.push('origin', branch_name)
98
+ `git branch --set-upstream-to=origin/#{branch_name} #{branch_name}`
99
+ prompt.ok("Set upstream to origin/#{branch_name}")
100
+ end
101
+
102
+ def git
103
+ @git ||= Git.open('.')
104
+ rescue Git::Repository::NoRepositoryError => e
105
+ logger.error('Your current directory is not a git repository!', error: e)
106
+ exit 121
107
+ end
108
+
109
+ def default_branch
110
+ @default_branch ||= Git.default_branch git.repo.path
111
+ end
63
112
  end
64
113
  end
65
114
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rubyists
4
4
  module Linear
5
- VERSION = '0.4.5'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'semantic_logger'
4
+ require_relative '../issue'
4
5
 
5
6
  module Rubyists
6
7
  # Namespace for Linear
@@ -14,12 +15,13 @@ module Rubyists
14
15
  class Create
15
16
  include SemanticLogger::Loggable
16
17
  include Rubyists::Linear::CLI::CommonOptions
17
- include Rubyists::Linear::CLI::Issue # for #gimme_da_issue and other methods
18
- desc 'Create a new issue'
18
+ include Rubyists::Linear::CLI::Issue # for #gimme_da_issue! and other Issue methods
19
+ desc 'Create a new issue'
19
20
  option :title, type: :string, aliases: ['-t'], desc: 'Issue Title'
20
- option :team, type: :string, aliases: ['-T'], desc: 'Team Identifier'
21
21
  option :description, type: :string, aliases: ['-d'], desc: 'Issue Description'
22
+ option :team, type: :string, aliases: ['-T'], desc: 'Team Identifier'
22
23
  option :labels, type: :array, aliases: ['-l'], desc: 'Labels for the issue (Comma separated list)'
24
+ option :develop, type: :boolean, aliases: ['-D', '--dev'], desc: 'Start development after creating the issue'
23
25
 
24
26
  def call(**options)
25
27
  logger.debug('Creating issue', options:)
@@ -27,6 +29,7 @@ module Rubyists
27
29
  logger.debug('Issue created', issue:)
28
30
  prompt.yes?('Do you want to take this issue?') && gimme_da_issue!(issue.id, User.me)
29
31
  display issue, options
32
+ Rubyists::Linear::CLI::Issue::Develop.new.call(issue_id: issue.id, **options) if options[:develop]
30
33
  end
31
34
  end
32
35
  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
+ Develop = Class.new Dry::CLI::Command
15
+ # The Develop class is a Dry::CLI::Command to start/update development status of an issue
16
+ class Develop
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 'Start or update development status of an issue'
21
+ argument :issue_id, required: true, desc: 'The Issue (i.e. ISS-1)'
22
+
23
+ def call(issue_id:, **options)
24
+ logger.debug('Developing issue', options:)
25
+ issue = gimme_da_issue!(issue_id, Rubyists::Linear::User.me)
26
+ branch_name = issue.branchName
27
+ branch = branch_for(branch_name)
28
+ branch.checkout
29
+ prompt.ok "Checked out branch #{branch_name}"
30
+ pull_or_push_new_branch!(branch_name)
31
+ prompt.ok 'Ready to develop!'
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -13,8 +13,9 @@ module Rubyists
13
13
  # Aliases for Issue commands
14
14
  ALIASES = {
15
15
  create: %w[c new add], # aliases for the create command
16
+ develop: %w[d dev], # aliases for the create command
16
17
  list: %w[l ls], # aliases for the list command
17
- show: %w[s view v display d], # aliases for the show command
18
+ show: %w[s view v], # aliases for the show command
18
19
  issue: %w[i issues] # aliases for the main issue command itself
19
20
  }.freeze
20
21
 
@@ -28,10 +29,16 @@ module Rubyists
28
29
  end
29
30
 
30
31
  def gimme_da_issue!(issue_id, me) # rubocop:disable Naming/MethodParameterName
32
+ logger.trace('Looking up issue', issue_id:, me:)
31
33
  issue = Rubyists::Linear::Issue.find(issue_id)
32
- logger.debug 'Taking issue', issue:, assignee: me
34
+ if issue.assignee && issue.assignee[:id] == me.id
35
+ prompt.say("You are already assigned #{issue_id}")
36
+ return issue
37
+ end
38
+
39
+ prompt.say("Assigning issue #{issue_id} to ya")
33
40
  updated = issue.assign! me
34
- logger.debug 'Issue taken', issue: updated
41
+ logger.trace 'Issue taken', issue: updated
35
42
  updated
36
43
  end
37
44
  end
@@ -21,6 +21,7 @@ module Rubyists
21
21
  identifier
22
22
  title
23
23
  assignee { ___ User::Base }
24
+ branchName
24
25
  description
25
26
  createdAt
26
27
  updatedAt
@@ -12,14 +12,29 @@ module Rubyists
12
12
  class Label
13
13
  include SemanticLogger::Loggable
14
14
 
15
+ PLURAL = :issueLabels
15
16
  Base = fragment('BaseLabel', 'IssueLabel') do
16
17
  id
17
18
  description
18
19
  name
20
+ isGroup
19
21
  createdAt
20
22
  updatedAt
21
23
  end
22
24
 
25
+ def self.base_fragment # rubocop:disable Metrics/AbcSize
26
+ define_method(:team) { updated_data[:team] }
27
+ define_method(:team=) { |val| updated_data[:team] = val }
28
+ define_method(:parent) { updated_data[:parent] }
29
+ define_method(:parent=) { |val| updated_data[:parent] = val }
30
+
31
+ fragment('LabelWithTeams', 'IssueLabel') do
32
+ ___ Base
33
+ parent { ___ Base }
34
+ team { ___ Team::Base }
35
+ end
36
+ end
37
+
23
38
  def self.find_all_by_name(names)
24
39
  q = query do
25
40
  issueLabels(filter: { name: { in: names } }) do
@@ -46,7 +61,7 @@ module Rubyists
46
61
  end
47
62
 
48
63
  def full
49
- format('%<to_s>-10s %<description>s', description: , to_s:)
64
+ format('%<to_s>-10s %<description>s', description:, to_s:)
50
65
  end
51
66
 
52
67
  def display(_options)
@@ -13,7 +13,13 @@ module Rubyists
13
13
  include SemanticLogger::Loggable
14
14
 
15
15
  # TODO: Make this configurable
16
- IgnoredLabels = [/-ios$/, /-android$/].freeze # rubocop:disable Naming/ConstantName
16
+ BaseFilter = { # rubocop:disable Naming/ConstantName
17
+ and: [
18
+ { name: { notEndsWith: ' Releases' } },
19
+ { name: { notEndsWith: '-ios' } },
20
+ { name: { notEndsWith: '-android' } }
21
+ ]
22
+ }.freeze
17
23
 
18
24
  Base = fragment('BaseTeam', 'Team') do
19
25
  description
@@ -51,26 +57,26 @@ module Rubyists
51
57
  team_id = id
52
58
  query do
53
59
  team(id: team_id) do
54
- labels do
55
- nodes { ___ Label::Base }
60
+ labels(first: 100, filter: BaseFilter) do
61
+ nodes { ___ Label.base_fragment }
56
62
  end
57
63
  end
58
64
  end
59
65
  end
60
66
 
61
- def label_filter(labels = nil)
62
- return [] unless labels
63
-
64
- labels.reject { |label| IgnoredLabels.detect { |i| label.name.match? i } }
67
+ def label_groups
68
+ @label_groups ||= []
65
69
  end
66
70
 
67
- def labels
68
- return @labels if @labels && !@labels.empty?
71
+ def labels # rubocop:disable Metrics/CyclomaticComplexity
72
+ return @labels if @labels
73
+
74
+ @labels = Api.query(label_query).dig(:team, :labels, :nodes)&.map do |label|
75
+ label_groups << Label.new(label) if label[:isGroup]
76
+ next if label[:isGroup] || label[:parent]
69
77
 
70
- all = Api.query(label_query).dig(:team, :labels, :nodes)&.map do |label|
71
78
  Label.new label
72
- end
73
- @labels = label_filter(all)
79
+ end&.compact
74
80
  end
75
81
 
76
82
  def members
data/linear-cli.gemspec CHANGED
@@ -36,8 +36,10 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'base64', '~> 0.2'
37
37
  spec.add_dependency 'dry-cli', '~> 1.0'
38
38
  spec.add_dependency 'dry-cli-completion', '~> 1.0'
39
+ spec.add_dependency 'git', '~> 1.5'
39
40
  spec.add_dependency 'gqli', '~> 1.2'
40
41
  spec.add_dependency 'httpx', '~> 1.2'
42
+ spec.add_dependency 'octokit', '~> 5.0'
41
43
  spec.add_dependency 'semantic_logger', '~> 4.0'
42
44
  spec.add_dependency 'sequel', '~> 5.0'
43
45
  spec.add_dependency 'sqlite3', '~> 1.7'
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.5
4
+ version: 0.5.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-02 00:00:00.000000000 Z
11
+ date: 2024-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: git
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.5'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: gqli
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '1.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: octokit
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: semantic_logger
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -177,6 +205,7 @@ files:
177
205
  - lib/linear/cli/watcher.rb
178
206
  - lib/linear/commands/issue.rb
179
207
  - lib/linear/commands/issue/create.rb
208
+ - lib/linear/commands/issue/develop.rb
180
209
  - lib/linear/commands/issue/list.rb
181
210
  - lib/linear/commands/issue/take.rb
182
211
  - lib/linear/commands/team.rb