linear-cli 0.4.5 → 0.5.0

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: 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