Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
linear-cli (0.7.5)
linear-cli (0.7.6)
base64 (~> 0.2)
dry-cli (~> 1.0)
dry-cli-completion (~> 1.0)
Expand Down
7 changes: 4 additions & 3 deletions exe/lc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ then
linear-cli "$@" 2>&1|sed 's/linear-cli/lc/g'
exit 0
fi
if ! linear-cli "$@"
then
printf "lc: linear-cli failed\n" >&2
linear-cli "$@"
result=$?
if [ $result -gt 1 ]; then
printf "lc: linear-cli failed %s\n" $result >&2
lc "$@" --help 2>&1
exit 1
fi
2 changes: 1 addition & 1 deletion lib/linear/cli/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Rubyists
module Linear
VERSION = '0.7.5'
VERSION = '0.7.6'
end
end
93 changes: 72 additions & 21 deletions lib/linear/commands/issue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# as well as other helpers which are used in multiple commands and subcommands
# This is also where the #prompt method is defined, which is used to display messages to the user and get input
require_relative '../cli/sub_commands'
require 'tty-editor'
require 'git'

module Rubyists
module Linear
Expand All @@ -15,27 +17,29 @@ module Issue
include CLI::SubCommands

DESCRIPTION = 'Manage issues'
ALLOWED_PR_TYPES = 'bug|fix|sec(urity)|feat(ure)|chore|refactor|test|docs|style|ci|perf'

# Aliases for Issue commands
ALIASES = {
create: %w[c new add], # aliases for the create command
develop: %w[d dev], # aliases for the develop command
list: %w[l ls], # aliases for the list command
update: %w[u], # aliases for the close command
pr: %w[pull-request], # aliases for the pr command
issue: %w[i issues] # aliases for the main issue command itself
}.freeze

def issue_comment(issue, comment)
issue.add_comment(comment)
prompt.ok("Comment added to #{issue.identifier}")
issue.add_comment comment
prompt.ok "Comment added to #{issue.identifier}"
end

def cancel_issue(issue, **options)
reason = reason_for(options[:reason], four: "cancelling #{issue.identifier} - #{issue.title}")
issue_comment(issue, reason)
issue_comment issue, reason
cancel_state = cancel_state_for(issue)
issue.close!(state: cancel_state, trash: options[:trash])
prompt.ok("#{issue.identifier} was cancelled")
issue.close! state: cancel_state, trash: options[:trash]
prompt.ok "#{issue.identifier} was cancelled"
end

def close_issue(issue, **options)
Expand All @@ -44,14 +48,61 @@ def close_issue(issue, **options)
done = cancelled ? 'cancelled' : 'closed'
workflow_state = cancelled ? cancelled_state_for(issue) : completed_state_for(issue)
reason = reason_for(options[:reason], four: "#{doing} #{issue.identifier} - #{issue.title}")
issue_comment(issue, reason)
issue.close!(state: workflow_state, trash: options[:trash])
prompt.ok("#{issue.identifier} was #{done}")
issue_comment issue, reason
issue.close! state: workflow_state, trash: options[:trash]
prompt.ok "#{issue.identifier} was #{done}"
end

def issue_pr(issue)
issue.create_pr!
prompt.ok("Pull request created for #{issue.identifier}")
def pr_type_for(issue)
proposed_type = issue.title.match(/^(#{ALLOWED_PR_TYPES})/i)
return proposed_type[1].downcase if proposed_type

prompt.select('What type of PR is this?', %w[fix feature chore refactor test docs style ci perf security])
end

def pr_scope_for(title)
proposed_scope = title.match(/^\w+\(([^\)]+)\)/)
return proposed_scope[1].downcase if proposed_scope

scope = prompt.ask('What is the scope of this PR?', default: 'none')
return nil if scope.empty? && scope == 'none'

scope
end

def pr_title_for(issue)
proposed = [pr_type_for(issue)]
proposed_scope = pr_scope_for(issue.title)
proposed << "(#{proposed_scope})" if proposed_scope
summary = issue.title.sub(/(?:#{ALLOWED_PR_TYPES})(\([^)]+\))? /, '')
proposed << ": #{issue.identifier} - #{summary}"
prompt.ask("Title for PR for #{issue.identifier} - #{summary}", default: proposed.join)
end

def pr_description_for(issue)
tmpfile = Tempfile.new([issue.identifier, '.md'], Rubyists::Linear.tmpdir)
# TODO: Look up templates
proposed = "# Context\n\n#{issue.description}\n\n## Issue\n\n#{issue.identifier}\n\n# Solution\n\n# Testing\n\n# Notes\n\n" # rubocop:disable Layout/LineLength
tmpfile.write(proposed) && tmpfile.close
desc = TTY::Editor.open(tmpfile.path)
return tmpfile if desc

File.open(tmpfile.path, 'w+') do |file|
file.puts prompt.ask("Description for PR for #{issue.identifier} - #{issue.title}", default: proposed)
end
tmpfile
end

def create_pr!(title:, body:)
return `gh pr create -a @me --title "#{title}" --body-file "#{body.path}"` if body.respond_to?(:path)

`gh pr create -a @me --title "#{title}" --body "#{body}"`
end

def issue_pr(issue, **options)
title = options[:title] || pr_title_for(issue)
body = options[:description] || pr_description_for(issue)
create_pr!(title:, body:)
end

def update_issue(issue, **options)
Expand All @@ -60,29 +111,29 @@ def update_issue(issue, **options)
return issue_pr(issue) if options[:pr]
return if options[:comment]

prompt.warn('No action taken, no options specified')
prompt.ok('Issue was not updated')
prompt.warn 'No action taken, no options specified'
prompt.ok 'Issue was not updated'
end

def make_da_issue!(**options)
# These *_for methods are defined in Rubyists::Linear::CLI::SubCommands
title = title_for options[:title]
description = description_for options[:description]
team = team_for options[:team]
labels = labels_for team, options[:labels]
title = title_for(options[:title])
description = description_for(options[:description])
team = team_for(options[:team])
labels = labels_for(team, options[:labels])
Rubyists::Linear::Issue.create(title:, description:, team:, labels:)
end

def gimme_da_issue!(issue_id, me: Rubyists::Linear::User.me) # rubocop:disable Naming/MethodParameterName
logger.trace('Looking up issue', issue_id:, me:)
issue = Rubyists::Linear::Issue.find(issue_id)
if issue.assignee && issue.assignee[:id] == me.id
prompt.say("You are already assigned #{issue_id}")
if issue.assignee && issue.assignee.id == me.id
prompt.say "You are already assigned #{issue_id}"
return issue
end

prompt.say("Assigning issue #{issue_id} to ya")
updated = issue.assign! me
prompt.say "Assigning issue #{issue_id} to ya"
updated = issue.assign!(me)
logger.trace 'Issue taken', issue: updated
updated
end
Expand Down
38 changes: 38 additions & 0 deletions lib/linear/commands/issue/pr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

require 'semantic_logger'
require 'git'
require_relative '../issue'

module Rubyists
# Namespace for Linear
module Linear
M :issue, :user, :label
# Namespace for CLI
module CLI
module Issue
Pr = Class.new Dry::CLI::Command
# The Develop class is a Dry::CLI::Command to start/update development status of an issue
class Pr
include SemanticLogger::Loggable
include Rubyists::Linear::CLI::CommonOptions
include Rubyists::Linear::CLI::Issue # for #gimme_da_issue! and other Issue methods
desc 'Create a PR for an issue and push it to the remote'
argument :issue_id, required: true, desc: 'The Issue (i.e. CRY-1)'
option :title, required: false, desc: 'The title of the PR'
option :description, required: false, desc: 'The description of the PR'

def call(issue_id:, **options)
logger.debug('Creating PR for issue issue', options:)
issue = gimme_da_issue!(issue_id, me: Rubyists::Linear::User.me)
branch_name = issue.branchName
branch = branch_for(branch_name)
branch.checkout
prompt.ok "Checked out branch #{branch_name}"
issue_pr(issue, **options)
end
end
end
end
end
end