Skip to content

Commit a3a4b32

Browse files
committed
Adds the issue develop command, and --dev option to issue create
1 parent e28487d commit a3a4b32

10 files changed

Lines changed: 154 additions & 19 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ group :development, :test do
1616
gem 'rubocop-rake', require: false
1717
gem 'rubocop-rspec', require: false
1818
end
19+

Gemfile.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ PATH
55
base64 (~> 0.2)
66
dry-cli (~> 1.0)
77
dry-cli-completion (~> 1.0)
8+
git (~> 1.5)
89
gqli (~> 1.2)
910
httpx (~> 1.2)
11+
octokit (~> 5.0)
1012
semantic_logger (~> 4.0)
1113
sequel (~> 5.0)
1214
sqlite3 (~> 1.7)
@@ -66,11 +68,18 @@ GEM
6668
dry-cli (1.0.0)
6769
dry-cli-completion (1.0.0)
6870
completely (~> 0.5)
71+
faraday (2.9.0)
72+
faraday-net_http (>= 2.0, < 3.2)
73+
faraday-net_http (3.1.0)
74+
net-http
6975
ffi (1.16.3)
7076
ffi-compiler (1.0.1)
7177
ffi (>= 1.0.0)
7278
rake
7379
gem-release (2.2.2)
80+
git (1.19.1)
81+
addressable (~> 2.8)
82+
rchardet (~> 1.8)
7483
gqli (1.2.0)
7584
hashie (> 3.0)
7685
http (> 0.8, < 6.0)
@@ -102,6 +111,11 @@ GEM
102111
docopt_ng (~> 0.7, >= 0.7.1)
103112
multi_json (1.15.0)
104113
multi_test (1.1.0)
114+
net-http (0.4.1)
115+
uri
116+
octokit (5.6.1)
117+
faraday (>= 1, < 3)
118+
sawyer (~> 0.9)
105119
parallel (1.24.0)
106120
parser (3.3.0.5)
107121
ast (~> 2.4.1)
@@ -118,6 +132,7 @@ GEM
118132
racc (1.7.3)
119133
rainbow (3.1.1)
120134
rake (13.1.0)
135+
rchardet (1.8.0)
121136
regexp_parser (2.9.0)
122137
rexml (3.2.6)
123138
rouge (4.2.0)
@@ -158,6 +173,9 @@ GEM
158173
rubocop-capybara (~> 2.17)
159174
rubocop-factory_bot (~> 2.22)
160175
ruby-progressbar (1.13.0)
176+
sawyer (0.9.2)
177+
addressable (>= 2.3.5)
178+
faraday (>= 0.17.3, < 3)
161179
semantic_logger (4.15.0)
162180
concurrent-ruby (~> 1.0)
163181
sequel (5.76.0)
@@ -192,6 +210,7 @@ GEM
192210
tty-screen (0.8.2)
193211
unicode-display_width (2.5.0)
194212
unicode_utils (1.4.0)
213+
uri (0.13.0)
195214
wisper (2.0.1)
196215

197216
PLATFORMS

lib/linear/cli/sub_commands.rb

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def ask_for_team
3030
logger.info('Only one team found, using it', team: teams.first.name)
3131
teams.first
3232
elsif teams.empty?
33-
logger.error('No teams found for you. Please join a team or pass an existing team name.')
34-
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
3535
else
3636
choose_a_team! teams
3737
end
@@ -62,8 +62,53 @@ def title_for(title = nil)
6262
def labels_for(team, labels = nil)
6363
return Rubyists::Linear::Label.find_all_by_name(labels.map(&:strip)) if labels
6464

65+
prompt.on(:keypress) do |event|
66+
prompt.trigger(:keydown) if event.value == 'j'
67+
prompt.trigger(:keyup) if event.value == 'k'
68+
end
6569
prompt.multi_select('Labels:', team.labels.to_h { |t| [t.name, t] })
6670
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
67112
end
68113
end
69114
end

lib/linear/commands/issue.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ module Issue
1313
# Aliases for Issue commands
1414
ALIASES = {
1515
create: %w[c new add], # aliases for the create command
16+
develop: %w[d dev], # aliases for the create command
1617
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
1819
issue: %w[i issues] # aliases for the main issue command itself
1920
}.freeze
2021

@@ -28,10 +29,16 @@ def make_da_issue!(**options)
2829
end
2930

3031
def gimme_da_issue!(issue_id, me) # rubocop:disable Naming/MethodParameterName
32+
logger.trace('Looking up issue', issue_id:, me:)
3133
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")
3340
updated = issue.assign! me
34-
logger.debug 'Issue taken', issue: updated
41+
logger.trace 'Issue taken', issue: updated
3542
updated
3643
end
3744
end

lib/linear/commands/issue/create.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,20 @@ class Create
1616
include SemanticLogger::Loggable
1717
include Rubyists::Linear::CLI::CommonOptions
1818
include Rubyists::Linear::CLI::Issue # for #gimme_da_issue! and other Issue methods
19-
desc 'Create a new issue'
19+
desc 'Create a new issue'
2020
option :title, type: :string, aliases: ['-t'], desc: 'Issue Title'
2121
option :description, type: :string, aliases: ['-d'], desc: 'Issue Description'
2222
option :team, type: :string, aliases: ['-T'], desc: 'Team Identifier'
2323
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'
2425

2526
def call(**options)
2627
logger.debug('Creating issue', options:)
2728
issue = make_da_issue!(**options)
2829
logger.debug('Issue created', issue:)
2930
prompt.yes?('Do you want to take this issue?') && gimme_da_issue!(issue.id, User.me)
3031
display issue, options
32+
Rubyists::Linear::CLI::Issue::Develop.new.call(issue_id: issue.id, **options) if options[:develop]
3133
end
3234
end
3335
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/linear/models/issue.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Issue
2121
identifier
2222
title
2323
assignee { ___ User::Base }
24+
branchName
2425
description
2526
createdAt
2627
updatedAt

lib/linear/models/label.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,29 @@ module Linear
1212
class Label
1313
include SemanticLogger::Loggable
1414

15+
PLURAL = :issueLabels
1516
Base = fragment('BaseLabel', 'IssueLabel') do
1617
id
1718
description
1819
name
20+
isGroup
1921
createdAt
2022
updatedAt
2123
end
2224

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+
2338
def self.find_all_by_name(names)
2439
q = query do
2540
issueLabels(filter: { name: { in: names } }) do
@@ -46,7 +61,7 @@ def to_s
4661
end
4762

4863
def full
49-
format('%<to_s>-10s %<description>s', description: , to_s:)
64+
format('%<to_s>-10s %<description>s', description:, to_s:)
5065
end
5166

5267
def display(_options)

lib/linear/models/team.rb

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ class Team
1313
include SemanticLogger::Loggable
1414

1515
# 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
1723

1824
Base = fragment('BaseTeam', 'Team') do
1925
description
@@ -51,26 +57,26 @@ def label_query
5157
team_id = id
5258
query do
5359
team(id: team_id) do
54-
labels do
55-
nodes { ___ Label::Base }
60+
labels(first: 100, filter: BaseFilter) do
61+
nodes { ___ Label.base_fragment }
5662
end
5763
end
5864
end
5965
end
6066

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 ||= []
6569
end
6670

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]
6977

70-
all = Api.query(label_query).dig(:team, :labels, :nodes)&.map do |label|
7178
Label.new label
72-
end
73-
@labels = label_filter(all)
79+
end&.compact
7480
end
7581

7682
def members

linear-cli.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ Gem::Specification.new do |spec|
3636
spec.add_dependency 'base64', '~> 0.2'
3737
spec.add_dependency 'dry-cli', '~> 1.0'
3838
spec.add_dependency 'dry-cli-completion', '~> 1.0'
39+
spec.add_dependency 'git', '~> 1.5'
3940
spec.add_dependency 'gqli', '~> 1.2'
4041
spec.add_dependency 'httpx', '~> 1.2'
42+
spec.add_dependency 'octokit', '~> 5.0'
4143
spec.add_dependency 'semantic_logger', '~> 4.0'
4244
spec.add_dependency 'sequel', '~> 5.0'
4345
spec.add_dependency 'sqlite3', '~> 1.7'

0 commit comments

Comments
 (0)