2014-02-21 09:10:06 -05:00
#!/usr/bin/env ruby
require 'open-uri'
2014-02-22 06:29:21 -05:00
require 'openssl'
2014-02-21 09:10:06 -05:00
require 'net/http'
require 'json'
require 'io/console'
require 'stringio'
require 'strscan'
2014-02-22 06:29:21 -05:00
require 'optparse'
2015-03-03 10:30:54 -05:00
require 'abbrev'
2014-02-21 09:10:06 -05:00
require 'pp'
2015-01-19 23:09:24 -05:00
begin
require 'readline'
rescue LoadError
2015-01-19 23:23:43 -05:00
module Readline ; end
2015-01-19 23:09:24 -05:00
end
2014-02-21 09:10:06 -05:00
2014-02-22 06:29:21 -05:00
VERSION = '0.0.1'
opts = OptionParser . new
target_version = nil
repo_path = nil
api_key = nil
ssl_verify = true
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 ( '-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 . version = VERSION
opts . parse! ( ARGV )
http_options = { use_ssl : true }
http_options [ :verify_mode ] = OpenSSL :: SSL :: VERIFY_NONE unless ssl_verify
2015-03-03 12:14:22 -05:00
$openuri_options = { }
$openuri_options [ :ssl_verify_mode ] = OpenSSL :: SSL :: VERIFY_NONE unless ssl_verify
2014-02-22 06:29:21 -05:00
TARGET_VERSION = target_version || ENV [ 'TARGET_VERSION' ] || ( raise 'need to specify TARGET_VERSION' )
RUBY_REPO_PATH = repo_path || ENV [ 'RUBY_REPO_PATH' ]
2014-02-21 09:10:06 -05:00
BACKPORT_CF_KEY = 'cf_5'
STATUS_CLOSE = 5
2014-02-22 06:29:21 -05:00
REDMINE_API_KEY = api_key || ENV [ 'REDMINE_API_KEY' ] || ( raise 'need to specify REDMINE_API_KEY' )
2014-02-21 09:10:06 -05:00
REDMINE_BASE = 'https://bugs.ruby-lang.org'
@query = {
'f[]' = > BACKPORT_CF_KEY ,
" op[ #{ BACKPORT_CF_KEY } ] " = > '~' ,
" v[ #{ BACKPORT_CF_KEY } ][] " = > " #{ TARGET_VERSION } : REQUIRED " ,
'limit' = > 40 ,
'status_id' = > STATUS_CLOSE ,
'sort' = > 'updated_on'
}
PRIORITIES = {
'Low' = > [ :white , :blue ] ,
'Normal' = > [ ] ,
'High' = > [ :red ] ,
'Urgent' = > [ :red , :white ] ,
'Immediate' = > [ :red , :white , { underscore : true } ] ,
}
COLORS = {
black : 30 ,
red : 31 ,
green : 32 ,
yellow : 33 ,
blue : 34 ,
magenta : 35 ,
cyan : 36 ,
white : 37 ,
}
class String
def color ( fore = nil , back = nil , bold : false , underscore : false )
seq = " "
if bold
seq << " \e [1m "
end
if underscore
seq << " \e [2m "
end
if fore
c = COLORS [ fore ]
raise " unknown foreground color #{ fore } " unless c
seq << " \e [ #{ c } m "
end
if back
2014-02-22 06:50:56 -05:00
c = COLORS [ back ]
2014-02-21 09:10:06 -05:00
raise " unknown background color #{ back } " unless c
2014-02-22 06:50:56 -05:00
seq << " \e [ #{ c + 10 } m "
2014-02-21 09:10:06 -05:00
end
if seq . empty?
self
else
seq << self << " \e [0m "
end
end
end
def wcwidth ( wc )
return 8 if wc == " \t "
n = wc . ord
if n < 0x20
0
elsif n < 0x80
1
else
2
end
end
def fold ( str , col )
i = 0
size = str . size
len = 0
while i < size
case c = str [ i ]
when " \r " , " \n "
len = 0
else
d = wcwidth ( c )
len += d
if len == col
str . insert ( i + 1 , " \n " )
len = 0
i += 2
next
elsif len > col
str . insert ( i , " \n " )
len = d
i += 2
next
end
end
i += 1
end
str
end
class StringScanner
# lx: limit of x (colmns of screen)
# ly: limit of y (rows of screen)
def getrows ( lx , ly )
cp1 = charpos
x = 0
y = 0
until eos?
case c = getch
when " \r "
x = 0
when " \n "
x = 0
y += 1
when " \t "
x += 8
when / [ \ x00- \ x7f] /
# halfwidth
x += 1
else
# fullwidth
x += 2
end
if x > lx
x = 0
y += 1
unscan
end
if y > = ly
return string [ cp1 ... charpos ]
end
end
string [ cp1 .. - 1 ]
end
end
def more ( sio )
console = IO . console
ly , lx = console . winsize
ly -= 1
str = sio . string
cls = " \r " + ( " " * lx ) + " \r "
ss = StringScanner . new ( str )
rows = ss . getrows ( lx , ly )
puts rows
until ss . eos?
print " : "
case c = console . getch
2014-02-26 23:10:00 -05:00
when ' '
2014-02-21 09:10:06 -05:00
rows = ss . getrows ( lx , ly )
puts cls + rows
2014-02-26 23:10:00 -05:00
when 'j' , " \r "
2014-02-21 09:10:06 -05:00
rows = ss . getrows ( lx , 1 )
puts cls + rows
when " q "
print cls
break
else
print " \b "
end
end
end
2015-01-27 02:00:50 -05:00
class << Readline
def readline ( prompt = '' )
console = IO . console
console . binmode
ly , 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 1
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
2015-03-03 09:42:15 -05:00
if c > = " "
print c
line << c
end
2015-01-27 02:00:50 -05:00
end
end
2015-01-19 23:09:13 -05:00
end
2015-01-27 02:00:50 -05:00
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
''
2015-01-19 21:25:48 -05:00
else
2015-01-27 02:00:50 -05:00
self [ @pos ]
2015-01-19 21:25:48 -05:00
end
end
2015-01-19 23:09:24 -05:00
end unless defined? ( Readline . readline )
2015-01-19 21:25:48 -05:00
2014-02-21 09:10:06 -05:00
def mergeinfo
` svn propget svn:mergeinfo #{ RUBY_REPO_PATH } `
end
2014-02-22 08:43:16 -05:00
def find_svn_log ( pattern )
2015-01-22 04:05:53 -05:00
` svn log --xml --stop-on-copy --search=" #{ pattern } " #{ RUBY_REPO_PATH } `
2014-02-22 08:43:16 -05:00
end
2014-02-21 09:10:06 -05:00
def show_last_journal ( http , uri )
res = http . get ( " #{ uri . path } ?include=journals " )
res . value
h = JSON ( res . body )
x = h [ " issue " ]
raise " no issue " unless x
x = x [ " journals " ]
raise " no journals " unless x
x = x . last
puts " == #{ x [ " user " ] [ " name " ] } ( #{ x [ " created_on " ] } ) "
x [ " details " ] . each do | y |
puts JSON ( y )
end
puts x [ " notes " ]
end
2016-03-29 05:59:27 -04:00
def merger_path
2016-04-22 02:07:19 -04:00
RUBY_PLATFORM =~ / mswin|mingw / ? 'merger' : File . expand_path ( '../merger.rb' , __FILE__ )
2016-03-29 05:59:27 -04:00
end
2014-02-21 09:10:06 -05:00
def backport_command_string
2015-03-03 12:14:22 -05:00
unless @changesets . respond_to? ( :validated )
@changesets = @changesets . select do | c |
begin
uri = URI ( " #{ REDMINE_BASE } /projects/ruby-trunk/repository/revisions/ #{ c } " )
uri . read ( $openuri_options )
true
rescue
false
end
end
@changesets . define_singleton_method ( :validated ) { true }
end
2016-04-15 12:34:22 -04:00
" #{ merger_path } --ticket= #{ @issue } #{ @changesets . sort . join ( ',' ) } "
2014-02-21 09:10:06 -05:00
end
2015-02-23 04:30:24 -05:00
def status_char ( obj )
case obj [ " name " ]
when " Closed "
" C " . color ( bold : true )
else
obj [ " name " ] [ 0 ]
end
end
2014-02-21 09:10:06 -05:00
console = IO . console
row , col = console . winsize
@query [ 'limit' ] = row - 2
puts " Backporter #{ VERSION } " . color ( bold : true ) + " for #{ TARGET_VERSION } "
2015-03-03 10:30:54 -05:00
class CommandSyntaxError < RuntimeError ; end
commands = {
" ls " = > proc { | args |
raise CommandSyntaxError unless / \ A( \ d+)? \ z / =~ args
2015-01-19 23:23:43 -05:00
uri = URI ( REDMINE_BASE + '/projects/ruby-trunk/issues.json?' + URI . encode_www_form ( @query . dup . merge ( 'page' = > ( $1 ? $1 . to_i : 1 ) ) ) )
2014-02-21 09:10:06 -05:00
# puts uri
2015-03-03 12:14:22 -05:00
res = JSON ( uri . read ( $openuri_options ) )
2014-02-21 09:10:06 -05:00
@issues = issues = res [ " issues " ]
from = res [ " offset " ] + 1
total = res [ " total_count " ]
to = from + issues . size - 1
puts " #{ from } - #{ to } / #{ total } "
issues . each_with_index do | x , i |
id = " # #{ x [ " id " ] } " . color ( * PRIORITIES [ x [ " priority " ] [ " name " ] ] )
2015-02-23 04:30:24 -05:00
puts " #{ '%2d' % i } #{ id } #{ x [ " priority " ] [ " name " ] [ 0 ] } #{ status_char ( x [ " status " ] ) } #{ x [ " subject " ] [ 0 , 80 ] } "
2014-02-21 09:10:06 -05:00
end
2015-03-03 10:30:54 -05:00
} ,
" show " = > proc { | args |
2016-03-29 05:59:27 -04:00
if / \ A( \ d+) \ z / =~ args
id = $1 . to_i
id = @issues [ id ] [ " id " ] if @issues && id < @issues . size
@issue = id
elsif @issue
id = @issue
else
raise CommandSyntaxError
end
2014-02-21 09:10:06 -05:00
uri = " #{ REDMINE_BASE } /issues/ #{ id } "
uri = URI ( uri + " .json?include=children,attachments,relations,changesets,journals " )
2015-03-03 12:14:22 -05:00
res = JSON ( uri . read ( $openuri_options ) )
2014-02-21 09:10:06 -05:00
i = res [ " issue " ]
2014-12-23 23:26:18 -05:00
unless i [ " changesets " ]
abort " You don't have view_changesets permission "
end
2014-02-21 09:10:06 -05:00
id = " # #{ i [ " id " ] } " . color ( * PRIORITIES [ i [ " priority " ] [ " name " ] ] )
sio = StringIO . new
sio . puts <<eom
2015-01-05 20:19:01 -05:00
#{i["subject"].color(bold: true, underscore: true)}
2014-02-21 09:10:06 -05:00
#{i["project"]["name"]} [#{i["tracker"]["name"]} #{id}] #{i["status"]["name"]} (#{i["created_on"]})
author : #{i["author"]["name"]}
assigned : #{i["assigned_to"].to_h["name"]}
eom
i [ " custom_fields " ] . each do | x |
sio . puts " %-10s: %s " % [ x [ " name " ] , x [ " value " ] ]
end
#res["attachements"].each do |x|
#end
sio . puts i [ " description " ]
sio . puts
2015-01-05 20:19:01 -05:00
sio . puts " = changesets " . color ( bold : true , underscore : true )
2014-12-23 23:26:18 -05:00
@changesets = [ ]
i [ " changesets " ] . each do | x |
@changesets << x [ " revision " ]
2015-01-05 20:19:01 -05:00
sio . puts " == #{ x [ " revision " ] } #{ x [ " committed_on " ] } #{ x [ " user " ] [ " name " ] rescue nil } " . color ( bold : true , underscore : true )
2014-12-23 23:26:18 -05:00
sio . puts x [ " comments " ]
2014-02-21 09:10:06 -05:00
end
2015-03-03 12:14:22 -05:00
@changesets = @changesets . sort . uniq
2014-12-23 08:04:06 -05:00
if i [ " journals " ] && ! i [ " journals " ] . empty?
2015-01-05 20:19:01 -05:00
sio . puts " = journals " . color ( bold : true , underscore : true )
2014-12-23 08:04:06 -05:00
i [ " journals " ] . each do | x |
2015-01-05 20:19:01 -05:00
sio . puts " == #{ x [ " user " ] [ " name " ] } ( #{ x [ " created_on " ] } ) " . color ( bold : true , underscore : true )
2014-12-23 08:04:06 -05:00
x [ " details " ] . each do | y |
sio . puts JSON ( y )
end
sio . puts x [ " notes " ]
2014-02-21 09:10:06 -05:00
end
end
more ( sio )
2015-03-03 10:30:54 -05:00
} ,
2014-02-21 09:10:06 -05:00
2015-03-03 10:30:54 -05:00
" rel " = > proc { | args |
2015-01-16 04:48:57 -05:00
# this feature requires custom redmine which allows add_related_issue API
2016-03-29 12:55:55 -04:00
raise CommandSyntaxError unless / \ Ar?( \ d+) \ z / =~ args
2015-03-03 10:30:54 -05:00
unless @issue
puts " ticket not selected "
next
end
2016-04-16 15:20:11 -04:00
rev = $1
2015-01-16 04:48:57 -05:00
uri = URI ( " #{ REDMINE_BASE } /projects/ruby-trunk/repository/revisions/ #{ rev } /issues.json " )
Net :: HTTP . start ( uri . host , uri . port , http_options ) do | http |
res = http . post ( uri . path , " issue_id= #@issue " ,
'X-Redmine-API-Key' = > REDMINE_API_KEY )
2016-03-29 05:59:27 -04:00
begin
res . value
rescue
2016-10-27 03:34:32 -04:00
if $! . respond_to? ( :response ) && $! . response . is_a? ( Net :: HTTPConflict )
$stderr . puts " the revision has already related to the ticket "
else
$stderr . puts " deployed redmine doesn't have https://github.com/ruby/bugs.ruby-lang.org/commit/01fbba60d68cb916ddbccc8a8710e68c5217171d \n ask naruse or hsbt "
end
2016-04-26 10:43:50 -04:00
next
2016-03-29 05:59:27 -04:00
end
2015-01-16 04:48:57 -05:00
puts res . body
2016-03-29 12:55:55 -04:00
@changesets << rev
2015-05-25 09:38:00 -04:00
class << @changesets
remove_method ( :validated ) rescue nil
end
2015-01-16 04:48:57 -05:00
end
2015-03-03 10:30:54 -05:00
} ,
2015-01-16 04:48:57 -05:00
2015-03-03 10:30:54 -05:00
" backport " = > proc { | args |
2015-01-16 04:48:57 -05:00
# this feature implies backport command which wraps tool/merger.rb
2016-11-11 10:43:43 -05:00
raise CommandSyntaxError unless args . empty?
2014-02-22 06:29:21 -05:00
unless @issue
puts " ticket not selected "
next
end
2014-02-21 09:10:06 -05:00
puts backport_command_string
2015-03-03 10:30:54 -05:00
} ,
2014-02-21 09:10:06 -05:00
2015-03-03 10:30:54 -05:00
" done " = > proc { | args |
2015-04-13 04:25:34 -04:00
raise CommandSyntaxError unless / \ A( \ d+)?(?: \ s*-- +(.*))? \ z / =~ args
2014-02-21 09:10:06 -05:00
notes = $2
2014-02-22 08:43:16 -05:00
notes . strip! if notes
2014-02-21 09:10:06 -05:00
if $1
2014-02-22 06:29:21 -05:00
i = $1 . to_i
2014-02-21 09:10:06 -05:00
i = @issues [ i ] [ " id " ] if @issues && i < @issues . size
@issue = i
end
2014-02-22 06:29:21 -05:00
unless @issue
puts " ticket not selected "
next
end
2014-02-21 09:10:06 -05:00
2014-02-22 08:43:16 -05:00
log = find_svn_log ( " # #@issue ] " )
2014-03-01 06:07:15 -05:00
if log && / revision="(?<rev> \ d+) / =~ log
2014-02-22 08:43:16 -05:00
str = log [ / merge revision \ (s \ ) ([^:]+)(?=:) / ]
str . insert ( 5 , " d " )
str = " ruby_ #{ TARGET_VERSION . tr ( '.' , '_' ) } r #{ rev } #{ str } . "
if notes
str << " \n "
str << notes
end
notes = str
else
puts " no commit is found whose log include # #@issue "
next
end
puts notes
2014-02-21 09:10:06 -05:00
uri = URI ( " #{ REDMINE_BASE } /issues/ #{ @issue } .json " )
2014-02-22 06:29:21 -05:00
Net :: HTTP . start ( uri . host , uri . port , http_options ) do | http |
2014-02-21 09:10:06 -05:00
res = http . get ( uri . path )
data = JSON ( res . body )
h = data [ " issue " ] [ " custom_fields " ] . find { | x | x [ " id " ] == 5 }
2017-03-12 14:31:38 -04:00
if h and val = h [ " value " ] and val != " "
2014-02-21 09:10:06 -05:00
case val [ / (?: \ A|, ) #{ Regexp . quote TARGET_VERSION } : ([^,]+) / , 1 ]
2014-07-02 02:39:06 -04:00
when 'REQUIRED' , 'UNKNOWN' , 'DONTNEED' , 'WONTFIX'
2015-01-22 04:05:53 -05:00
val [ $~ . offset ( 1 ) [ 0 ] ... $~ . offset ( 1 ) [ 1 ] ] = 'DONE'
2014-02-21 09:10:06 -05:00
when 'DONE' # , /\A\d+\z/
puts 'already backport is done'
next # already done
when nil
val << " , #{ TARGET_VERSION } : DONE "
else
raise " unknown status ' # $1' "
end
else
2017-03-12 14:31:38 -04:00
val = " #{ TARGET_VERSION } : DONE "
2014-02-21 09:10:06 -05:00
end
data = { " issue " = > { " custom_fields " = > [ { " id " = > 5 , " value " = > val } ] } }
data [ 'issue' ] [ 'notes' ] = notes if notes
res = http . put ( uri . path , JSON ( data ) ,
'X-Redmine-API-Key' = > REDMINE_API_KEY ,
'Content-Type' = > 'application/json' )
res . value
show_last_journal ( http , uri )
end
2015-03-03 10:30:54 -05:00
} ,
" close " = > proc { | args |
raise CommandSyntaxError unless / \ A( \ d+)? \ z / =~ args
2014-02-21 09:10:06 -05:00
if $1
i = $1 . to_i
i = @issues [ i ] [ " id " ] if @issues && i < @issues . size
@issue = i
end
2014-02-22 06:29:21 -05:00
unless @issue
puts " ticket not selected "
next
end
2014-02-21 09:10:06 -05:00
uri = URI ( " #{ REDMINE_BASE } /issues/ #{ @issue } .json " )
2014-02-22 06:29:21 -05:00
Net :: HTTP . start ( uri . host , uri . port , http_options ) do | http |
2014-02-21 09:10:06 -05:00
data = { " issue " = > { " status_id " = > STATUS_CLOSE } }
res = http . put ( uri . path , JSON ( data ) ,
'X-Redmine-API-Key' = > REDMINE_API_KEY ,
'Content-Type' = > 'application/json' )
res . value
show_last_journal ( http , uri )
end
2015-03-03 10:30:54 -05:00
} ,
" last " = > proc { | args |
raise CommandSyntaxError unless / \ A( \ d+)? \ z / =~ args
2014-02-21 09:10:06 -05:00
if $1
i = $1 . to_i
i = @issues [ i ] [ " id " ] if @issues && i < @issues . size
@issue = i
end
2014-02-22 06:29:21 -05:00
unless @issue
puts " ticket not selected "
next
end
2014-02-21 09:10:06 -05:00
uri = URI ( " #{ REDMINE_BASE } /issues/ #{ @issue } .json " )
2014-02-22 06:29:21 -05:00
Net :: HTTP . start ( uri . host , uri . port , http_options ) do | http |
2014-02-21 09:10:06 -05:00
show_last_journal ( http , uri )
end
2015-03-03 10:30:54 -05:00
} ,
" ! " = > proc { | args |
system ( args . strip )
} ,
" quit " = > proc { | args |
raise CommandSyntaxError unless args . empty?
2014-02-21 09:10:06 -05:00
exit
2015-03-03 10:30:54 -05:00
} ,
" exit " = > " quit " ,
" help " = > proc { | args |
2015-01-19 22:59:03 -05:00
puts 'ls [PAGE] ' . color ( bold : true ) + ' show all required tickets'
2015-03-03 02:58:39 -05:00
puts '[show] TICKET ' . color ( bold : true ) + ' show the detail of the TICKET, and select it'
2015-03-03 10:30:54 -05:00
puts 'backport ' . color ( bold : true ) + ' show the option of selected ticket for merger.rb'
2015-01-19 21:31:50 -05:00
puts 'rel REVISION ' . color ( bold : true ) + ' add the selected ticket as related to the REVISION'
2014-02-22 06:29:21 -05:00
puts 'done [TICKET] [-- NOTE]' . color ( bold : true ) + ' set Backport field of the TICKET to DONE'
puts 'close [TICKET] ' . color ( bold : true ) + ' close the TICKET'
puts 'last [TICKET] ' . color ( bold : true ) + ' show the last journal of the TICKET'
2015-01-27 02:04:38 -05:00
puts '! COMMAND ' . color ( bold : true ) + ' execute COMMAND'
2015-03-03 10:30:54 -05:00
}
}
list = Abbrev . abbrev ( commands . keys )
@issues = nil
@issue = nil
@changesets = nil
while true
begin
l = Readline . readline " #{ ( '#' + @issue . to_s ) . color ( bold : true ) if @issue } > "
rescue Interrupt
break
end
break unless l
2015-03-24 03:49:02 -04:00
cmd , args = l . strip . split ( / \ s+| \ b / , 2 )
2015-03-03 10:30:54 -05:00
next unless cmd
if ( ! args || args . empty? ) && / \ A \ d+ \ z / =~ cmd
args = cmd
cmd = " show "
end
2016-07-06 12:05:57 -04:00
cmd = list [ cmd ]
2015-03-03 10:30:54 -05:00
if commands [ cmd ] . is_a? String
2016-07-06 12:05:57 -04:00
cmd = list [ commands [ cmd ] ]
2015-03-03 10:30:54 -05:00
end
begin
if cmd
2015-03-24 03:49:02 -04:00
commands [ cmd ] . call ( args )
2015-03-03 10:30:54 -05:00
else
raise CommandSyntaxError
end
rescue CommandSyntaxError
2014-02-21 09:10:06 -05:00
puts " error #{ l . inspect } "
end
end