File: commit-email.rb

package info (click to toggle)
subversion 1.4.2dfsg1-3
  • links: PTS
  • area: main
  • in suites: etch
  • size: 37,284 kB
  • ctags: 32,888
  • sloc: ansic: 406,472; python: 38,378; sh: 15,438; cpp: 9,604; ruby: 8,313; perl: 5,308; java: 4,576; lisp: 3,860; xml: 3,298; makefile: 856
file content (435 lines) | stat: -rwxr-xr-x 9,793 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#!/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