| #!/usr/bin/env ruby |
| |
| require "optparse" |
| require "ostruct" |
| require "stringio" |
| require "tempfile" |
| |
| SENDMAIL = "/usr/sbin/sendmail" |
| |
| def parse(args) |
| options = OpenStruct.new |
| options.to = [] |
| options.error_to = [] |
| options.from = nil |
| options.add_diff = true |
| options.repository_uri = nil |
| options.rss_path = nil |
| options.rss_uri = nil |
| options.name = 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("-n", "--no-diff", |
| "Don't add diffs") do |from| |
| options.add_diff = false |
| 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 << added_dirs(info) |
| body << added_files(info) |
| body << copied_dirs(info) |
| body << copied_files(info) |
| body << deleted_dirs(info) |
| body << deleted_files(info) |
| body << modified_dirs(info) |
| body << modified_files(info) |
| body << "\n" |
| body << " Log:\n" |
| info.log.each_line do |line| |
| body << " #{line}" |
| end |
| body << "\n" |
| body << change_info(info, params[:repository_uri], params[:add_diff]) |
| body |
| 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" |
| if block_given? |
| yield(rv, items) |
| else |
| rv << items.collect {|item| " #{item}\n"}.join('') |
| end |
| end |
| rv |
| end |
| |
| def changed_files(title, files, &block) |
| changed_items(title, "files", files, &block) |
| 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 copied_files(info) |
| changed_files("Copied", info.copied_files) do |rv, files| |
| rv << files.collect do |file, from_file, from_rev| |
| <<-INFO |
| #{file} |
| (from rev #{from_rev}, #{from_file}) |
| INFO |
| end.join("") |
| end |
| end |
| |
| def changed_dirs(title, files, &block) |
| changed_items(title, "directories", files, &block) |
| 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 |
| |
| def copied_dirs(info) |
| changed_dirs("Copied", info.copied_dirs) do |rv, dirs| |
| rv << dirs.collect do |dir, from_dir, from_rev| |
| " #{dir} (from rev #{from_rev}, #{from_dir})\n" |
| end.join("") |
| end |
| end |
| |
| |
| CHANGED_TYPE = { |
| :added => "Added", |
| :modified => "Modified", |
| :deleted => "Deleted", |
| :copied => "Copied", |
| :property_changed => "Property changed", |
| } |
| |
| CHANGED_MARK = Hash.new("=") |
| CHANGED_MARK[:property_changed] = "_" |
| |
| def change_info(info, uri, add_diff) |
| result = changed_dirs_info(info, uri) |
| result = "\n#{result}" unless result.empty? |
| result << "\n" |
| diff_info(info, uri, add_diff).each do |key, infos| |
| infos.each do |desc, link| |
| result << "#{desc}\n" |
| end |
| end |
| result |
| end |
| |
| def changed_dirs_info(info, uri) |
| rev = info.revision |
| (info.added_dirs.collect do |dir| |
| " Added: #{dir}\n" |
| end + info.copied_dirs.collect do |dir, from_dir, from_rev| |
| <<-INFO |
| Copied: #{dir} |
| (from rev #{from_rev}, #{from_dir}) |
| INFO |
| end + info.deleted_dirs.collect do |dir| |
| <<-INFO |
| Deleted: #{dir} |
| % svn ls #{[uri, dir].compact.join("/")}@#{rev - 1} |
| INFO |
| end + info.updated_dirs.collect do |dir| |
| " Modified: #{dir}\n" |
| end).join("\n") |
| end |
| |
| def diff_info(info, uri, add_diff) |
| info.diffs.collect do |key, values| |
| [ |
| key, |
| values.collect do |type, value| |
| args = [] |
| rev = info.revision |
| case type |
| when :added |
| command = "cat" |
| when :modified, :property_changed |
| command = "diff" |
| args.concat(["-r", "#{info.revision - 1}:#{info.revision}"]) |
| when :deleted |
| command = "cat" |
| rev -= 1 |
| when :copied |
| command = "cat" |
| else |
| raise "unknown diff type: #{value.type}" |
| end |
| |
| command += " #{args.join(' ')}" unless args.empty? |
| |
| link = [uri, key].compact.join("/") |
| |
| line_info = "+#{value.added_line} -#{value.deleted_line}" |
| desc = <<-HEADER |
| #{CHANGED_TYPE[value.type]}: #{key} (#{line_info}) |
| #{CHANGED_MARK[value.type] * 67} |
| HEADER |
| |
| if add_diff |
| desc << value.body |
| else |
| desc << <<-CONTENT |
| % svn #{command} #{link}@#{rev} |
| CONTENT |
| end |
| |
| [desc, link] |
| end |
| ] |
| end |
| end |
| |
| def make_header(to, from, info, params) |
| headers = [] |
| headers << x_author(info) |
| headers << x_repository(info) |
| headers << x_id(info) |
| headers << x_sha256(info) |
| headers << "Content-Type: text/plain; charset=UTF-8" |
| headers << "Content-Transfer-Encoding: 8bit" |
| 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) |
| subject = "" |
| subject << "#{name}:" if name |
| subject << "r#{info.revision}: " |
| subject << info.log.lstrip.to_a.first.to_s.chomp |
| NKF.nkf("-WM", 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_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) |
| args = to.collect {|address| address.dump}.join(' ') |
| open("| #{SENDMAIL} #{args}", "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, true).each do |name, infos| |
| infos.each do |desc, link| |
| item = maker.items.new_item |
| item.title = name |
| item.description = info.log |
| 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 |
| if ARGV.find {|arg| arg == "--help"} |
| parse(ARGV) |
| else |
| repos, revision, to, *rest = ARGV |
| options = parse(rest) |
| end |
| |
| require "svn/info" |
| info = Svn::Info.new(repos, revision) |
| from = options.from || info.author |
| to = [to, *options.to] |
| params = { |
| :repository_uri => options.repository_uri, |
| :name => options.name, |
| :add_diff => options.add_diff, |
| } |
| 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 |