]> git.pld-linux.org Git - packages/cvsspam.git/blob - cvsspam-branch.diff
- shadow outer var fix
[packages/cvsspam.git] / cvsspam-branch.diff
1 Index: cvsspam.conf
2 ===================================================================
3 --- cvsspam.conf        (.../tags/RELEASE-0_2_12)       (revision 275)
4 +++ cvsspam.conf        (.../trunk)     (revision 275)
5 @@ -34,11 +34,19 @@
6  #
7  #     When $jiraURL is given, text of the form 'project-1234' will be linked
8  #   to this issue in JIRA.
9 +#
10 +#     When $xplannerStoryURL, $xplannerIterationURL and $xplannerProjectURL are
11 +#   given, text of the form XS1234 will be linked to XPlanner stories; text of
12 +#   the form XI1234 will be linked to XPlanner iterations; and text of the form
13 +#   XP1234 will be linked to XPlanner projects.
14  
15  #$bugzillaURL = "http://bugzilla.mozilla.org/show_bug.cgi?id=%s"
16  
17  #$jiraURL = "http://jira.atlassian.com/secure/ViewIssue.jspa?key=%s"
18  
19 +#$xplannerStoryURL = "http://www.example.com/xplanner/do/view/userstory?oid=%s"
20 +#$xplannerIterationURL = "http://www.example.com/xplanner/do/view/iteration?oid=%s"
21 +#$xplannerProjectURL = "http://www.example.com/xplanner/do/view/project?oid=%s"
22  
23  # Link to Wiki systems
24  # 
25 @@ -71,6 +79,7 @@
26  
27  #$cvswebURL = "http://localhost/cgi-bin/cvsweb.cgi"
28  
29 +#$tracURL = "http://localhost/trac/project"
30  
31  
32  # Additional SMTP Headers                                            (Optional)
33 @@ -119,12 +128,21 @@
34  # cvsdiff keyword ignoring                  (Default: show changes in keywords)
35  #
36  #     Changes in CVS keywords can be distracting.  For instance, the
37 -#   $Revision$ keyword will change on each commit.  Set this value to true
38 +#   $Revision$ keyword will change on each commit.  Set this value to true
39  #   to exclude changes in keyword fields (adds the -kk option to cvs diff).
40  
41  #$diff_ignore_keywords = true
42  
43  
44 +# cvsdiff whitespace ignoring               (Default: show whitespace-only changes)
45 +#
46 +#   Whitespace-only changes can distract from the rest of a diff. Set this
47 +#   value to true to exclude changes in the amount of whitespace (adds the -b
48 +#   option to cvs diff).
49 +
50 +$diff_ignore_whitespace = true
51 +
52 +
53  # $no_removed_file_diff and $no_added_file_diff
54  #
55  #     Set both these options, and emails will only include diffs for files
56 @@ -166,14 +184,46 @@
57  #     Allows the specification of a character set for all generated emails.
58  #   The files CVS is dealing with should already be in the character set you
59  #   specify -- no transcoding is done.
60 +#
61 +#   Note that you can override this with --charset argument per module, etc.
62  
63  #$charset="ISO-8859-1"
64  
65  
66  
67 +# Users file                                  (Default: $CVSROOT/CVSROOT/users)
68 +#
69 +#      Specify users file to lookup From addresses for commites
70 +
71 +#$users_file = "/srv/svn/users"
72 +
73 +# Users file charset                                        (Default: $charset)
74 +#
75 +#      If the users file is encoded differently than $charset, You can override
76 +#   it here. Especially useful if you use --charset argument. See above.
77 +
78 +#$users_file_charset = "ISO-8859-1"
79 +
80 +
81  # File names in Subject                      (Default: no filenames in Subject)
82  #
83  #     Some people like file names to appear in the email subject.  To make
84  #   them happy, you can say $files_in_subject = true here.
85  
86  #$files_in_subject = false
87 +
88 +
89 +# Module Path email header           (Default: no X-CVSspam-Module-Path header)
90 +#
91 +#     Sets 'X-CVSspam-Module-Path' header to contain common path of files commited.
92 +#   Useful for server side mail filtering.
93 +
94 +#$cvsroot_email_header = true
95 +
96 +# Email size limit                                        (Default: around 2MB)
97 +#
98 +#     When large changes are committed, large CVSspam emails can result.  Here
99 +#   you can set the size of email that CVSspam is not allowed to append any
100 +#   more diffs onto.  Specify the number of bytes.
101 +
102 +#$mail_size_limit = 2097152
103
104 Property changes on: cvsspam.conf
105 ___________________________________________________________________
106 Deleted: svn:executable
107    - *
108 Modified: svn:keywords
109    - Author Date Id Revision
110    + Author Date Id
111
112 Index: collect_diffs.rb
113 ===================================================================
114 --- collect_diffs.rb    (.../tags/RELEASE-0_2_12)       (revision 275)
115 +++ collect_diffs.rb    (.../trunk)     (revision 275)
116 @@ -27,6 +27,13 @@
117  $dirtemplate = "#cvsspam.#{Process.getpgrp}.#{Process.uid}"
118  
119  def find_data_dir
120 +  if $from_address
121 +    safe_from = make_fromaddr_safe_for_filename($from_address)
122 +    Dir["#{$tmpdir}/#{$dirtemplate}.#{safe_from}-*"].each do |dir|
123 +      stat = File.stat(dir)
124 +      return dir if stat.owned?
125 +    end
126 +  end
127    Dir["#{$tmpdir}/#{$dirtemplate}-*"].each do |dir|
128      stat = File.stat(dir)
129      return dir if stat.owned?
130 @@ -35,6 +42,14 @@
131  end
132  
133  
134 +# transform any special / unexpected characters appearing in the argument to
135 +# --from so that they will not cause problems if the value is inserted into
136 +# a file or directory name
137 +def make_fromaddr_safe_for_filename(addr)
138 +  addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
139 +end
140 +
141 +
142  def blah(msg)
143    if $debug
144      $stderr.puts "collect_diffs.rb: #{msg}"
145 @@ -129,7 +144,14 @@
146    changes = Array.new
147    i = 0
148    while i < cvs_info.length
149 -    changes << ChangeInfo.new(cvs_info[i], cvs_info[i+=1], cvs_info[i+=1])
150 +    change_file = cvs_info[i]
151 +    # It's been reported,
152 +    # http://lists.badgers-in-foil.co.uk/pipermail/cvsspam-devel/2005-September/000380.html
153 +    # that sometimes the second revision number that CVS gives us contains a
154 +    # trailing newline character, so we strip ws from these values before use,
155 +    change_from = cvs_info[i+=1].strip
156 +    change_to = cvs_info[i+=1].strip
157 +    changes << ChangeInfo.new(change_file, change_from, change_to)
158      i+=1
159    end
160    return changes
161 @@ -222,6 +244,7 @@
162  
163        diff_cmd = Array.new << $cvs_prog << "-nq" << "diff" << "-Nu"
164        diff_cmd << "-kk" if $diff_ignore_keywords
165 +      diff_cmd << "-b" if $diff_ignore_whitespace
166  
167        if change.isAddition
168          file.write "#A "
169 @@ -333,9 +356,11 @@
170  end
171  
172  $config = nil
173 +$from_address = nil
174  $cvs_prog = "cvs"
175  $debug = false
176  $diff_ignore_keywords = false
177 +$diff_ignore_whitespace = false
178  $task_keywords = []
179  
180  unless ENV.has_key?('CVSROOT')
181 @@ -387,6 +412,7 @@
182    end
183    $config = arg if opt=="--config"
184    $debug = true if opt == "--debug"
185 +  $from_address = arg if opt == "--from"
186  end
187  
188  blah("CVSROOT is #{ENV['CVSROOT']}")
189 Index: record_lastdir.rb
190 ===================================================================
191 --- record_lastdir.rb   (.../tags/RELEASE-0_2_12)       (revision 275)
192 +++ record_lastdir.rb   (.../trunk)     (revision 275)
193 @@ -4,7 +4,6 @@
194  #   http://www.badgers-in-foil.co.uk/projects/cvsspam/
195  # Copyright (c) David Holroyd
196  
197 -$repositorydir = ARGV.shift
198  
199  $tmpdir = ENV["TMPDIR"] || "/tmp"
200  
201 @@ -19,6 +18,36 @@
202    nil
203  end
204  
205 +
206 +# transform any special / unexpected characters appearing in the argument to
207 +# --from so that they will not cause problems if the value is inserted into
208 +# a file or directory name
209 +def make_fromaddr_safe_for_filename(addr)
210 +  addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
211 +end
212 +
213 +# Option processing doesn't use GetoptLong (for the moment) bacause arguments
214 +# given to this script by CVS include the names of committed files.  It
215 +# seems quite possible that one of those file names could begin with a '-'
216 +# and therefore be treated by GetoptLong as a value which requires processing.
217 +# This would probably result in an error.
218 +#
219 +# [That could be worked around by placing a '--' option (which tells GetoptLong
220 +# to stop processing option arguments) at the very end of the arguments to
221 +# record_lastdir.rb in commitinfo, but that's very easily forgotten, and isn't
222 +# really backwards compatable with the behaviour of older CVSspam releases.]
223 +if ARGV.first == "--from"
224 +  # we could, of course, be tricked, if the first committed file in the list
225 +  # happened to be named '--from' :S
226 +
227 +  # drop the "--from"
228 +  ARGV.shift
229 +  # and use the value which was given following the option,
230 +  $dirtemplate << "." << make_fromaddr_safe_for_filename(ARGV.shift)
231 +end
232 +
233 +$repositorydir = ARGV.shift
234 +
235  $datadir = find_data_dir()
236  
237  if $datadir==nil
238
239 Property changes on: TODO
240 ___________________________________________________________________
241 Deleted: svn:executable
242    - *
243
244 Index: project.xml
245 ===================================================================
246 Index: svn_post_commit_hook.rb
247 ===================================================================
248 --- svn_post_commit_hook.rb     (.../tags/RELEASE-0_2_12)       (revision 0)
249 +++ svn_post_commit_hook.rb     (.../trunk)     (revision 275)
250 @@ -0,0 +1,410 @@
251 +#!/usr/bin/ruby -w
252 +
253 +$svnlook_exe = "svnlook"  # default assumes the program is in $PATH
254 +
255 +def usage(msg)
256 +  $stderr.puts(msg)
257 +  exit(1)
258 +end
259 +
260 +def blah(msg)
261 +  if $debug
262 +    $stderr.puts "svn_post_commit_hook.rb: #{msg}"
263 +  end
264 +end
265 +
266 +$debug = false
267 +$tmpdir = ENV["TMPDIR"] || "/tmp"
268 +$dirtemplate = "#svnspam.#{Process.getpgrp}.#{Process.uid}"
269 +# arguments to pass though to 'cvsspam.rb'
270 +$passthrough_args = []
271 +
272 +def make_data_dir
273 +  dir = "#{$tmpdir}/#{$dirtemplate}-#{rand(99999999)}"
274 +  Dir.mkdir(dir, 0700)
275 +  dir
276 +end
277 +
278 +def init
279 +  $datadir = make_data_dir
280 +
281 +  # set PWD so that svnlook can create its .svnlook directory
282 +  Dir.chdir($datadir)
283 +end
284 +
285 +def cleanup
286 +  unless $debug
287 +         File.unlink("#{$datadir}/logfile")
288 +         Dir.rmdir($datadir)
289 +  end
290 +end
291 +
292 +def send_email
293 +  cmd = File.dirname($0) + "/cvsspam.rb"
294 +  unless system(cmd,"--svn","#{$datadir}/logfile", *$passthrough_args)
295 +    fail "problem running '#{cmd}'"
296 +  end
297 +end
298 +
299 +# Like IO.popen, but accepts multiple arguments like Kernel.exec
300 +# (So no need to escape shell metacharacters)
301 +def safer_popen(*args)
302 +  IO.popen("-") do |pipe|
303 +    if pipe==nil
304 +      exec(*args)
305 +    else
306 +      yield pipe
307 +    end
308 +  end
309 +end
310 +
311 +
312 +# Process the command-line arguments in the given list
313 +def process_args
314 +  require 'getoptlong'
315 +
316 +  opts = GetoptLong.new(
317 +    [ "--to",     "-t", GetoptLong::REQUIRED_ARGUMENT ],
318 +    [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
319 +    [ "--debug",  "-d", GetoptLong::NO_ARGUMENT ],
320 +    [ "--from",   "-u", GetoptLong::REQUIRED_ARGUMENT ],
321 +    [ "--charset",      GetoptLong::REQUIRED_ARGUMENT ]
322 +  )
323 +
324 +  opts.each do |opt, arg|
325 +    if ["--to", "--config", "--from", "--charset"].include?(opt)
326 +      $passthrough_args << opt << arg
327 +    end
328 +    if ["--debug"].include?(opt)
329 +      $passthrough_args << opt
330 +    end
331 +    $config = arg if opt=="--config"
332 +    $debug = true if opt == "--debug"
333 +  end
334 +
335 +  $repository = ARGV[0]
336 +  $revision = ARGV[1]
337 +
338 +  unless $revision =~ /^\d+$/
339 +    usage("revision must be an integer: #{revision.inspect}")
340 +  end
341 +  $revision = $revision.to_i
342 +
343 +  unless FileTest.directory?($repository)
344 +    usage("no such directory: #{$repository.inspect}")
345 +  end
346 +end
347 +
348 +# runs the given svnlook subcommand
349 +def svnlook(cmd, revision, *args)
350 +  rev = revision.to_s
351 +  safer_popen($svnlook_exe, cmd, $repository, "-r", rev, *args) do |io|
352 +    yield io
353 +  end
354 +end
355 +
356 +class Change
357 +  def initialize(filechange, propchange, path)
358 +    @filechange = filechange
359 +    @propchange = propchange
360 +    @path = path
361 +  end
362 +
363 +  attr_accessor :filechange, :propchange, :path
364 +
365 +  def property_change?
366 +    @propchange != " "
367 +  end
368 +
369 +  def file_change?
370 +    @filechange != "_"
371 +  end
372 +
373 +  def addition?
374 +    @filechange == "A"
375 +  end
376 +
377 +  def deletion?
378 +    @filechange == "D"
379 +  end
380 +end
381 +
382 +
383 +
384 +# Line-oriented access to an underlying IO object.  Remembers 'current' line
385 +# for lookahead during parsing.
386 +class LineReader
387 +  def initialize(io)
388 +    @io = io
389 +  end
390 +
391 +  def current
392 +    @line
393 +  end
394 +
395 +  def next_line
396 +    (@line = @io.gets) != nil
397 +  end
398 +
399 +  def assert_current(re)
400 +    raise "unexpected #{current.inspect}" unless @line =~ re
401 +    $~
402 +  end
403 +
404 +  def assert_next(re=nil)
405 +    raise "unexpected end of text" unless next_line
406 +    unless re.nil?
407 +      raise "unexpected #{current.inspect}" unless @line =~ re
408 +    end
409 +    $~
410 +  end
411 +end
412 +
413 +
414 +def read_modified_diff(out, lines, path)
415 +  lines.assert_next(/^=+$/)
416 +  lines.assert_next
417 +  if lines.current =~ /\(Binary files differ\)/
418 +    process_modified_binary_diff(out, lines, path)
419 +  else
420 +    process_modified_text_diff(out, lines, path)
421 +  end
422 +end
423 +
424 +
425 +def process_modified_binary_diff(out, lines, path)
426 +  prev_rev= $revision-1
427 +  next_rev= $revision
428 +  out.puts "#V #{prev_rev},#{next_rev}"
429 +  out.puts "#M #{path}"
430 +  out.puts "#U diff x x"
431 +  out.puts "#U Binary files x and y differ"
432 +end
433 +
434 +
435 +def process_modified_text_diff(out, lines, path)
436 +  m = lines.assert_current(/^---.*\(rev (\d+)\)$/)
437 +  prev_rev = m[1].to_i
438 +  diff1 = lines.current
439 +  m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
440 +  next_rev = m[1].to_i
441 +  diff2 = lines.current
442 +  out.puts "#V #{prev_rev},#{next_rev}"
443 +  out.puts "#M #{path}"
444 +  out.puts "#U #{diff1}"
445 +  out.puts "#U #{diff2}"
446 +  while lines.next_line && lines.current =~ /^[-\+ @\\]/
447 +    out.puts "#U #{lines.current}"
448 +  end
449 +end
450 +
451 +def read_added_diff(out, lines, path)
452 +  lines.assert_next(/^=+$/)
453 +  lines.assert_next
454 +  if lines.current =~ /\(Binary files differ\)/
455 +    process_added_binary_diff(out, lines, path)
456 +  else
457 +    process_added_text_diff(out, lines, path)
458 +  end
459 +end
460 +
461 +def process_added_binary_diff(out, lines, path)
462 +  next_rev= $revision
463 +  out.puts "#V NONE,#{next_rev}"
464 +  out.puts "#A #{path}"
465 +  out.puts "#U diff x x"
466 +  out.puts "#U Binary file x added"
467 +end
468 +
469 +def process_added_text_diff(out, lines, path)
470 +  m = lines.assert_current(/^---.*\(rev (\d+)\)$/)
471 +  prev_rev = m[1].to_i
472 +  diff1 = lines.current
473 +  m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
474 +  next_rev = m[1].to_i
475 +  diff2 = lines.current
476 +  out.puts "#V NONE,#{next_rev}"
477 +  out.puts "#A #{path}"
478 +  out.puts "#U #{diff1}"
479 +  out.puts "#U #{diff2}"
480 +  while lines.next_line && lines.current =~ /^[-\+ @\\]/
481 +    out.puts "#U #{lines.current}"
482 +  end
483 +end
484 +
485 +def read_deleted_diff(out, lines, path)
486 +  lines.assert_next(/^=+$/)
487 +  m = lines.assert_next(/^---.*\(rev (\d+)\)$/)
488 +  prev_rev = m[1].to_i
489 +  diff1 = lines.current
490 +  m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
491 +  next_rev = m[1].to_i
492 +  diff2 = lines.current
493 +  out.puts "#V #{prev_rev},NONE"
494 +  out.puts "#R #{path}"
495 +  out.puts "#U #{diff1}"
496 +  out.puts "#U #{diff2}"
497 +  while lines.next_line && lines.current =~ /^[-\+ @\\]/
498 +    out.puts "#U #{lines.current}"
499 +  end
500 +end
501 +
502 +def read_property_lines(path, prop_name, revision)
503 +  lines = []
504 +  svnlook("propget", revision, prop_name, path) do |io|
505 +    io.each_line do |line|
506 +      lines << line.chomp
507 +    end
508 +  end
509 +  lines
510 +end
511 +
512 +def assert_prop_match(a, b)
513 +  if !b.nil? && a != b
514 +    raise "property mismatch: #{a.inspect}!=#{b.inspect}"
515 +  end
516 +end
517 +
518 +# We need to read the property change from the output of svnlook, but have
519 +# a difficulty in that there's no unambiguous delimiter marking the end of
520 +# a potentially multi-line property value.  Therefore, we do a seperate
521 +# svn propget on the given file to get the value of the property on its own,
522 +# and then use that value as a guide as to how much data to read from the
523 +# svnlook output.
524 +def munch_prop_text(path, prop_name, revision, lines, line0)
525 +  prop = read_property_lines(path, prop_name, revision)
526 +  if prop.empty?
527 +    assert_prop_match(line0, "")
528 +    return
529 +  end
530 +  assert_prop_match(line0, prop.shift)
531 +  prop.each do |prop_line|
532 +    lines.assert_next
533 +    assert_prop_match(lines.current.chomp, prop_line)
534 +  end
535 +end
536 +
537 +def read_properties_changed(out, lines, path)
538 +  prev_rev= $revision-1
539 +  next_rev= $revision
540 +  lines.assert_next(/^_+$/)
541 +  return unless lines.next_line
542 +  out.puts "#V #{prev_rev},#{next_rev}"
543 +  out.puts "#P #{path}"
544 +# The first three get consumed and not highlighted
545 +  out.puts "#U "
546 +  out.puts "#U Property changes:"
547 +  out.puts "#U "
548 +
549 +  while true
550 +    break unless lines.current =~ /^(?:Name|Added|Deleted): (.+)$/
551 +
552 +    prop_name = $1
553 +    m = lines.assert_next(/^   ([-+]) (.*)/)
554 +    op = m[1]
555 +    line0 = m[2]
556 +    if op == "-"
557 +      munch_prop_text(path, prop_name, $revision-1, lines, line0)
558 +      if lines.next_line && lines.current =~ /^   \+ (.*)/
559 +       munch_prop_text(path, prop_name, $revision, lines, $1)
560 +       lines.next_line
561 +      end
562 +    else  # op == "+"
563 +      munch_prop_text(path, prop_name, $revision, lines, line0)
564 +      lines.next_line
565 +    end
566 +    out.puts "#U #{m[1]} #{prop_name}:#{m[2]}"
567 +  end
568 +  out.puts "#U "
569 +end
570 +
571 +def handle_copy(out, lines, path, from_ref, from_file)
572 +  prev_rev= $revision-1
573 +  next_rev= $revision
574 +  out.puts "#V #{from_file}:#{prev_rev},#{next_rev}"
575 +  out.puts "#C #{path}"
576 +  if lines.next_line && lines.current =~ /^=+$/
577 +    m = lines.assert_next(/^---.*\(rev (\d+)\)$/)
578 +    prev_rev = m[1].to_i
579 +    diff1 = lines.current
580 +    m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
581 +    next_rev = m[1].to_i
582 +    diff2 = lines.current
583 +    out.puts "#U #{diff1}"
584 +    out.puts "#U #{diff2}"
585 +    while lines.next_line && lines.current =~ /^[-\+ @\\]/
586 +      out.puts "#U #{lines.current}"
587 +    end
588 +  else
589 +    out.puts "#U "
590 +    out.puts "#U Copied from #{from_file}:#{from_ref}"
591 +    out.puts "#U "
592 +  end
593 +end
594 +
595 +def svnlook_author
596 +  svnlook("author", $revision) do |io|
597 +    return io.readline.chomp
598 +  end
599 +  nil
600 +end
601 +
602 +def find_author
603 +  return if $passthrough_args.include?("--from")
604 +  author = svnlook_author
605 +  if author
606 +    blah("Author from svnlook: '#{author}'")
607 +    $passthrough_args << "--from" << author
608 +  end
609 +end
610 +
611 +def process_svnlook_log(file)
612 +  svnlook("log", $revision) do |io|
613 +    io.each_line do |line|
614 +      file.puts("#> #{line}")
615 +    end
616 +  end
617 +end
618 +
619 +def process_svnlook_diff(file)
620 +  svnlook("diff", $revision) do |io|
621 +    lines = LineReader.new(io)
622 +    while lines.next_line
623 +      if lines.current =~ /^Modified:\s+(.*)/
624 +       read_modified_diff(file, lines, $1)
625 +      elsif lines.current =~ /^Added:\s+(.*)/
626 +       read_added_diff(file, lines, $1)
627 +      elsif lines.current =~ /^Copied:\s+(.*) \(from rev (\d+), (.*)\)$/
628 +       handle_copy(file, lines, $1, $2, $3)
629 +      elsif lines.current =~ /^Deleted:\s+(.*)/
630 +       read_deleted_diff(file, lines, $1)
631 +      elsif lines.current =~ /^Property changes on:\s+(.*)/
632 +       read_properties_changed(file, lines, $1)
633 +      elsif lines.current == "\n"
634 +       # ignore
635 +      else
636 +       raise "unable to parse line '#{lines.current.inspect}'"
637 +      end
638 +    end
639 +  end
640 +end
641 +
642 +def process_commit()
643 +  File.open("#{$datadir}/logfile", File::WRONLY|File::CREAT) do |file|
644 +    process_svnlook_log(file)
645 +    process_svnlook_diff(file)
646 +  end
647 +end
648 +
649 +
650 +def main
651 +  init()
652 +  process_args()
653 +  find_author()
654 +  process_commit()
655 +  send_email()
656 +  cleanup()
657 +end
658 +
659 +
660 +main
661
662 Property changes on: svn_post_commit_hook.rb
663 ___________________________________________________________________
664 Added: svn:mergeinfo
665 Added: svn:executable
666    + *
667
668
669 Property changes on: COPYING
670 ___________________________________________________________________
671 Deleted: svn:executable
672    - *
673
674 Index: CREDITS
675 ===================================================================
676 --- CREDITS     (.../tags/RELEASE-0_2_12)       (revision 275)
677 +++ CREDITS     (.../trunk)     (revision 275)
678 @@ -29,3 +29,10 @@
679    Elan Ruusamäe
680    Steve Fox
681    Christopher Petro
682 +  Robin Getz
683 +  Glen Starrett
684 +  Jonathan Rafkind
685 +  Ryan Dlugosz
686 +  Steve Woodcock
687 +  Andy Selle
688 +  Charles Duffy
689 Index: cvsspam-doc.xml
690 ===================================================================
691 --- cvsspam-doc.xml     (.../tags/RELEASE-0_2_12)       (revision 275)
692 +++ cvsspam-doc.xml     (.../trunk)     (revision 275)
693 @@ -452,6 +452,23 @@
694  </screen></informalexample>
695        </para>
696    </section>
697 +
698 +  <section>
699 +    <title>RT</title>
700 +
701 +    <para>For Gforge, when a CVS log comment contains text like <userinput>Fix
702 +    for Bug [#123]</userinput>, or <userinput>Task [T456] ...</userinput>, the
703 +    text "[#123]" or "[T456]" will become a hyper-link to that Gforge page in
704 +    the generated email.  The format [#<replaceable>nnn</replaceable>] and
705 +    [T<replaceable>nnn</replaceable>] is taken from the existing plugin for
706 +    Gforge called cvstracker.</para>
707 +
708 +    <para>To enable, give your Gforge's URL in CVSspam's configuration file:
709 +<informalexample><screen>$gforgeBugURL = "http://gforge.org/tracker/index.php?func=detail&amp;aid=%s"
710 +$gforgeTaskURL = "http://gforge.org/pm/task.php?func=detailtask&amp;project_task_id=%s"</screen></informalexample>
711 +    The marker %s tells CVSspam where in the URL to put the bugId from the
712 +    log message.</para>
713 +  </section>
714  </section>
715  
716  <section><title>CVS Web Frontends</title>
717
718 Property changes on: cvsspam-doc.xml
719 ___________________________________________________________________
720 Deleted: svn:executable
721    - *
722
723 Index: cvsspam.rb
724 ===================================================================
725 --- cvsspam.rb  (.../tags/RELEASE-0_2_12)       (revision 275)
726 +++ cvsspam.rb  (.../trunk)     (revision 275)
727 @@ -20,6 +20,7 @@
728  
729  $version = "0.2.12"
730  
731 +require 'time'
732  
733  $maxSubjectLength = 200
734  $maxLinesPerDiff = 1000
735 @@ -35,10 +36,6 @@
736    a<b ? a : b
737  end
738  
739 -# NB must ensure the time is UTC
740 -# (the Ruby Time object's strftime() doesn't supply a numeric timezone)
741 -DATE_HEADER_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
742 -
743  # Perform (possibly) multiple global substitutions on a string.
744  # the regexps given as keys must not use capturing subexpressions '(...)'
745  class MultiSub
746 @@ -48,7 +45,7 @@
747      @mash = Array.new
748      expr = nil
749      hash.each do |key,val|
750 -      if expr == nil ; expr="(" else expr<<"|(" end
751 +      if expr == nil ; expr="(" else expr << "|(" end
752        expr << key << ")"
753        @mash << val
754      end
755 @@ -116,6 +113,8 @@
756    UNDERSCORE = chr("_")
757    SPACE = chr(" ")
758    TAB = chr("\t")
759 +  HOOK = chr("?")
760 +  EQUALS = chr("=")
761  
762    # encode a header value according to the RFC-2047 quoted-printable spec,
763    # allowing non-ASCII characters to appear in header values, and wrapping
764 @@ -137,8 +136,8 @@
765    # return a string representing the given character-code in quoted-printable
766    # format
767    def quoted_encode_char(b)
768 -    if b>126 || b==UNDERSCORE || b==TAB
769 -      sprintf("=%02x", b)
770 +    if b>126 || b==UNDERSCORE || b==TAB || b==HOOK || b==EQUALS
771 +      sprintf("=%02X", b)
772      elsif b == SPACE
773        "_"
774      else
775 @@ -163,8 +162,9 @@
776  
777    # gives a string starting "=?", and including a charset specification, that
778    # marks the start of a quoted-printable character sequence
779 -  def marker_start_quoted
780 -    "=?#{@charset}?#{@encoding}?"
781 +  def marker_start_quoted(charset=nil)
782 +    charset = @charset if charset.nil?
783 +    "=?#{charset}?#{@encoding}?"
784    end
785  
786    # test to see of the given string contains non-ASCII characters
787 @@ -388,6 +388,7 @@
788  class FileEntry
789    def initialize(path)
790      @path = path
791 +    @fromVer = @toVer = nil
792      @lineAdditions = @lineRemovals = 0
793      @repository = Repository.get(path)
794      @repository.merge_common_prefix(basedir())
795 @@ -397,7 +398,7 @@
796  
797    # the full path and filename within the repository
798    attr_accessor :path
799 -  # the type of change committed 'M'=modified, 'A'=added, 'R'=removed
800 +  # the type of change committed 'M'=modified, 'A'=added, 'R'=removed, 'P'=properties, 'C'=copied
801    attr_accessor :type
802    # records number of 'addition' lines in diff output, once counted
803    attr_accessor :lineAdditions
804 @@ -452,17 +453,28 @@
805    def removal?
806      @type == "R"
807    end
808 -
809 +  
810    # was this file added during the commit?
811    def addition?
812      @type == "A"
813    end
814  
815 +  # was this file copied during the commit?
816 +  def copied?
817 +    @type == "C"
818 +  end
819 +
820    # was this file simply modified during the commit?
821    def modification?
822      @type == "M"
823    end
824 +  
825 +  # was this file simply modified during the commit?
826 +  def modifiedprops?
827 +    @type == "P"
828 +  end
829  
830 +
831    # passing true, this object remembers that a diff will appear in the email,
832    # passing false, this object remembers that no diff will appear in the email.
833    # Once the value is set, it will not be changed
834 @@ -533,6 +545,14 @@
835  # TODO: consolidate these into a nicer framework,
836  mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" }
837  urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" }
838 +gforgeTaskSub = proc { |match|
839 +  match =~ /([0-9]+)/
840 +  "<a href=\"#{$gforgeTaskURL.sub(/%s/, $1)}\">#{match}</a>"
841 +}
842 +gforgeBugSub = proc { |match|
843 +  match =~ /([0-9]+)/
844 +  "<a href=\"#{$gforgeBugURL.sub(/%s/, $1)}\">#{match}</a>"
845 +}
846  bugzillaSub = proc { |match|
847    match =~ /([0-9]+)/
848    "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>"
849 @@ -544,11 +564,27 @@
850    match =~ /([0-9]+)/
851    "<a href=\"#{$ticketURL.sub(/%s/, $1)}\">#{match}</a>"
852  }
853 +issueSub = proc { |match|
854 +  match =~ /([0-9]+)/
855 +  "<a href=\"#{$issueURL.sub(/%s/, $1)}\">#{match}</a>"
856 +}
857  wikiSub = proc { |match| 
858 -  match =~ /\[\[(.*)\]\]/
859 +  match =~ /\[\[(.*?)\]\]/
860    raw = $1
861    "<a href=\"#{$wikiURL.sub(/%s/, urlEncode(raw))}\">[[#{raw}]]</a>"
862  }
863 +xplannerIterationSub = proc { |match|
864 +  match =~ /([0-9]+)/
865 +  "<a href=\"#{$xplannerIterationURL.sub(/%s/, $1)}\">#{match}</a>"
866 +}
867 +xplannerProjectSub = proc { |match|
868 +  match =~ /([0-9]+)/
869 +  "<a href=\"#{$xplannerProjectURL.sub(/%s/, $1)}\">#{match}</a>"
870 +}
871 +xplannerStorySub = proc { |match|
872 +  match =~ /([0-9]+)/
873 +  "<a href=\"#{$xplannerStoryURL.sub(/%s/, $1)}\">#{match}</a>"
874 +}
875  commentSubstitutions = {
876                 '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub,
877                 '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub
878 @@ -670,6 +706,12 @@
879    def diff(file)
880      '-&gt;'
881    end
882 +
883 +  # may be overridden by subclasses that are able to make a hyperlink to a
884 +  # history log for a file
885 +  def log(file)
886 +    ''
887 +  end
888  end
889  
890  # Superclass for objects that can link to CVS frontends on the web (ViewCVS,
891 @@ -710,6 +752,14 @@
892      "<a href=\"#{diff_url(file)}\">#{super(file)}</a>"
893    end
894  
895 +  def log(file)
896 +    link = log_url(file)
897 +    if link
898 +      return "<span id=\"info\">(<a href=\"#{link}\">log</a>)</span>"
899 +    end
900 +    return nil
901 +  end
902 +
903   protected
904    def add_repo(url)
905      if @repository_name
906 @@ -722,6 +772,10 @@
907        url
908      end
909    end
910 +
911 +  def log_url(file)
912 +    nil
913 +  end
914  end
915  
916  # Link to ViewCVS
917 @@ -745,6 +799,15 @@
918    def diff_url(file)
919      add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=#{file.fromVer}&amp;r2=#{file.toVer}")
920    end
921 +
922 +  def log_url(file)
923 +    if file.toVer
924 +      log_anchor = "#rev#{file.toVer}"
925 +    else
926 +      log_anchor = ""
927 +    end
928 +    add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
929 +  end
930  end
931  
932  # Link to Chora, from the Horde framework
933 @@ -767,9 +830,9 @@
934  class CVSwebFrontend < WebFrontend
935    def path_url(path, tag)
936      if tag == nil
937 -      add_repo(@base_url + urlEncode(path))
938 +      add_repo(@base_url + urlEncode(path) + "/")
939      else
940 -      add_repo("#{@base_url}#{urlEncode(path)}?only_with_tag=#{urlEncode(tag)}")
941 +      add_repo("#{@base_url}#{urlEncode(path)}/?only_with_tag=#{urlEncode(tag)}")
942      end
943    end
944  
945 @@ -780,9 +843,45 @@
946    def diff_url(file)
947      add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=text&amp;tr1=#{file.fromVer}&amp;r2=text&amp;tr2=#{file.toVer}&amp;f=h")
948    end
949 +
950 +  protected
951 +
952 +  def log_url(file)
953 +    if file.toVer
954 +      log_anchor = "#rev#{file.toVer}"
955 +    else
956 +      log_anchor = ""
957 +    end
958 +    add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
959 +  end
960  end
961  
962 +# Link to Trac
963 +class TracFrontend < WebFrontend
964 +  def path_url(path, tag)
965 +    add_repo("#{@base_url}browser/#{urlEncode(path)}")
966 +  end
967  
968 +  def version_url(path, version)
969 +    add_repo("#{@base_url}browser/#{urlEncode(path)}?rev=#{version}")
970 +  end
971 +
972 +  def diff_url(file)
973 +    add_repo("#{@base_url}changeset/#{file.toVer}")
974 +  end
975 +
976 +  protected
977 +
978 +  def log_url(file)
979 +    if file.toVer
980 +      log_anchor = "?rev=#{file.toVer}"
981 +    else
982 +      log_anchor = ""
983 +    end
984 +    add_repo("#{@base_url}log/#{urlEncode(file.path)}#{log_anchor}")
985 +  end
986 +end
987 +
988  # in need of refactoring...
989  
990  # Note when LogReader finds record of a file that was added in this commit
991 @@ -801,6 +900,15 @@
992    end
993  end
994  
995 +# Note when LogReader finds record of a file that was copied in this commit
996 +class CopiedFileHandler < FileHandler
997 +  def handleFile(file)
998 +    file.type="C"
999 +    file.fromVer=$fromVer
1000 +    file.toVer=$toVer
1001 +  end
1002 +end
1003 +
1004  # Note when LogReader finds record of a file that was modified in this commit
1005  class ModifiedFileHandler < FileHandler
1006    def handleFile(file)
1007 @@ -810,7 +918,16 @@
1008    end
1009  end
1010  
1011 +# Note when LogReader finds record of a file whose properties were modified in this commit
1012 +class ModifiedPropsFileHandler < FileHandler
1013 +  def handleFile(file)
1014 +    file.type="P"
1015 +    file.fromVer=$fromVer
1016 +    file.toVer=$toVer
1017 +  end
1018 +end
1019  
1020 +
1021  # Used by UnifiedDiffHandler to record the number of added and removed lines
1022  # appearing in a unidiff.
1023  class UnifiedDiffStats
1024 @@ -873,7 +990,10 @@
1025          addInfixSize = line.length - (prefixLen+suffixLen)
1026          oversize_change = deleteInfixSize*100/@lineJustDeleted.length>33 || addInfixSize*100/line.length>33
1027  
1028 -        if prefixLen==1 && suffixLen==0 || deleteInfixSize<=0 || oversize_change
1029 +        # avoid doing 'within-a-line highlighting' if a multibyte encoding
1030 +        # is suspected, as all the suffix/prefix stuff above is byte, not
1031 +        # character based
1032 +        if multibyte_encoding? || prefixLen==1 && suffixLen==0 || deleteInfixSize<=0 || oversize_change
1033            print(htmlEncode(@lineJustDeleted))
1034          else
1035            print(htmlEncode(@lineJustDeleted[0,prefixLen]))
1036 @@ -905,7 +1025,7 @@
1037          @lineJustDeleted = nil
1038        end
1039        shift(initial)
1040 -      if prefixLen==1 && suffixLen==0 || addInfixSize<=0 || oversize_change
1041 +      if multibyte_encoding? || prefixLen==1 && suffixLen==0 || addInfixSize<=0 || oversize_change
1042          encoded = htmlEncode(line)
1043        else
1044          encoded = htmlEncode(line[0,prefixLen]) +
1045 @@ -958,7 +1078,7 @@
1046      end
1047      shift(nil)
1048      if @truncatedLineCount>0
1049 -      println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partialy shown]</strong>")
1050 +      println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partially shown]</strong>")
1051      end
1052    end
1053  
1054 @@ -976,11 +1096,21 @@
1055          print($frontend.path($file.basedir, $file.tag))
1056          println("</span><br />")
1057          println("<div class=\"fileheader\" id=\"removed\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">removed after #{$frontend.version($file.path,$file.fromVer)}</small></div>")
1058 +      when "C"
1059 +        print("<span class=\"pathname\" id=\"copied\">")
1060 +        print($frontend.path($file.basedir, $file.tag))
1061 +        println("</span><br />")
1062 +        println("<div class=\"fileheader\" id=\"copied\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">copied from #{$frontend.version($file.path,$file.fromVer)}</small></div>")
1063        when "M"
1064          print("<span class=\"pathname\">")
1065          print($frontend.path($file.basedir, $file.tag))
1066          println("</span><br />")
1067          println("<div class=\"fileheader\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">#{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}</small></div>")
1068 +      when "P"
1069 +        print("<span class=\"pathname\">")
1070 +        print($frontend.path($file.basedir, $file.tag))
1071 +        println("</span><br />")
1072 +        println("<div class=\"fileheader\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">#{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}</small></div>")
1073      end
1074      print("<pre class=\"diff\"><small id=\"info\">")
1075      lines.each do |line|
1076 @@ -1181,7 +1311,7 @@
1077  
1078  # an RFC 822 email address
1079  class EmailAddress
1080 -  def initialize(text)
1081 +  def initialize(text, charset=nil)
1082      if text =~ /^\s*([^<]+?)\s*<\s*([^>]+?)\s*>\s*$/
1083        @personal_name = $1
1084        @address = $2
1085 @@ -1189,9 +1319,10 @@
1086        @personal_name = nil
1087        @address = text
1088      end
1089 +    @charset=charset
1090    end
1091  
1092 -  attr_accessor :personal_name, :address
1093 +  attr_accessor :personal_name, :address, :charset
1094  
1095    def has_personal_name?
1096      return !@personal_name.nil?
1097 @@ -1222,7 +1353,7 @@
1098    # rfc2047 encode the word, if it contains non-ASCII characters
1099    def encode_word(word)
1100      if $encoder.requires_rfc2047?(word)
1101 -      encoded = $encoder.marker_start_quoted
1102 +      encoded = $encoder.marker_start_quoted(@charset)
1103        $encoder.each_char_encoded(word) do |code|
1104         encoded << code
1105        end
1106 @@ -1233,26 +1364,40 @@
1107    end
1108  end
1109  
1110 +# guess if the users selected encoding is multibyte, since some CVSspam code
1111 +# isn't multibyte-safe, and needs to be disabled.
1112 +def multibyte_encoding?
1113 +  $charset && ["utf-8", "utf-16"].include?($charset.downcase)
1114 +end
1115  
1116  cvsroot_dir = "#{ENV['CVSROOT']}/CVSROOT"
1117  $config = "#{cvsroot_dir}/cvsspam.conf"
1118  $users_file = "#{cvsroot_dir}/users"
1119 +$users_file_charset = nil
1120  
1121  $debug = false
1122 +$svn = false
1123  $recipients = Array.new
1124  $sendmail_prog = "/usr/sbin/sendmail"
1125  $hostname = ENV['HOSTNAME'] || 'localhost'
1126  $no_removed_file_diff = false
1127  $no_added_file_diff = false
1128  $no_diff = false
1129 -$task_keywords = ['TODO', 'FIXME']
1130 +$task_keywords = ['TODO', 'FIXME', 'FIXIT', 'todo']
1131  $bugzillaURL = nil
1132 +$gforgeBugURL = nil
1133 +$gforgeTaskURL = nil
1134  $wikiURL = nil
1135  $jiraURL = nil
1136  $ticketURL = nil
1137 +$issueURL = nil
1138  $viewcvsURL = nil
1139 +$xplannerIterationURL = nil
1140 +$xplannerProjectURL = nil
1141 +$xplannerStoryURL = nil
1142  $choraURL = nil
1143  $cvswebURL = nil
1144 +$tracURL = nil
1145  $from_address = nil
1146  $subjectPrefix = nil
1147  $files_in_subject = false;
1148 @@ -1261,6 +1406,7 @@
1149  # 2MiB limit on attached diffs,
1150  $mail_size_limit = 1024 * 1024 * 2
1151  $arg_charset = nil
1152 +$cvsroot_email_header = false
1153  
1154  require 'getoptlong'
1155  
1156 @@ -1268,6 +1414,7 @@
1157    [ "--to",     "-t", GetoptLong::REQUIRED_ARGUMENT ],
1158    [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
1159    [ "--debug",  "-d", GetoptLong::NO_ARGUMENT ],
1160 +  [ "--svn",    "-s", GetoptLong::NO_ARGUMENT ],
1161    [ "--from",   "-u", GetoptLong::REQUIRED_ARGUMENT ],
1162    [ "--charset",      GetoptLong::REQUIRED_ARGUMENT ]
1163  )
1164 @@ -1276,6 +1423,7 @@
1165    $recipients << EmailAddress.new(arg) if opt=="--to"
1166    $config = arg if opt=="--config"
1167    $debug = true if opt=="--debug"
1168 +  $svn = true if opt=="--svn"
1169    $from_address = EmailAddress.new(arg) if opt=="--from"
1170    # must use different variable as the config is readed later.
1171    $arg_charset = arg if opt == "--charset"
1172 @@ -1288,7 +1436,7 @@
1173    else
1174      $stderr.puts "missing required file argument"
1175    end
1176 -  puts "Usage: cvsspam.rb [ --to <email> ] [ --config <file> ] <collect_diffs file>"
1177 +  puts "Usage: cvsspam.rb [ --svn ] [ --to <email> ] [ --config <file> ] <collect_diffs file>"
1178    exit(-1)
1179  end
1180  
1181 @@ -1321,6 +1469,8 @@
1182    blah("Config file '#{$config}' not found, ignoring")
1183  end
1184  
1185 +blah("Users file: '#{$users_file}'")
1186 +
1187  unless $arg_charset.nil?
1188    $charset = $arg_charset
1189  end
1190 @@ -1337,6 +1487,9 @@
1191  elsif $cvswebURL !=nil
1192    $cvswebURL << "/" unless $cvswebURL =~ /\/$/
1193    $frontend = CVSwebFrontend.new($cvswebURL)
1194 +elsif $tracURL !=nil
1195 +  $tracURL << "/" unless $tracURL =~ /\/$/
1196 +  $frontend = TracFrontend.new($tracURL)
1197  else
1198    $frontend = NoFrontend.new
1199  end
1200 @@ -1353,17 +1506,35 @@
1201  
1202  
1203  if $bugzillaURL != nil
1204 -  commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugzillaSub
1205 +  commentSubstitutions['\b[Bb](?:[Uu][Gg])?\s*[#:]?\s*\[?[0-9]+\]?'] = bugzillaSub
1206  end
1207 +if $gforgeBugURL != nil
1208 +  commentSubstitutions['\B\[#[0-9]+\]'] = gforgeBugSub
1209 +end
1210 +if $gforgeTaskURL != nil
1211 +  commentSubstitutions['\B\[[Tt][0-9]+\]'] = gforgeTaskSub
1212 +end
1213  if $jiraURL != nil
1214    commentSubstitutions['\b[a-zA-Z]+-[0-9]+\b'] = jiraSub
1215  end
1216  if $ticketURL != nil
1217    commentSubstitutions['\b[Tt][Ii][Cc][Kk][Ee][Tt]\s*#?[0-9]+\b'] = ticketSub
1218  end
1219 +if $issueURL != nil
1220 +  commentSubstitutions['\b[Ii][Ss][Ss][Uu][Ee]\s*#?[0-9]+\b'] = issueSub
1221 +end
1222  if $wikiURL != nil
1223    commentSubstitutions['\[\[.+\]\]'] = wikiSub
1224  end
1225 +if $xplannerIterationURL != nil
1226 +  commentSubstitutions['\bXI\[?[0-9]+\]?'] = xplannerIterationSub
1227 +end
1228 +if $xplannerProjectURL != nil
1229 +  commentSubstitutions['\bXP\[?[0-9]+\]?'] = xplannerProjectSub
1230 +end
1231 +if $xplannerStoryURL != nil
1232 +  commentSubstitutions['\bXS\[?[0-9]+\]?'] = xplannerStorySub
1233 +end
1234  $commentEncoder = MultiSub.new(commentSubstitutions)
1235  
1236  
1237 @@ -1374,12 +1545,16 @@
1238                  "T" => tagHandler,
1239                  "A" => AddedFileHandler.new,
1240                  "R" => RemovedFileHandler.new,
1241 +                "C" => CopiedFileHandler.new,
1242                  "M" => ModifiedFileHandler.new,
1243 +                "P" => ModifiedPropsFileHandler.new,
1244                  "V" => VersionHandler.new]
1245  
1246  $handlers["A"].setTagHandler(tagHandler)
1247  $handlers["R"].setTagHandler(tagHandler)
1248 +$handlers["C"].setTagHandler(tagHandler)
1249  $handlers["M"].setTagHandler(tagHandler)
1250 +$handlers["P"].setTagHandler(tagHandler)
1251  
1252  $fileEntries = Array.new
1253  $task_list = Array.new
1254 @@ -1404,7 +1579,11 @@
1255  end
1256  
1257  if $subjectPrefix == nil
1258 -  $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
1259 +  if $svn
1260 +    $subjectPrefix = "[SVN #{Repository.array.join(',')}]"
1261 +  else
1262 +    $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
1263 +  end
1264  end
1265  
1266  if $files_in_subject
1267 @@ -1451,6 +1630,8 @@
1268    #removed {background-color:#ffdddd;}
1269    #removedchars {background-color:#ff9999;font-weight:bolder;}
1270    tr.alt #removed {background-color:#f7cccc;}
1271 +  #copied {background-color:#ccccff;}
1272 +  tr.alt #copied {background-color:#bbbbf7;}
1273    #info {color:#888888;}
1274    #context {background-color:#eeeeee;}
1275    td {padding-left:.3em;padding-right:.3em;}
1276 @@ -1483,7 +1664,9 @@
1277  
1278    filesAdded = 0
1279    filesRemoved = 0
1280 +  filesCopied = 0
1281    filesModified  = 0
1282 +  filesModifiedProps  = 0
1283    totalLinesAdded = 0
1284    totalLinesRemoved = 0
1285    file_count = 0
1286 @@ -1492,24 +1675,26 @@
1287    $fileEntries.each do |file|
1288      unless file.repository == last_repository
1289        last_repository = file.repository
1290 -      mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 5 : 4}\">")
1291 +      mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 6 : 5}\">")
1292        if last_repository.has_multiple_tags
1293          mail.print("Mixed-tag commit")
1294        else
1295          mail.print("Commit")
1296        end
1297        mail.print(" in <b><tt>#{htmlEncode(last_repository.common_prefix)}</tt></b>")
1298 -      if last_repository.trunk_only?
1299 -        mail.print("<span id=\"info\"> on MAIN</span>")
1300 -      else
1301 -        mail.print(" on ")
1302 -        tagCount = 0
1303 -        last_repository.each_tag do |tag|
1304 -          tagCount += 1
1305 -          if tagCount > 1
1306 -            mail.print tagCount<last_repository.tag_count ? ", " : " &amp; "
1307 +      if !$svn
1308 +        if last_repository.trunk_only?
1309 +          mail.print("<span id=\"info\"> on MAIN</span>")
1310 +        else
1311 +          mail.print(" on ")
1312 +          tagCount = 0
1313 +          last_repository.each_tag do |tag|
1314 +            tagCount += 1
1315 +            if tagCount > 1
1316 +              mail.print tagCount<last_repository.tag_count ? ", " : " &amp; "
1317 +            end
1318 +            mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>"
1319            end
1320 -          mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>"
1321          end
1322        end
1323        mail.puts("</td></tr>")
1324 @@ -1524,8 +1709,12 @@
1325        filesAdded += 1
1326      elsif file.removal?
1327        filesRemoved += 1
1328 +    elsif file.copied?
1329 +      filesCopied += 1
1330      elsif file.modification?
1331        filesModified += 1
1332 +    elsif file.modifiedprops?
1333 +      filesModifiedProps += 1
1334      end
1335      name = htmlEncode(file.name_after_common_prefix)
1336      slashPos = name.rindex("/")
1337 @@ -1545,17 +1734,29 @@
1338        name = "<span id=\"added\">#{name}</span>"
1339      elsif file.removal?
1340        name = "<span id=\"removed\">#{name}</span>"
1341 +    elsif file.copied?
1342 +      name = "<span id=\"copied\">#{name}</span>"
1343      end
1344 +    mail.print("<td>")
1345      if file.has_diff?
1346 -      mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt></td>")
1347 +      mail.print("<tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt>")
1348      else
1349 -      mail.print("<td><tt>#{prefix}#{name}</tt></td>")
1350 +      mail.print("<tt>#{prefix}#{name}</tt>")
1351      end
1352 -    if file.isEmpty
1353 -      mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
1354 +    mail.print(" #{$frontend.log(file)}")
1355 +    mail.print("</td>")
1356 +    if file.copied?
1357 +      mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[copied]</small></td>")
1358 +    elsif file.isEmpty
1359 +      mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[empty]</small></td>")
1360      elsif file.isBinary
1361 -      mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[binary]</small></td>")
1362 +      mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[binary]</small></td>")
1363      else
1364 +      if file.modifiedprops?
1365 +        mail.print("<td align=\"right\"><small id=\"info\">[props]</small></td>")
1366 +      else
1367 +        mail.print("<td></td>")
1368 +      end
1369        if file.lineAdditions>0
1370          totalLinesAdded += file.lineAdditions
1371          mail.print("<td align=\"right\" id=\"added\">+#{file.lineAdditions}</td>")
1372 @@ -1582,15 +1783,19 @@
1373        mail.print("<td nowrap=\"nowrap\" align=\"right\">added #{$frontend.version(file.path,file.toVer)}</td>")
1374      elsif file.removal?
1375        mail.print("<td nowrap=\"nowrap\">#{$frontend.version(file.path,file.fromVer)} removed</td>")
1376 +    elsif file.copied?
1377 +      mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
1378      elsif file.modification?
1379        mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
1380 +    elsif file.modifiedprops?
1381 +      mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
1382      end
1383  
1384      mail.puts("</tr>")
1385    end
1386    if $fileEntries.size>1 && (totalLinesAdded+totalLinesRemoved)>0
1387      # give total number of lines added/removed accross all files
1388 -    mail.print("<tr><td></td>")
1389 +    mail.print("<tr><td></td><td></td>")
1390      if totalLinesAdded>0
1391        mail.print("<td align=\"right\" id=\"added\">+#{totalLinesAdded}</td>")
1392      else
1393 @@ -1607,7 +1812,7 @@
1394    
1395    mail.puts("</table>")
1396  
1397 -  totalFilesChanged = filesAdded+filesRemoved+filesModified
1398 +  totalFilesChanged = filesAdded+filesRemoved+filesCopied+filesModified+filesModifiedProps
1399    if totalFilesChanged > 1
1400      mail.print("<small id=\"info\">")
1401      changeKind = 0
1402 @@ -1620,11 +1825,21 @@
1403        mail.print("#{filesRemoved} removed")
1404        changeKind += 1
1405      end
1406 +    if filesCopied>0
1407 +      mail.print(" + ") if changeKind>0
1408 +      mail.print("#{filesCopied} copied")
1409 +      changeKind += 1
1410 +    end
1411      if filesModified>0
1412        mail.print(" + ") if changeKind>0
1413        mail.print("#{filesModified} modified")
1414        changeKind += 1
1415      end
1416 +    if filesModifiedProps>0
1417 +      mail.print(" + ") if changeKind>0
1418 +      mail.print("#{filesModifiedProps} modified properties")
1419 +      changeKind += 1
1420 +    end
1421      mail.print(", total #{totalFilesChanged}") if changeKind > 1
1422      mail.puts(" files</small><br />")
1423    end
1424 @@ -1667,12 +1882,13 @@
1425  # CVSROOT/users file, if the file exists.  The argument is returned unchanged
1426  # if no alias is found.
1427  def sender_alias(email)
1428 +  blah("Lookup '#{email}' from users file")
1429    if File.exists?($users_file)
1430      File.open($users_file) do |io|
1431        io.each_line do |line|
1432          if line =~ /^([^:]+)\s*:\s*(['"]?)([^\n\r]+)(\2)/
1433            if email.address == $1
1434 -            return EmailAddress.new($3)
1435 +            return EmailAddress.new($3, $users_file_charset)
1436            end
1437          end
1438        end
1439 @@ -1686,6 +1902,8 @@
1440  # sensible header formatting, and for ensuring that the body is seperated
1441  # from the message headers by a blank line (as it is required to be).
1442  class MailContext
1443 +  ENCODE_HEADERS = ["Subject", "X-CVSspam-Module-Path"]
1444 +
1445    def initialize(io)
1446      @done_headers = false
1447      @io = io
1448 @@ -1695,8 +1913,8 @@
1449    # called
1450    def header(name, value)
1451      raise "headers already commited" if @done_headers
1452 -    if name == "Subject"
1453 -      $encoder.encode_header(@io, "Subject", value)
1454 +    if ENCODE_HEADERS.include?(name)
1455 +      $encoder.encode_header(@io, name, value)
1456      else
1457        @io.puts("#{name}: #{value}")
1458      end
1459 @@ -1769,7 +1987,7 @@
1460        ctx.header("To", recipients.map{|addr| addr.encoded}.join(','))
1461        blah("Mail From: <#{from}>")
1462        ctx.header("From", from.encoded) if from
1463 -      ctx.header("Date", Time.now.utc.strftime(DATE_HEADER_FORMAT))
1464 +      ctx.header("Date", Time.now.rfc2822)
1465        yield ctx
1466      end
1467    end
1468 @@ -1800,10 +2018,10 @@
1469    return unless $fileEntries.length == 1
1470    file = $fileEntries[0]
1471    name = zap_header_special_chars(file.path)
1472 -  unless file.fromVer == "NONE"
1473 +  if file.fromVer
1474      mail.header("References", make_msg_id("#{name}.#{file.fromVer}", $hostname))
1475    end
1476 -  unless file.toVer == "NONE"
1477 +  if file.toVer
1478      mail.header("Message-ID", make_msg_id("#{name}.#{file.toVer}", $hostname))
1479    end
1480  end
1481 @@ -1834,6 +2052,14 @@
1482      end
1483    end
1484    mail.header("X-Mailer", "CVSspam #{$version} <http://www.badgers-in-foil.co.uk/projects/cvsspam/>")
1485 +  if $cvsroot_email_header
1486 +    mod = '/'
1487 +    if Repository.count == 1
1488 +      rep = Repository.array.first
1489 +      mod << rep.common_prefix
1490 +    end
1491 +    mail.header("X-CVSspam-Module-Path", mod)
1492 +  end
1493  
1494    mail.body do |body|
1495      make_html_email(body)
1496
1497 Property changes on: testcases/data/remove.png
1498 ___________________________________________________________________
1499 Deleted: svn:executable
1500    - *
1501
1502
1503 Property changes on: testcases/data/fiddlyedits.after
1504 ___________________________________________________________________
1505 Deleted: svn:executable
1506    - *
1507
1508
1509 Property changes on: testcases/data/fiddlyedits.before
1510 ___________________________________________________________________
1511 Deleted: svn:executable
1512    - *
1513
1514
1515 Property changes on: testcases/data/add.png
1516 ___________________________________________________________________
1517 Deleted: svn:executable
1518    - *
1519
1520
1521 Property changes on: testcases/README
1522 ___________________________________________________________________
1523 Deleted: svn:executable
1524    - *
1525
1526
1527 Property changes on: .
1528 ___________________________________________________________________
1529 Added: svn:ignore
1530    + .project
1531
1532
This page took 0.194553 seconds and 3 git commands to generate.