Sync merger.rb and redmine-backporter.rb

This commit is contained in:
Hiroshi SHIBATA 2024-05-30 10:34:29 +09:00
parent a777087be6
commit e96ba90fd6
No known key found for this signature in database
GPG key ID: F9CF13417264FAC2
2 changed files with 94 additions and 241 deletions

View file

@ -2,9 +2,8 @@
# -*- ruby -*- # -*- ruby -*-
exec "${RUBY-ruby}" "-x" "$0" "$@" && [ ] if false exec "${RUBY-ruby}" "-x" "$0" "$@" && [ ] if false
#!ruby #!ruby
# This needs ruby 2.0, Subversion and Git. # This needs ruby 2.0 and Git.
# As a Ruby committer, run this in an SVN repository # As a Ruby committer, run this in a git repository to commit a change.
# to commit a change.
require 'tempfile' require 'tempfile'
require 'net/http' require 'net/http'
@ -15,20 +14,11 @@ ENV['LC_ALL'] = 'C'
ORIGIN = 'git@git.ruby-lang.org:ruby.git' ORIGIN = 'git@git.ruby-lang.org:ruby.git'
GITHUB = 'git@github.com:ruby/ruby.git' GITHUB = 'git@github.com:ruby/ruby.git'
module Merger class << Merger = Object.new
REPOS = 'svn+ssh://svn@ci.ruby-lang.org/ruby/'
end
class << Merger
include Merger
def help def help
puts <<-HELP puts <<-HELP
\e[1msimple backport\e[0m \e[1msimple backport\e[0m
ruby #$0 1234 ruby #$0 1234abc
\e[1mbackport from other branch\e[0m
ruby #$0 17502 mvm
\e[1mrevision increment\e[0m \e[1mrevision increment\e[0m
ruby #$0 revisionup ruby #$0 revisionup
@ -37,16 +27,16 @@ class << Merger
ruby #$0 teenyup ruby #$0 teenyup
\e[1mtagging major release\e[0m \e[1mtagging major release\e[0m
ruby #$0 tag 2.2.0 ruby #$0 tag 3.2.0
\e[1mtagging patch release\e[0m (about 2.1.0 or later, it means X.Y.Z (Z > 0) release) \e[1mtagging patch release\e[0m (for 2.1.0 or later, it means X.Y.Z (Z > 0) release)
ruby #$0 tag ruby #$0 tag
\e[1mtagging preview/RC\e[0m \e[1mtagging preview/RC\e[0m
ruby #$0 tag 2.2.0-preview1 ruby #$0 tag 3.2.0-preview1
\e[1mremove tag\e[0m \e[1mremove tag\e[0m
ruby #$0 removetag 2.2.9 ruby #$0 removetag 3.2.9
\e[33;1m* all operations shall be applied to the working directory.\e[0m \e[33;1m* all operations shall be applied to the working directory.\e[0m
HELP HELP
@ -57,11 +47,11 @@ class << Merger
yield if block_given? yield if block_given?
STDERR.puts "\e[1;33m#{str} ([y]es|[a]bort|[r]etry#{'|[e]dit' if editfile})\e[0m" STDERR.puts "\e[1;33m#{str} ([y]es|[a]bort|[r]etry#{'|[e]dit' if editfile})\e[0m"
case STDIN.gets case STDIN.gets
when /\Aa/i then exit when /\Aa/i then exit 1
when /\Ar/i then redo when /\Ar/i then redo
when /\Ay/i then break when /\Ay/i then break
when /\Ae/i then system(ENV['EDITOR'], editfile) when /\Ae/i then system(ENV['EDITOR'], editfile)
else exit else exit 1
end end
end end
end end
@ -69,11 +59,7 @@ class << Merger
def version_up(teeny: false) def version_up(teeny: false)
now = Time.now now = Time.now
now = now.localtime(9*60*60) # server is Japan Standard Time +09:00 now = now.localtime(9*60*60) # server is Japan Standard Time +09:00
if svn_mode? system('git', 'checkout', 'HEAD', 'version.h')
system('svn', 'revert', 'version.h')
else
system('git', 'checkout', 'HEAD', 'version.h')
end
v, pl = version v, pl = version
if teeny if teeny
@ -129,31 +115,10 @@ class << Merger
end end
tagname = "v#{v.join('_')}#{("_#{pl}" if v[0] < "2" || (v[0] == "2" && v[1] < "1") || /^(?:preview|rc)/ =~ pl)}" tagname = "v#{v.join('_')}#{("_#{pl}" if v[0] < "2" || (v[0] == "2" && v[1] < "1") || /^(?:preview|rc)/ =~ pl)}"
if svn_mode? unless execute('git', 'tag', tagname)
if relname abort 'specfied tag already exists. check tag name and remove it if you want to force re-tagging'
branch_url = `svn info`[/URL: (.*)/, 1]
else
branch_url = "#{REPOS}branches/ruby_"
if v[0] < '2' || (v[0] == '2' && v[1] < '1')
abort 'patchlevel must be greater than 0 for patch release' if pl == '0'
branch_url << v.join('_')
else
abort 'teeny must be greater than 0 for patch release' if v[2] == '0'
branch_url << v.join('_').sub(/_\d+\z/, '')
end
end
tag_url = "#{REPOS}tags/#{tagname}"
system('svn', 'info', tag_url, out: IO::NULL, err: IO::NULL)
if $?.success?
abort 'specfied tag already exists. check tag name and remove it if you want to force re-tagging'
end
execute('svn', 'cp', '-m', "add tag #{tagname}", branch_url, tag_url, interactive: true)
else
unless execute('git', 'tag', tagname)
abort 'specfied tag already exists. check tag name and remove it if you want to force re-tagging'
end
execute('git', 'push', ORIGIN, tagname, interactive: true)
end end
execute('git', 'push', ORIGIN, tagname, interactive: true)
end end
def remove_tag(relname) def remove_tag(relname)
@ -173,64 +138,44 @@ class << Merger
tagname = relname tagname = relname
end end
if svn_mode? execute('git', 'tag', '-d', tagname)
tag_url = "#{REPOS}tags/#{tagname}" execute('git', 'push', ORIGIN, ":#{tagname}", interactive: true)
execute('svn', 'rm', '-m', "remove tag #{tagname}", tag_url, interactive: true) execute('git', 'push', GITHUB, ":#{tagname}", interactive: true)
else
execute('git', 'tag', '-d', tagname)
execute('git', 'push', ORIGIN, ":#{tagname}", interactive: true)
execute('git', 'push', GITHUB, ":#{tagname}", interactive: true)
end
end end
def update_revision_h def update_revision_h
if svn_mode?
execute('svn', 'up')
end
execute('ruby tool/file2lastrev.rb --revision.h . > revision.tmp') execute('ruby tool/file2lastrev.rb --revision.h . > revision.tmp')
execute('tool/ifchange', '--timestamp=.revision.time', 'revision.h', 'revision.tmp') execute('tool/ifchange', '--timestamp=.revision.time', 'revision.h', 'revision.tmp')
execute('rm', '-f', 'revision.tmp') execute('rm', '-f', 'revision.tmp')
end end
def stat def stat
if svn_mode? `git status --short`
`svn stat`
else
`git status --short`
end
end end
def diff(file = nil) def diff(file = nil)
if svn_mode? command = %w[git diff --color HEAD]
command = %w[svn diff --diff-cmd=diff -x -upw]
else
command = %w[git diff --color]
end
IO.popen(command + [file].compact, &:read) IO.popen(command + [file].compact, &:read)
end end
def commit(file) def commit(file)
if svn_mode? current_branch = IO.popen(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], &:read).strip
begin execute('git', 'add', '.') && execute('git', 'commit', '-F', file)
execute('svn', 'ci', '-F', file) end
execute('svn', 'update') # svn ci doesn't update revision info on working copy
ensure def has_conflicts?
execute('rm', '-f', 'subversion.commitlog') changes = IO.popen(%w[git status --porcelain -z]) { |io| io.readlines("\0", chomp: true) }
end # Discover unmerged files
else # AU: unmerged, added by us
current_branch = IO.popen(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], &:read).strip # DU: unmerged, deleted by us
execute('git', 'add', '.') && # UU: unmerged, both modified
execute('git', 'commit', '-F', file) # AA: unmerged, both added
end conflict = changes.grep(/\A(?:.U|AA) /) {$'}
!conflict.empty?
end end
private private
def svn_mode?
return @svn_mode if defined?(@svn_mode)
@svn_mode = system("svn info", %i(out err) => IO::NULL)
end
# Prints the version of Ruby found in version.h # Prints the version of Ruby found in version.h
def version def version
v = p = nil v = p = nil
@ -289,21 +234,21 @@ else
case ARGV[0] case ARGV[0]
when /--ticket=(.*)/ when /--ticket=(.*)/
tickets = $1.split(/,/).map{|num| " [Backport ##{num}]"}.join tickets = $1.split(/,/)
ARGV.shift ARGV.shift
else else
tickets = '' tickets = []
detect_ticket = true
end end
revstr = ARGV[0].delete('^, :\-0-9a-fA-F') revstr = ARGV[0].gsub(%r!https://github\.com/ruby/ruby/commit/|https://bugs\.ruby-lang\.org/projects/ruby-master/repository/git/revisions/!, '')
revstr = revstr.delete('^, :\-0-9a-fA-F')
revs = revstr.split(/[,\s]+/) revs = revstr.split(/[,\s]+/)
commit_message = '' commit_message = ''
revs.each do |rev| revs.each do |rev|
git_rev = nil git_rev = nil
case rev case rev
when /\A\d{1,6}\z/
svn_rev = rev
when /\A\h{7,40}\z/ when /\A\h{7,40}\z/
git_rev = rev git_rev = rev
when nil then when nil then
@ -314,27 +259,24 @@ else
exit exit
end end
# Merge revision from Git patch or SVN # Merge revision from Git patch
if git_rev git_uri = "https://git.ruby-lang.org/ruby.git/patch/?id=#{git_rev}"
git_uri = "https://git.ruby-lang.org/ruby.git/patch/?id=#{git_rev}" resp = Net::HTTP.get_response(URI(git_uri))
resp = Net::HTTP.get_response(URI(git_uri)) if resp.code != '200'
if resp.code != '200' abort "'#{git_uri}' returned status '#{resp.code}':\n#{resp.body}"
abort "'#{git_uri}' returned status '#{resp.code}':\n#{resp.body}"
end
patch = resp.body.sub(/^diff --git a\/version\.h b\/version\.h\nindex .*\n--- a\/version\.h\n\+\+\+ b\/version\.h\n@@ .* @@\n(?:[-\+ ].*\n|\n)+/, '')
message = "\n\n#{(patch[/^Subject: (.*)\n\ndiff --git/m, 1] || "Message not found for revision: #{git_rev}\n")}"
puts '+ git apply'
IO.popen(['git', 'apply'], 'wb') { |f| f.write(patch) }
else
default_merge_branch = (%r{^URL: .*/branches/ruby_1_8_} =~ `svn info` ? 'branches/ruby_1_8' : 'trunk')
svn_src = "#{Merger::REPOS}#{ARGV[1] || default_merge_branch}"
message = IO.popen(['svn', 'log', '-c', svn_rev, svn_src], &:read)
cmd = ['svn', 'merge', '--accept=postpone', '-c', svn_rev, svn_src]
puts "+ #{cmd.join(' ')}"
system(*cmd)
end end
patch = resp.body.sub(/^diff --git a\/version\.h b\/version\.h\nindex .*\n--- a\/version\.h\n\+\+\+ b\/version\.h\n@@ .* @@\n(?:[-\+ ].*\n|\n)+/, '')
if detect_ticket
tickets += patch.scan(/\[(?:Bug|Feature|Misc) #(\d+)\]/i).map(&:first)
end
message = "#{(patch[/^Subject: (.*)\n---\n /m, 1] || "Message not found for revision: #{git_rev}\n")}"
message.gsub!(/\G(.*)\n( .*)/, "\\1\\2")
message = "\n\n#{message}"
puts '+ git apply'
IO.popen(['git', 'apply', '--3way'], 'wb') { |f| f.write(patch) }
commit_message << message.sub(/\A-+\nr.*/, '').sub(/\n-+\n\z/, '').gsub(/^./, "\t\\&") commit_message << message.sub(/\A-+\nr.*/, '').sub(/\n-+\n\z/, '').gsub(/^./, "\t\\&")
end end
@ -345,20 +287,22 @@ else
Merger.version_up Merger.version_up
f = Tempfile.new 'merger.rb' f = Tempfile.new 'merger.rb'
f.printf "merge revision(s) %s:%s", revstr, tickets f.printf "merge revision(s) %s:%s", revstr, tickets.map{|num| " [Backport ##{num}]"}.join
f.write commit_message f.write commit_message
f.flush f.flush
f.close f.close
Merger.interactive('conflicts resolved?', f.path) do if Merger.has_conflicts?
IO.popen(ENV['PAGER'] || ['less', '-R'], 'w') do |g| Merger.interactive('conflicts resolved?', f.path) do
g << Merger.stat IO.popen(ENV['PAGER'] || ['less', '-R'], 'w') do |g|
g << "\n\n" g << Merger.stat
f.open g << "\n\n"
g << f.read f.open
f.close g << f.read
g << "\n\n" f.close
g << Merger.diff g << "\n\n"
g << Merger.diff
end
end end
end end

View file

@ -10,13 +10,7 @@ require 'optparse'
require 'abbrev' require 'abbrev'
require 'pp' require 'pp'
require 'shellwords' require 'shellwords'
begin require 'reline'
require 'readline'
rescue LoadError
module Readline; end
end
VERSION = '0.0.1'
opts = OptionParser.new opts = OptionParser.new
target_version = nil target_version = nil
@ -24,10 +18,9 @@ repo_path = nil
api_key = nil api_key = nil
ssl_verify = true ssl_verify = true
opts.on('-k REDMINE_API_KEY', '--key=REDMINE_API_KEY', 'specify your REDMINE_API_KEY') {|v| api_key = v} opts.on('-k REDMINE_API_KEY', '--key=REDMINE_API_KEY', 'specify your REDMINE_API_KEY') {|v| api_key = v}
opts.on('-t TARGET_VERSION', '--target=TARGET_VARSION', /\A\d(?:\.\d)+\z/, 'specify target version (ex: 2.1)') {|v| target_version = v} opts.on('-t TARGET_VERSION', '--target=TARGET_VARSION', /\A\d(?:\.\d)+\z/, 'specify target version (ex: 3.1)') {|v| target_version = v}
opts.on('-r RUBY_REPO_PATH', '--repository=RUBY_REPO_PATH', 'specify repository path') {|v| repo_path = v} opts.on('-r RUBY_REPO_PATH', '--repository=RUBY_REPO_PATH', 'specify repository path') {|v| repo_path = v}
opts.on('--[no-]ssl-verify', TrueClass, 'use / not use SSL verify') {|v| ssl_verify = v} opts.on('--[no-]ssl-verify', TrueClass, 'use / not use SSL verify') {|v| ssl_verify = v}
opts.version = VERSION
opts.parse!(ARGV) opts.parse!(ARGV)
http_options = {use_ssl: true} http_options = {use_ssl: true}
@ -35,17 +28,17 @@ http_options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE unless ssl_verify
$openuri_options = {} $openuri_options = {}
$openuri_options[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE unless ssl_verify $openuri_options[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE unless ssl_verify
TARGET_VERSION = target_version || ENV['TARGET_VERSION'] || (raise 'need to specify TARGET_VERSION') TARGET_VERSION = target_version || ENV['TARGET_VERSION'] || (puts opts.help; raise 'need to specify TARGET_VERSION')
RUBY_REPO_PATH = repo_path || ENV['RUBY_REPO_PATH'] RUBY_REPO_PATH = repo_path || ENV['RUBY_REPO_PATH']
BACKPORT_CF_KEY = 'cf_5' BACKPORT_CF_KEY = 'cf_5'
STATUS_CLOSE = 5 STATUS_CLOSE = 5
REDMINE_API_KEY = api_key || ENV['REDMINE_API_KEY'] || (raise 'need to specify REDMINE_API_KEY') REDMINE_API_KEY = api_key || ENV['REDMINE_API_KEY'] || (puts opts.help; raise 'need to specify REDMINE_API_KEY')
REDMINE_BASE = 'https://bugs.ruby-lang.org' REDMINE_BASE = 'https://bugs.ruby-lang.org'
@query = { @query = {
'f[]' => BACKPORT_CF_KEY, 'f[]' => BACKPORT_CF_KEY,
"op[#{BACKPORT_CF_KEY}]" => '~', "op[#{BACKPORT_CF_KEY}]" => '~',
"v[#{BACKPORT_CF_KEY}][]" => "#{TARGET_VERSION}: REQUIRED", "v[#{BACKPORT_CF_KEY}][]" => "\"#{TARGET_VERSION}: REQUIRED\"",
'limit' => 40, 'limit' => 40,
'status_id' => STATUS_CLOSE, 'status_id' => STATUS_CLOSE,
'sort' => 'updated_on' 'sort' => 'updated_on'
@ -70,28 +63,28 @@ COLORS = {
} }
class String class String
def color(fore=nil, back=nil, bold: false, underscore: false) def color(fore=nil, back=nil, opts={}, bold: false, underscore: false)
seq = "" seq = ""
if bold if bold || opts[:bold]
seq << "\e[1m" seq = seq + "\e[1m"
end end
if underscore if underscore || opts[:underscore]
seq << "\e[2m" seq = seq + "\e[2m"
end end
if fore if fore
c = COLORS[fore] c = COLORS[fore]
raise "unknown foreground color #{fore}" unless c raise "unknown foreground color #{fore}" unless c
seq << "\e[#{c}m" seq = seq + "\e[#{c}m"
end end
if back if back
c = COLORS[back] c = COLORS[back]
raise "unknown background color #{back}" unless c raise "unknown background color #{back}" unless c
seq << "\e[#{c + 10}m" seq = seq + "\e[#{c + 10}m"
end end
if seq.empty? if seq.empty?
self self
else else
seq << self << "\e[0m" seq = seq + self + "\e[0m"
end end
end end
end end
@ -162,84 +155,6 @@ def more(sio)
end end
end end
class << Readline
def readline(prompt = '')
console = IO.console
console.binmode
_, lx = console.winsize
if /mswin|mingw/ =~ RUBY_PLATFORM or /^(?:vt\d\d\d|xterm)/i =~ ENV["TERM"]
cls = "\r\e[2K"
else
cls = "\r" << (" " * lx)
end
cls << "\r" << prompt
console.print prompt
console.flush
line = ''
while true
case c = console.getch
when "\r", "\n"
puts
HISTORY << line
return line
when "\C-?", "\b" # DEL/BS
print "\b \b" if line.chop!
when "\C-u"
print cls
line.clear
when "\C-d"
return nil if line.empty?
line << c
when "\C-p"
HISTORY.pos -= 1
line = HISTORY.current
print cls
print line
when "\C-n"
HISTORY.pos += 1
line = HISTORY.current
print cls
print line
else
if c >= " "
print c
line << c
end
end
end
end
HISTORY = []
def HISTORY.<<(val)
HISTORY.push(val)
@pos = self.size
self
end
def HISTORY.pos
@pos ||= 0
end
def HISTORY.pos=(val)
@pos = val
if @pos < 0
@pos = -1
elsif @pos >= self.size
@pos = self.size
end
end
def HISTORY.current
@pos ||= 0
if @pos < 0 || @pos >= self.size
''
else
self[@pos]
end
end
end unless defined?(Readline.readline)
def find_svn_log(pattern)
`svn log --xml --stop-on-copy --search="#{pattern}" #{RUBY_REPO_PATH}`
end
def find_git_log(pattern) def find_git_log(pattern)
`git #{RUBY_REPO_PATH ? "-C #{RUBY_REPO_PATH.shellescape}" : ""} log --grep="#{pattern}"` `git #{RUBY_REPO_PATH ? "-C #{RUBY_REPO_PATH.shellescape}" : ""} log --grep="#{pattern}"`
end end
@ -276,10 +191,12 @@ def backport_command_string
# check if the Git revision is included in master # check if the Git revision is included in master
has_commit(c, "master") has_commit(c, "master")
end.sort_by do |changeset|
Integer(IO.popen(%W[git show -s --format=%ct #{changeset}], &:read))
end end
@changesets.define_singleton_method(:validated){true} @changesets.define_singleton_method(:validated){true}
end end
" #{merger_path} --ticket=#{@issue} #{@changesets.sort.join(',')}" "#{merger_path} --ticket=#{@issue} #{@changesets.join(',')}"
end end
def status_char(obj) def status_char(obj)
@ -294,7 +211,7 @@ end
console = IO.console console = IO.console
row, = console.winsize row, = console.winsize
@query['limit'] = row - 2 @query['limit'] = row - 2
puts "Backporter #{VERSION}".color(bold: true) + " for #{TARGET_VERSION}" puts "Redmine Backporter".color(bold: true) + " for Ruby #{TARGET_VERSION}"
class CommandSyntaxError < RuntimeError; end class CommandSyntaxError < RuntimeError; end
commands = { commands = {
@ -306,10 +223,11 @@ commands = {
@issues = issues = res["issues"] @issues = issues = res["issues"]
from = res["offset"] + 1 from = res["offset"] + 1
total = res["total_count"] total = res["total_count"]
closed = issues.count { |x, _| x["status"]["name"] == "Closed" }
to = from + issues.size - 1 to = from + issues.size - 1
puts "#{from}-#{to} / #{total}" puts "#{from}-#{to} / #{total} (closed: #{closed})"
issues.each_with_index do |x, i| issues.each_with_index do |x, i|
id = "##{x["id"]}".color(*PRIORITIES[x["priority"]["name"]]) id = "##{x["id"]}".color(*PRIORITIES[x["priority"]["name"]], bold: x["status"]["name"] == "Closed")
puts "#{'%2d' % i} #{id} #{x["priority"]["name"][0]} #{status_char(x["status"])} #{x["subject"][0,80]}" puts "#{'%2d' % i} #{id} #{x["priority"]["name"][0]} #{status_char(x["status"])} #{x["subject"][0,80]}"
end end
}, },
@ -376,9 +294,6 @@ eom
"rel" => proc{|args| "rel" => proc{|args|
# this feature requires custom redmine which allows add_related_issue API # this feature requires custom redmine which allows add_related_issue API
case args case args
when /\Ar?(\d+)\z/ # SVN
rev = $1
uri = URI("#{REDMINE_BASE}/projects/ruby-master/repository/trunk/revisions/#{rev}/issues.json")
when /\A\h{7,40}\z/ # Git when /\A\h{7,40}\z/ # Git
rev = args rev = args
uri = URI("#{REDMINE_BASE}/projects/ruby-master/repository/git/revisions/#{rev}/issues.json") uri = URI("#{REDMINE_BASE}/projects/ruby-master/repository/git/revisions/#{rev}/issues.json")
@ -422,7 +337,7 @@ eom
}, },
"done" => proc{|args| "done" => proc{|args|
raise CommandSyntaxError unless /\A(\d+)?(?: by (\h+))?(?:\s*-- +(.*))?\z/ =~ args raise CommandSyntaxError unless /\A(\d+)?(?: *by (\h+))?(?:\s*-- +(.*))?\z/ =~ args
notes = $3 notes = $3
notes.strip! if notes notes.strip! if notes
rev = $2 rev = $2
@ -436,23 +351,17 @@ eom
next next
end end
if rev if rev.nil? && log = find_git_log("##@issue]")
elsif system("svn info #{RUBY_REPO_PATH&.shellescape}", %i(out err) => IO::NULL) # SVN /^commit (?<rev>\h{40})$/ =~ log
if (log = find_svn_log("##@issue]")) && (/revision="(?<rev>\d+)/ =~ log)
rev = "r#{rev}"
end
else # Git
if log = find_git_log("##@issue]")
/^commit (?<rev>\h{40})$/ =~ log
end
end end
if log && rev if log && rev
str = log[/merge revision\(s\) ([^:]+)(?=:)/] str = log[/merge revision\(s\) ([^:]+)(?=:)/]
if str if str
str.insert(5, "d") str.sub!(/\Amerge/, 'merged')
str = "ruby_#{TARGET_VERSION.tr('.','_')} #{rev} #{str}." str.gsub!(/\h{40}/, 'commit:\0')
str = "ruby_#{TARGET_VERSION.tr('.','_')} commit:#{rev} #{str}."
else else
str = "ruby_#{TARGET_VERSION.tr('.','_')} #{rev}." str = "ruby_#{TARGET_VERSION.tr('.','_')} commit:#{rev}."
end end
if notes if notes
str << "\n" str << "\n"
@ -571,7 +480,7 @@ list = Abbrev.abbrev(commands.keys)
@changesets = nil @changesets = nil
while true while true
begin begin
l = Readline.readline "#{('#' + @issue.to_s).color(bold: true) if @issue}> " l = Reline.readline "#{('#' + @issue.to_s).color(bold: true) if @issue}> "
rescue Interrupt rescue Interrupt
break break
end end