]>
Commit | Line | Data |
---|---|---|
3eeef56f ER |
1 | --- cvsspam-0.2.12/svn_cvsspam.rb 2005-07-11 18:53:29.000000000 +0300 |
2 | +++ cvsspam-svn/svn_cvsspam.rb 2008-08-07 17:27:52.632725455 +0300 | |
3 | @@ -18,7 +18,7 @@ | |
4 | # to your cvssppam.conf | |
5 | ||
6 | ||
7 | -$version = "0.2.11" | |
8 | +$version = "0.2.12" | |
9 | ||
10 | ||
11 | $maxSubjectLength = 200 | |
12 | @@ -339,8 +339,11 @@ | |
13 | ||
14 | # gets the Repository object for the first component of the given path | |
15 | def Repository.get(name) | |
16 | - name =~ /^[^\/]+/ | |
17 | - name = $& | |
18 | + # Leading './' is ignored (for peeps who have done 'cvs checkout .') | |
19 | + # Trailing '/' ensures no match for files in root (we just want dirs) | |
20 | + name =~ /^(?:\.\/)?([^\/]+)\// | |
21 | + name = $1 | |
22 | + name = "/" if name.nil? # file at top-level? fake up a name for repo | |
23 | rep = @@repositories[name] | |
24 | if rep.nil? | |
25 | rep = Repository.new(name) | |
26 | @@ -385,6 +388,7 @@ | |
27 | class FileEntry | |
28 | def initialize(path) | |
29 | @path = path | |
30 | + @fromVer = @toVer = nil | |
31 | @lineAdditions = @lineRemovals = 0 | |
32 | @repository = Repository.get(path) | |
33 | @repository.merge_common_prefix(basedir()) | |
34 | @@ -394,7 +398,7 @@ | |
35 | ||
36 | # the full path and filename within the repository | |
37 | attr_accessor :path | |
38 | - # the type of change committed 'M'=modified, 'A'=added, 'R'=removed | |
39 | + # the type of change committed 'M'=modified, 'A'=added, 'R'=removed, 'P'=properties, 'C'=copied | |
40 | attr_accessor :type | |
41 | # records number of 'addition' lines in diff output, once counted | |
42 | attr_accessor :lineAdditions | |
43 | @@ -412,7 +416,7 @@ | |
44 | # works out the filename part of #path | |
45 | def file | |
46 | @path =~ /.*\/(.*)/ | |
47 | - $1 || @path | |
48 | + $1 | |
49 | end | |
50 | ||
51 | # set the branch on which this change was committed, and add it to the list | |
52 | @@ -430,7 +434,7 @@ | |
53 | # works out the directory part of #path | |
54 | def basedir | |
55 | @path =~ /(.*)\/.*/ | |
56 | - $1 || "/" | |
57 | + $1 | |
58 | end | |
59 | ||
60 | # gives the Repository object this file was automatically associated with | |
61 | @@ -449,16 +453,27 @@ | |
62 | def removal? | |
63 | @type == "R" | |
64 | end | |
65 | - | |
66 | + | |
67 | # was this file added during the commit? | |
68 | def addition? | |
69 | @type == "A" | |
70 | end | |
71 | ||
72 | + # was this file copied during the commit? | |
73 | + def copied? | |
74 | + @type == "C" | |
75 | + end | |
76 | + | |
77 | # was this file simply modified during the commit? | |
78 | def modification? | |
79 | @type == "M" | |
80 | end | |
81 | + | |
82 | + # was this file simply modified during the commit? | |
83 | + def modifiedprops? | |
84 | + @type == "P" | |
85 | + end | |
86 | + | |
87 | ||
88 | # passing true, this object remembers that a diff will appear in the email, | |
89 | # passing false, this object remembers that no diff will appear in the email. | |
90 | @@ -530,6 +545,14 @@ | |
91 | # TODO: consolidate these into a nicer framework, | |
92 | mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" } | |
93 | urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" } | |
94 | +gforgeTaskSub = proc { |match| | |
95 | + match =~ /([0-9]+)/ | |
96 | + "<a href=\"#{$gforgeTaskURL.sub(/%s/, $1)}\">#{match}</a>" | |
97 | +} | |
98 | +gforgeBugSub = proc { |match| | |
99 | + match =~ /([0-9]+)/ | |
100 | + "<a href=\"#{$gforgeBugURL.sub(/%s/, $1)}\">#{match}</a>" | |
101 | +} | |
102 | bugzillaSub = proc { |match| | |
103 | match =~ /([0-9]+)/ | |
104 | "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>" | |
105 | @@ -541,9 +564,15 @@ | |
106 | match =~ /([0-9]+)/ | |
107 | "<a href=\"#{$ticketURL.sub(/%s/, $1)}\">#{match}</a>" | |
108 | } | |
109 | +wikiSub = proc { |match| | |
110 | + match =~ /\[\[(.*)\]\]/ | |
111 | + raw = $1 | |
112 | + "<a href=\"#{$wikiURL.sub(/%s/, urlEncode(raw))}\">[[#{raw}]]</a>" | |
113 | +} | |
114 | commentSubstitutions = { | |
115 | '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub, | |
116 | - '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub} | |
117 | + '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub | |
118 | + } | |
119 | ||
120 | # outputs commit log comment text supplied by LogReader as preformatted HTML | |
121 | class CommentHandler < LineConsumer | |
122 | @@ -661,6 +690,12 @@ | |
123 | def diff(file) | |
124 | '->' | |
125 | end | |
126 | + | |
127 | + # may be overridden by subclasses that are able to make a hyperlink to a | |
128 | + # history log for a file | |
129 | + def log(file) | |
130 | + '' | |
131 | + end | |
132 | end | |
133 | ||
134 | # Superclass for objects that can link to CVS frontends on the web (ViewCVS, | |
135 | @@ -701,6 +736,14 @@ | |
136 | "<a href=\"#{diff_url(file)}\">#{super(file)}</a>" | |
137 | end | |
138 | ||
139 | + def log(file) | |
140 | + link = log_url(file) | |
141 | + if link | |
142 | + return "<span id=\"info\">(<a href=\"#{link}\">log</a>)</span>" | |
143 | + end | |
144 | + return nil | |
145 | + end | |
146 | + | |
147 | protected | |
148 | def add_repo(url) | |
149 | if @repository_name | |
150 | @@ -713,6 +756,10 @@ | |
151 | url | |
152 | end | |
153 | end | |
154 | + | |
155 | + def log_url(file) | |
156 | + nil | |
157 | + end | |
158 | end | |
159 | ||
160 | # Link to ViewCVS | |
161 | @@ -771,6 +818,17 @@ | |
162 | def diff_url(file) | |
163 | add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=text&tr1=#{file.fromVer}&r2=text&tr2=#{file.toVer}&f=h") | |
164 | end | |
165 | + | |
166 | + protected | |
167 | + | |
168 | + def log_url(file) | |
169 | + if file.toVer | |
170 | + log_anchor = "#rev#{file.toVer}" | |
171 | + else | |
172 | + log_anchor = "" | |
173 | + end | |
174 | + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}") | |
175 | + end | |
176 | end | |
177 | ||
178 | ||
179 | @@ -792,6 +850,15 @@ | |
180 | end | |
181 | end | |
182 | ||
183 | +# Note when LogReader finds record of a file that was copied in this commit | |
184 | +class CopiedFileHandler < FileHandler | |
185 | + def handleFile(file) | |
186 | + file.type="C" | |
187 | + file.fromVer=$fromVer | |
188 | + file.toVer=$toVer | |
189 | + end | |
190 | +end | |
191 | + | |
192 | # Note when LogReader finds record of a file that was modified in this commit | |
193 | class ModifiedFileHandler < FileHandler | |
194 | def handleFile(file) | |
195 | @@ -801,6 +868,15 @@ | |
196 | end | |
197 | end | |
198 | ||
199 | +# Note when LogReader finds record of a file whose properties were modified in this commit | |
200 | +class ModifiedPropsFileHandler < FileHandler | |
201 | + def handleFile(file) | |
202 | + file.type="P" | |
203 | + file.fromVer=$fromVer | |
204 | + file.toVer=$toVer | |
205 | + end | |
206 | +end | |
207 | + | |
208 | ||
209 | # Used by UnifiedDiffHandler to record the number of added and removed lines | |
210 | # appearing in a unidiff. | |
211 | @@ -967,11 +1043,21 @@ | |
212 | print($frontend.path($file.basedir, $file.tag)) | |
213 | println("</span><br />") | |
214 | 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>") | |
215 | + when "C" | |
216 | + print("<span class=\"pathname\" id=\"copied\">") | |
217 | + print($frontend.path($file.basedir, $file.tag)) | |
218 | + println("</span><br />") | |
219 | + 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>") | |
220 | when "M" | |
221 | print("<span class=\"pathname\">") | |
222 | print($frontend.path($file.basedir, $file.tag)) | |
223 | println("</span><br />") | |
224 | 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>") | |
225 | + when "P" | |
226 | + print("<span class=\"pathname\">") | |
227 | + print($frontend.path($file.basedir, $file.tag)) | |
228 | + println("</span><br />") | |
229 | + 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>") | |
230 | end | |
231 | print("<pre class=\"diff\"><small id=\"info\">") | |
232 | lines.each do |line| | |
233 | @@ -1045,7 +1131,7 @@ | |
234 | else | |
235 | @stats.consume(line) | |
236 | if $file.wants_diff_in_mail? | |
237 | - if @stats.diffLines < $maxLinesPerDiff | |
238 | + if $maxLinesPerDiff.nil? || @stats.diffLines < $maxLinesPerDiff | |
239 | @colour.consume(line) | |
240 | elsif @stats.diffLines == $maxLinesPerDiff | |
241 | @colour.consume(line) | |
242 | @@ -1062,7 +1148,7 @@ | |
243 | $file.isBinary = true | |
244 | else | |
245 | if $file.wants_diff_in_mail? | |
246 | - if @stats.diffLines > $maxLinesPerDiff | |
247 | + if $maxLinesPerDiff && @stats.diffLines > $maxLinesPerDiff | |
248 | println("</pre>") | |
249 | println("<strong class=\"error\">[truncated at #{$maxLinesPerDiff} lines; #{@stats.diffLines-$maxLinesPerDiff} more skipped]</strong>") | |
250 | else | |
251 | @@ -1230,13 +1316,18 @@ | |
252 | $users_file = "#{cvsroot_dir}/users" | |
253 | ||
254 | $debug = false | |
255 | +$svn = false | |
256 | $recipients = Array.new | |
257 | $sendmail_prog = "/usr/sbin/sendmail" | |
258 | +$hostname = ENV['HOSTNAME'] || 'localhost' | |
259 | $no_removed_file_diff = false | |
260 | $no_added_file_diff = false | |
261 | $no_diff = false | |
262 | -$task_keywords = ['TODO', 'FIXME'] | |
263 | +$task_keywords = ['TODO', 'FIXME', 'FIXIT', 'todo'] | |
264 | $bugzillaURL = nil | |
265 | +$gforgeBugURL = nil | |
266 | +$gforgeTaskURL = nil | |
267 | +$wikiURL = nil | |
268 | $jiraURL = nil | |
269 | $ticketURL = nil | |
270 | $viewcvsURL = nil | |
271 | @@ -1257,6 +1348,7 @@ | |
272 | [ "--to", "-t", GetoptLong::REQUIRED_ARGUMENT ], | |
273 | [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ], | |
274 | [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], | |
275 | + [ "--svn", "-s", GetoptLong::NO_ARGUMENT ], | |
276 | [ "--from", "-u", GetoptLong::REQUIRED_ARGUMENT ], | |
277 | [ "--charset", GetoptLong::REQUIRED_ARGUMENT ] | |
278 | ) | |
279 | @@ -1265,6 +1357,7 @@ | |
280 | $recipients << EmailAddress.new(arg) if opt=="--to" | |
281 | $config = arg if opt=="--config" | |
282 | $debug = true if opt=="--debug" | |
283 | + $svn = true if opt=="--svn" | |
284 | $from_address = EmailAddress.new(arg) if opt=="--from" | |
285 | # must use different variable as the config is readed later. | |
286 | $arg_charset = arg if opt == "--charset" | |
287 | @@ -1277,12 +1370,13 @@ | |
288 | else | |
289 | $stderr.puts "missing required file argument" | |
290 | end | |
291 | - puts "Usage: cvsspam.rb [ --to <email> ] [ --config <file> ] <collect_diffs file>" | |
292 | + puts "Usage: cvsspam.rb [ --svn ] [ --to <email> ] [ --config <file> ] <collect_diffs file>" | |
293 | exit(-1) | |
294 | end | |
295 | ||
296 | $logfile = ARGV[0] | |
297 | ||
298 | + | |
299 | $additionalHeaders = Array.new | |
300 | $problemHeaders = Array.new | |
301 | ||
302 | @@ -1343,12 +1437,21 @@ | |
303 | if $bugzillaURL != nil | |
304 | commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugzillaSub | |
305 | end | |
306 | +if $gforgeBugURL != nil | |
307 | + commentSubstitutions['\B\[#[0-9]+\]'] = gforgeBugSub | |
308 | +end | |
309 | +if $gforgeTaskURL != nil | |
310 | + commentSubstitutions['\B\[[Tt][0-9]+\]'] = gforgeTaskSub | |
311 | +end | |
312 | if $jiraURL != nil | |
313 | commentSubstitutions['\b[a-zA-Z]+-[0-9]+\b'] = jiraSub | |
314 | end | |
315 | if $ticketURL != nil | |
316 | commentSubstitutions['\b[Tt][Ii][Cc][Kk][Ee][Tt]\s*#?[0-9]+\b'] = ticketSub | |
317 | end | |
318 | +if $wikiURL != nil | |
319 | + commentSubstitutions['\[\[.+\]\]'] = wikiSub | |
320 | +end | |
321 | $commentEncoder = MultiSub.new(commentSubstitutions) | |
322 | ||
323 | ||
324 | @@ -1359,12 +1462,16 @@ | |
325 | "T" => tagHandler, | |
326 | "A" => AddedFileHandler.new, | |
327 | "R" => RemovedFileHandler.new, | |
328 | + "C" => CopiedFileHandler.new, | |
329 | "M" => ModifiedFileHandler.new, | |
330 | + "P" => ModifiedPropsFileHandler.new, | |
331 | "V" => VersionHandler.new] | |
332 | ||
333 | $handlers["A"].setTagHandler(tagHandler) | |
334 | $handlers["R"].setTagHandler(tagHandler) | |
335 | +$handlers["C"].setTagHandler(tagHandler) | |
336 | $handlers["M"].setTagHandler(tagHandler) | |
337 | +$handlers["P"].setTagHandler(tagHandler) | |
338 | ||
339 | $fileEntries = Array.new | |
340 | $task_list = Array.new | |
341 | @@ -1374,7 +1481,8 @@ | |
342 | ||
343 | $diff_output_limiter = OutputSizeLimiter.new(mail, $mail_size_limit) | |
344 | ||
345 | - reader = LogReader.new($stdin) | |
346 | + File.open($logfile) do |log| | |
347 | + reader = LogReader.new(log) | |
348 | ||
349 | until reader.eof | |
350 | handler = $handlers[reader.currentLineCode] | |
351 | @@ -1383,11 +1491,16 @@ | |
352 | end | |
353 | handler.handleLines(reader.getLines, $diff_output_limiter) | |
354 | end | |
355 | + end | |
356 | ||
357 | end | |
358 | ||
359 | if $subjectPrefix == nil | |
360 | - $subjectPrefix = "[SVN #{Repository.array.join(',')}]" | |
361 | + if $svn | |
362 | + $subjectPrefix = "[SVN #{Repository.array.join(',')}]" | |
363 | + else | |
364 | + $subjectPrefix = "[CVS #{Repository.array.join(',')}]" | |
365 | + end | |
366 | end | |
367 | ||
368 | if $files_in_subject | |
369 | @@ -1434,13 +1547,15 @@ | |
370 | #removed {background-color:#ffdddd;} | |
371 | #removedchars {background-color:#ff9999;font-weight:bolder;} | |
372 | tr.alt #removed {background-color:#f7cccc;} | |
373 | + #copied {background-color:#ccccff;} | |
374 | + tr.alt #copied {background-color:#bbbbf7;} | |
375 | #info {color:#888888;} | |
376 | #context {background-color:#eeeeee;} | |
377 | td {padding-left:.3em;padding-right:.3em;} | |
378 | tr.head {border-bottom-width:1px;border-bottom-style:solid;} | |
379 | tr.head td {padding:0;padding-top:.2em;} | |
380 | .task {background-color:#ffff00;} | |
381 | - .comment {padding:4px;border:1px dashed #000000;background-color:#ffffdd} | |
382 | + .comment {white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;white-space:pre-wrap;word-wrap:break-word;padding:4px;border:1px dashed #000000;background-color:#ffffdd} | |
383 | .error {color:red;} | |
384 | hr {border-width:0px;height:2px;background:black;} | |
385 | --></style> | |
386 | @@ -1466,7 +1581,9 @@ | |
387 | ||
388 | filesAdded = 0 | |
389 | filesRemoved = 0 | |
390 | + filesCopied = 0 | |
391 | filesModified = 0 | |
392 | + filesModifiedProps = 0 | |
393 | totalLinesAdded = 0 | |
394 | totalLinesRemoved = 0 | |
395 | file_count = 0 | |
396 | @@ -1475,24 +1592,26 @@ | |
397 | $fileEntries.each do |file| | |
398 | unless file.repository == last_repository | |
399 | last_repository = file.repository | |
400 | - mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 5 : 4}\">") | |
401 | + mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 6 : 5}\">") | |
402 | if last_repository.has_multiple_tags | |
403 | mail.print("Mixed-tag commit") | |
404 | else | |
405 | mail.print("Commit") | |
406 | end | |
407 | mail.print(" in <b><tt>#{htmlEncode(last_repository.common_prefix)}</tt></b>") | |
408 | - if last_repository.trunk_only? | |
409 | - mail.print("<span id=\"info\"> on MAIN</span>") | |
410 | - else | |
411 | - mail.print(" on ") | |
412 | - tagCount = 0 | |
413 | - last_repository.each_tag do |tag| | |
414 | - tagCount += 1 | |
415 | - if tagCount > 1 | |
416 | - mail.print tagCount<last_repository.tag_count ? ", " : " & " | |
417 | + if !$svn | |
418 | + if last_repository.trunk_only? | |
419 | + mail.print("<span id=\"info\"> on MAIN</span>") | |
420 | + else | |
421 | + mail.print(" on ") | |
422 | + tagCount = 0 | |
423 | + last_repository.each_tag do |tag| | |
424 | + tagCount += 1 | |
425 | + if tagCount > 1 | |
426 | + mail.print tagCount<last_repository.tag_count ? ", " : " & " | |
427 | + end | |
428 | + mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>" | |
429 | end | |
430 | - mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>" | |
431 | end | |
432 | end | |
433 | mail.puts("</td></tr>") | |
434 | @@ -1507,8 +1626,12 @@ | |
435 | filesAdded += 1 | |
436 | elsif file.removal? | |
437 | filesRemoved += 1 | |
438 | + elsif file.copied? | |
439 | + filesCopied += 1 | |
440 | elsif file.modification? | |
441 | filesModified += 1 | |
442 | + elsif file.modifiedprops? | |
443 | + filesModifiedProps += 1 | |
444 | end | |
445 | name = htmlEncode(file.name_after_common_prefix) | |
446 | slashPos = name.rindex("/") | |
447 | @@ -1528,17 +1651,29 @@ | |
448 | name = "<span id=\"added\">#{name}</span>" | |
449 | elsif file.removal? | |
450 | name = "<span id=\"removed\">#{name}</span>" | |
451 | + elsif file.copied? | |
452 | + name = "<span id=\"copied\">#{name}</span>" | |
453 | end | |
454 | + mail.print("<td>") | |
455 | if file.has_diff? | |
456 | - mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt></td>") | |
457 | + mail.print("<tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt>") | |
458 | else | |
459 | - mail.print("<td><tt>#{prefix}#{name}</tt></td>") | |
460 | + mail.print("<tt>#{prefix}#{name}</tt>") | |
461 | end | |
462 | - if file.isEmpty | |
463 | - mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>") | |
464 | + mail.print(" #{$frontend.log(file)}") | |
465 | + mail.print("</td>") | |
466 | + if file.copied? | |
467 | + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[copied]</small></td>") | |
468 | + elsif file.isEmpty | |
469 | + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[empty]</small></td>") | |
470 | elsif file.isBinary | |
471 | - mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[binary]</small></td>") | |
472 | + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[binary]</small></td>") | |
473 | else | |
474 | + if file.modifiedprops? | |
475 | + mail.print("<td align=\"right\"><small id=\"info\">[props]</small></td>") | |
476 | + else | |
477 | + mail.print("<td></td>") | |
478 | + end | |
479 | if file.lineAdditions>0 | |
480 | totalLinesAdded += file.lineAdditions | |
481 | mail.print("<td align=\"right\" id=\"added\">+#{file.lineAdditions}</td>") | |
482 | @@ -1565,15 +1700,19 @@ | |
483 | mail.print("<td nowrap=\"nowrap\" align=\"right\">added #{$frontend.version(file.path,file.toVer)}</td>") | |
484 | elsif file.removal? | |
485 | mail.print("<td nowrap=\"nowrap\">#{$frontend.version(file.path,file.fromVer)} removed</td>") | |
486 | + elsif file.copied? | |
487 | + mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>") | |
488 | elsif file.modification? | |
489 | mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>") | |
490 | + elsif file.modifiedprops? | |
491 | + mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>") | |
492 | end | |
493 | ||
494 | mail.puts("</tr>") | |
495 | end | |
496 | if $fileEntries.size>1 && (totalLinesAdded+totalLinesRemoved)>0 | |
497 | # give total number of lines added/removed accross all files | |
498 | - mail.print("<tr><td></td>") | |
499 | + mail.print("<tr><td></td><td></td>") | |
500 | if totalLinesAdded>0 | |
501 | mail.print("<td align=\"right\" id=\"added\">+#{totalLinesAdded}</td>") | |
502 | else | |
503 | @@ -1590,7 +1729,7 @@ | |
504 | ||
505 | mail.puts("</table>") | |
506 | ||
507 | - totalFilesChanged = filesAdded+filesRemoved+filesModified | |
508 | + totalFilesChanged = filesAdded+filesRemoved+filesCopied+filesModified+filesModifiedProps | |
509 | if totalFilesChanged > 1 | |
510 | mail.print("<small id=\"info\">") | |
511 | changeKind = 0 | |
512 | @@ -1603,11 +1742,21 @@ | |
513 | mail.print("#{filesRemoved} removed") | |
514 | changeKind += 1 | |
515 | end | |
516 | + if filesCopied>0 | |
517 | + mail.print(" + ") if changeKind>0 | |
518 | + mail.print("#{filesCopied} copied") | |
519 | + changeKind += 1 | |
520 | + end | |
521 | if filesModified>0 | |
522 | mail.print(" + ") if changeKind>0 | |
523 | mail.print("#{filesModified} modified") | |
524 | changeKind += 1 | |
525 | end | |
526 | + if filesModifiedProps>0 | |
527 | + mail.print(" + ") if changeKind>0 | |
528 | + mail.print("#{filesModifiedProps} modified properties") | |
529 | + changeKind += 1 | |
530 | + end | |
531 | mail.print(", total #{totalFilesChanged}") if changeKind > 1 | |
532 | mail.puts(" files</small><br />") | |
533 | end | |
534 | @@ -1742,7 +1891,7 @@ | |
535 | from = EmailAddress.new(ENV['USER'] || ENV['USERNAME'] || 'cvsspam') | |
536 | end | |
537 | unless from.address =~ /@/ | |
538 | - from.address = "#{from.address}@#{ENV['HOSTNAME']||'localhost'}" | |
539 | + from.address = "#{from.address}@#{$hostname}" | |
540 | end | |
541 | smtp = Net::SMTP.new(@smtp_host) | |
542 | blah("connecting to '#{@smtp_host}'") | |
543 | @@ -1758,6 +1907,40 @@ | |
544 | end | |
545 | end | |
546 | ||
547 | + | |
548 | +def make_msg_id(localpart, hostpart) | |
549 | + "<cvsspam-#{localpart}@#{hostpart}>" | |
550 | +end | |
551 | + | |
552 | + | |
553 | +# replaces control characters, and a selection of other characters that | |
554 | +# may not appear unquoted in an RFC822 'word', with underscores. (It | |
555 | +# doesn't actually zap '.' though.) | |
556 | +def zap_header_special_chars(text) | |
557 | + text.gsub(/<>()\[\]@,;:\\[\000-\037\177]/, "_") | |
558 | +end | |
559 | + | |
560 | + | |
561 | +# Mail clients will try to 'thread' together a conversation over | |
562 | +# several email messages by inspecting the In-Reply-To and References headers, | |
563 | +# which should refer to previous emails in the conversation by mentioning | |
564 | +# the value of the previous message's Message-Id header. This function invents | |
565 | +# values for these headers so that, in the special case where a *single* file | |
566 | +# is committed to repeatedly, the emails giving notification of these commits | |
567 | +# can be threaded together automatically by the mail client. | |
568 | +def inject_threading_headers(mail) | |
569 | + return unless $fileEntries.length == 1 | |
570 | + file = $fileEntries[0] | |
571 | + name = zap_header_special_chars(file.path) | |
572 | + if file.fromVer | |
573 | + mail.header("References", make_msg_id("#{name}.#{file.fromVer}", $hostname)) | |
574 | + end | |
575 | + if file.toVer | |
576 | + mail.header("Message-ID", make_msg_id("#{name}.#{file.toVer}", $hostname)) | |
577 | + end | |
578 | +end | |
579 | + | |
580 | + | |
581 | if $smtp_host | |
582 | require 'net/smtp' | |
583 | mailer = SMTPMailer.new($smtp_host) | |
584 | @@ -1769,6 +1952,7 @@ | |
585 | ||
586 | mailer.send($from_address, $recipients) do |mail| | |
587 | mail.header("Subject", mailSubject) | |
588 | + inject_threading_headers(mail) | |
589 | mail.header("MIME-Version", "1.0") | |
590 | mail.header("Content-Type", "text/html" + ($charset.nil? ? "" : "; charset=\"#{$charset}\"")) | |
591 | if ENV['REMOTE_HOST'] |