From 2940f8c36a8677ce8c202b72963d1a6643a8d27a Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Mon, 5 Feb 2024 13:31:39 -0600 Subject: [PATCH 1/3] Bump linear-cli to 0.7.6 --- lib/linear/cli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linear/cli/version.rb b/lib/linear/cli/version.rb index c5ad61f..be29c7b 100644 --- a/lib/linear/cli/version.rb +++ b/lib/linear/cli/version.rb @@ -2,6 +2,6 @@ module Rubyists module Linear - VERSION = '0.7.5' + VERSION = '0.7.6' end end From eae73046147696c210938e12acdfa07138e042af Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Mon, 5 Feb 2024 14:11:35 -0600 Subject: [PATCH 2/3] Formatting of parens, and pr create skeleton --- Gemfile.lock | 2 +- lib/linear/commands/issue.rb | 44 +++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1b1c90f..15b3720 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/lib/linear/commands/issue.rb b/lib/linear/commands/issue.rb index aa9599f..f3ff22d 100644 --- a/lib/linear/commands/issue.rb +++ b/lib/linear/commands/issue.rb @@ -26,16 +26,16 @@ module Issue }.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) @@ -44,14 +44,16 @@ 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 issue_pr(issue, **options) + title = options[:title] || pr_title_for(issue) + body = options[:description] || pr_description_for(issue) + issue.create_pr!(title:, body:) + prompt.ok "Pull request created for #{issue.identifier}" end def update_issue(issue, **options) @@ -60,29 +62,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 From ac54718ed38b7116bf3920957b80e7af96dd8b06 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Mon, 5 Feb 2024 19:48:03 -0600 Subject: [PATCH 3/3] Adds pr command --- exe/lc.sh | 7 +++-- lib/linear/commands/issue.rb | 53 +++++++++++++++++++++++++++++++-- lib/linear/commands/issue/pr.rb | 38 +++++++++++++++++++++++ 3 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 lib/linear/commands/issue/pr.rb diff --git a/exe/lc.sh b/exe/lc.sh index 1f1427c..5999c49 100755 --- a/exe/lc.sh +++ b/exe/lc.sh @@ -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 diff --git a/lib/linear/commands/issue.rb b/lib/linear/commands/issue.rb index f3ff22d..77478b5 100644 --- a/lib/linear/commands/issue.rb +++ b/lib/linear/commands/issue.rb @@ -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 @@ -15,6 +17,7 @@ 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 = { @@ -22,6 +25,7 @@ module Issue 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 @@ -49,11 +53,56 @@ def close_issue(issue, **options) prompt.ok "#{issue.identifier} was #{done}" end + 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) - issue.create_pr!(title:, body:) - prompt.ok "Pull request created for #{issue.identifier}" + create_pr!(title:, body:) end def update_issue(issue, **options) diff --git a/lib/linear/commands/issue/pr.rb b/lib/linear/commands/issue/pr.rb new file mode 100644 index 0000000..1b5eb8e --- /dev/null +++ b/lib/linear/commands/issue/pr.rb @@ -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