Skip to content

Instantly share code, notes, and snippets.

@eban
Created October 1, 2008 08:49
Show Gist options
  • Save eban/14054 to your computer and use it in GitHub Desktop.
Save eban/14054 to your computer and use it in GitHub Desktop.
commit-email.rb
#!/usr/bin/env ruby
require "optparse"
require "ostruct"
SENDMAIL = "/usr/sbin/sendmail"
def parse(args)
options = OpenStruct.new
options.to = []
options.error_to = []
options.from = nil
options.repository_uri = nil
options.rss_path = nil
options.rss_uri = nil
options.name = nil
options.viewvc_uri = nil
opts = OptionParser.new do |opts|
opts.separator ""
opts.on("-I", "--include [PATH]",
"Add [PATH] to load path") do |path|
$LOAD_PATH.unshift(path)
end
opts.on("-t", "--to [TO]",
"Add [TO] to to address") do |to|
options.to << to unless to.nil?
end
opts.on("-e", "--error-to [TO]",
"Add [TO] to to address when error is occurred") do |to|
options.error_to << to unless to.nil?
end
opts.on("-f", "--from [FROM]",
"Use [FROM] as from address") do |from|
options.from = from
end
opts.on("--viewvc-uri [URI]",
"Use [URI] as URI of viewvc") do |uri|
options.viewvc_uri = uri
end
opts.on("-r", "--repository-uri [URI]",
"Use [URI] as URI of repository") do |uri|
options.repository_uri = uri
end
opts.on("--rss-path [PATH]",
"Use [PATH] as output RSS path") do |path|
options.rss_path = path
end
opts.on("--rss-uri [URI]",
"Use [URI] as output RSS URI") do |uri|
options.rss_uri = uri
end
opts.on("--name [NAME]",
"Use [NAME] as repository name") do |name|
options.name = name
end
opts.on_tail("--help", "Show this message") do
puts opts
exit
end
end
opts.parse!(args)
options
end
def make_body(info, params)
body = ""
body << "#{info.author}\t#{format_time(info.date)}\n"
body << "\n"
body << " New Revision: #{info.revision}\n"
body << "\n"
body << change_info(info, params[:viewvc_uri])
body << "\n"
body << " Log:\n"
body << info.log.gsub(/^\t*/, " ").rstrip
body << "\n\n"
body << added_dirs(info)
body << added_files(info)
body << deleted_dirs(info)
body << deleted_files(info)
body << modified_dirs(info)
body << modified_files(info)
body.rstrip + "\n"
end
def format_time(time)
time.strftime('%Y-%m-%d %X %z (%a, %d %b %Y)')
end
def changed_items(title, type, items)
rv = ""
unless items.empty?
rv << " #{title} #{type}:\n"
rv << items.collect {|item| " #{item}\n"}.join('')
end
rv
end
def changed_files(title, files)
changed_items(title, "files", files)
end
def added_files(info)
changed_files("Added", info.added_files)
end
def deleted_files(info)
changed_files("Removed", info.deleted_files)
end
def modified_files(info)
changed_files("Modified", info.updated_files)
end
def changed_dirs(title, files)
changed_items(title, "directories", files)
end
def added_dirs(info)
changed_dirs("Added", info.added_dirs)
end
def deleted_dirs(info)
changed_dirs("Removed", info.deleted_dirs)
end
def modified_dirs(info)
changed_dirs("Modified", info.updated_dirs)
end
CHANGED_TYPE = {
:added => "Added",
:modified => "Modified",
:deleted => "Deleted",
:copied => "Copied",
:property_changed => "Property changed",
}
def change_info(info, uri)
" #{uri}?view=rev&revision=#{info.revision}\n"
end
def changed_dirs_info(info, uri)
rev = info.revision
(info.added_dirs.collect do |dir|
" Added: #{dir}\n"
end + info.deleted_dirs.collect do |dir|
" Deleted: #{dir}\n"
end + info.updated_dirs.collect do |dir|
" Modified: #{dir}\n"
end).join("\n")
end
def diff_info(info, uri)
info.diffs.collect do |key, values|
[
key,
values.collect do |type, value|
case type
when :added
command = "cat"
rev = "?revision=#{info.revision}&view=markup"
when :modified, :property_changed
command = "diff"
rev = "?r1=#{info.revision}&r2=#{info.revision - 1}&diff_format=u"
when :deleted, :copied
command = "cat"
rev = ""
else
raise "unknown diff type: #{value[:type]}"
end
link = [uri, key.sub(/ .+/,"")||""].join("/") + rev
=begin without_diff
desc = <<-HEADER
#{CHANGED_TYPE[value[:type]]}: #{key} (+#{value[:added]} -#{value[:deleted]})
HEADER
# result << <<-CONTENT
# % svn #{command} -r #{rev} #{link}
# CONTENT
desc << value[:body]
=end
desc = ''
[desc, link]
end
]
end
end
def make_header(to, from, info, params)
headers = []
headers << x_author(info)
headers << x_repository(info)
headers << x_revision(info)
headers << x_id(info)
# headers << x_sha256(info)
headers << "Content-Type: text/plain; charset=us-ascii"
headers << "Content-Transfer-Encoding: 7bit"
headers << "From: #{from}"
headers << "To: #{to.join(' ')}"
headers << "Subject: #{make_subject(params[:name], info)}"
headers.find_all do |header|
/\A\s*\z/ !~ header
end.join("\n")
end
def make_subject(name, info)
branches = info.sha256.map{|x,| x[/(?:branches\/|tags\/)?(.+?)\//, 1]}.uniq
subject = ""
subject << "#{name}:" if name
subject << "r#{info.revision}"
subject << " (#{branches.join(', ')})" unless branches.empty?
subject << ": "
subject << info.log.to_a.first.to_s.strip
subject
end
def x_author(info)
"X-SVN-Author: #{info.author}"
end
def x_repository(info)
# "X-SVN-Repository: #{info.path}"
"X-SVN-Repository: XXX"
end
def x_id(info)
"X-SVN-Commit-Id: #{info.entire_sha256}"
end
def x_revision(info)
"X-SVN-Revision: #{info.revision}"
end
def x_sha256(info)
info.sha256.collect do |name, inf|
"X-SVN-SHA256-Info: #{name}, #{inf[:revision]}, #{inf[:sha256]}"
end.join("\n")
end
def make_mail(to, from, info, params)
make_header(to, from, info, params) + "\n" + make_body(info, params)
end
def sendmail(to, from, mail)
open("| #{SENDMAIL} #{to.join(' ')}", "w") do |f|
f.print(mail)
end
end
def output_rss(name, file, rss_uri, repos_uri, info)
prev_rss = nil
begin
if File.exist?(file)
File.open(file) do |f|
prev_rss = RSS::Parser.parse(f)
end
end
rescue RSS::Error
end
File.open(file, "w") do |f|
f.print(make_rss(prev_rss, name, rss_uri, repos_uri, info).to_s)
end
end
def make_rss(base_rss, name, rss_uri, repos_uri, info)
RSS::Maker.make("1.0") do |maker|
maker.encoding = "UTF-8"
maker.channel.about = rss_uri
maker.channel.title = rss_title(name || repos_uri)
maker.channel.link = repos_uri
maker.channel.description = rss_title(name || repos_uri)
maker.channel.dc_date = info.date
if base_rss
base_rss.items.each do |item|
item.setup_maker(maker)
end
end
diff_info(info, repos_uri).each do |name, infos|
infos.each do |desc, link|
item = maker.items.new_item
item.title = name
item.description = desc
item.content_encoded = "<pre>#{h(desc)}</pre>"
item.link = link
item.dc_date = info.date
item.dc_creator = info.author
end
end
maker.items.do_sort = true
maker.items.max_size = 15
end
end
def rss_title(name)
"Repository of #{name}"
end
def rss_items(items, info, repos_uri)
diff_info(info, repos_uri).each do |name, infos|
infos.each do |desc, link|
items << [link, name, desc, info.date]
end
end
items.sort_by do |uri, title, desc, date|
date
end.reverse
end
def main
repos, revision, to, *rest = ARGV
options = parse(rest)
require "svn/info"
info = Svn::Info.new(repos, revision)
from = options.from || info.author + "@ruby-lang.org"
to = [to, *options.to]
params = {
:repository_uri => options.repository_uri,
:viewvc_uri => options.viewvc_uri,
:name => options.name
}
sendmail(to, from, make_mail(to, from, info, params))
if options.repository_uri and
options.rss_path and
options.rss_uri
require "rss/1.0"
require "rss/dublincore"
require "rss/content"
require "rss/maker"
include RSS::Utils
output_rss(options.name,
options.rss_path,
options.rss_uri,
options.repository_uri,
info)
end
end
begin
main
rescue Exception
_, _, to, *rest = ARGV
to = [to]
from = ENV["USER"]
begin
options = parse(rest)
to = options.error_to unless options.error_to.empty?
from = options.from
rescue Exception
end
sendmail(to, from, <<-MAIL)
From: #{from}
To: #{to.join(', ')}
Subject: Error
#{$!.class}: #{$!.message}
#{$@.join("\n")}
MAIL
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment