]>
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'] | |
1f386b00 ER |
592 | --- cvsspam-0.2.12/svn_post_commit_hook.rb 2005-07-11 18:53:29.000000000 +0300 |
593 | +++ cvsspam/cvsspam-svn/svn_post_commit_hook.rb 2008-08-07 17:27:52.628725224 +0300 | |
594 | @@ -34,7 +34,7 @@ | |
595 | ||
596 | def send_email | |
597 | cmd = File.dirname($0) + "/cvsspam.rb" | |
598 | - unless system(cmd, "#{$datadir}/logfile", *$passthrough_args) | |
599 | + unless system(cmd,"--svn","#{$datadir}/logfile", *$passthrough_args) | |
600 | fail "problem running '#{cmd}'" | |
601 | end | |
602 | end | |
603 | @@ -86,6 +86,8 @@ | |
604 | unless FileTest.directory?($repository) | |
605 | usage("no such directory: #{$repository.inspect}") | |
606 | end | |
607 | + $repository =~ /([^\/]+$)/ | |
608 | + $shortrepo = $1 | |
609 | end | |
610 | ||
611 | # runs the given svnlook subcommand | |
612 | @@ -123,16 +125,6 @@ | |
613 | end | |
614 | ||
615 | ||
616 | -def each_changed | |
617 | - svnlook("changed", $revision) do |io| | |
618 | - io.each_line do |line| | |
619 | - line =~ /^(.)(.) (.*)$/ | |
620 | - yield Change.new($1, $2, $3) | |
621 | - end | |
622 | - end | |
623 | -end | |
624 | - | |
625 | - | |
626 | ||
627 | # Line-oriented access to an underlying IO object. Remembers 'current' line | |
628 | # for lookahead during parsing. | |
629 | @@ -149,10 +141,15 @@ | |
630 | (@line = @io.gets) != nil | |
631 | end | |
632 | ||
633 | + def assert_current(re) | |
634 | + raise "unexpected #{current.inspect}" unless @line =~ re | |
635 | + $~ | |
636 | + end | |
637 | + | |
638 | def assert_next(re=nil) | |
639 | raise "unexpected end of text" unless next_line | |
640 | unless re.nil? | |
641 | - raise "unexpected #{lines.current.inspect}" unless @line =~ re | |
642 | + raise "unexpected #{current.inspect}" unless @line =~ re | |
643 | end | |
644 | $~ | |
645 | end | |
646 | @@ -161,14 +158,34 @@ | |
647 | ||
648 | def read_modified_diff(out, lines, path) | |
649 | lines.assert_next(/^=+$/) | |
650 | - m = lines.assert_next(/^---.*\(rev (\d+)\)$/) | |
651 | + lines.assert_next | |
652 | + if lines.current =~ /\(Binary files differ\)/ | |
653 | + process_modified_binary_diff(out, lines, path) | |
654 | + else | |
655 | + process_modified_text_diff(out, lines, path) | |
656 | + end | |
657 | +end | |
658 | + | |
659 | + | |
660 | +def process_modified_binary_diff(out, lines, path) | |
661 | + prev_rev= $revision-1 | |
662 | + next_rev= $revision | |
663 | + out.puts "#V #{prev_rev},#{next_rev}" | |
664 | + out.puts "#M #{$shortrepo}/#{path}" | |
665 | + out.puts "#U diff x x" | |
666 | + out.puts "#U Binary files x and y differ" | |
667 | +end | |
668 | + | |
669 | + | |
670 | +def process_modified_text_diff(out, lines, path) | |
671 | + m = lines.assert_current(/^---.*\(rev (\d+)\)$/) | |
672 | prev_rev = m[1].to_i | |
673 | diff1 = lines.current | |
674 | m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) | |
675 | next_rev = m[1].to_i | |
676 | diff2 = lines.current | |
677 | out.puts "#V #{prev_rev},#{next_rev}" | |
678 | - out.puts "#M #{path}" | |
679 | + out.puts "#M #{$shortrepo}/#{path}" | |
680 | out.puts "#U #{diff1}" | |
681 | out.puts "#U #{diff2}" | |
682 | while lines.next_line && lines.current =~ /^[-\+ @\\]/ | |
683 | @@ -178,14 +195,31 @@ | |
684 | ||
685 | def read_added_diff(out, lines, path) | |
686 | lines.assert_next(/^=+$/) | |
687 | - m = lines.assert_next(/^---.*\(rev (\d+)\)$/) | |
688 | + lines.assert_next | |
689 | + if lines.current =~ /\(Binary files differ\)/ | |
690 | + process_added_binary_diff(out, lines, path) | |
691 | + else | |
692 | + process_added_text_diff(out, lines, path) | |
693 | + end | |
694 | +end | |
695 | + | |
696 | +def process_added_binary_diff(out, lines, path) | |
697 | + next_rev= $revision | |
698 | + out.puts "#V NONE,#{next_rev}" | |
699 | + out.puts "#A #{$shortrepo}/#{path}" | |
700 | + out.puts "#U diff x x" | |
701 | + out.puts "#U Binary file x added" | |
702 | +end | |
703 | + | |
704 | +def process_added_text_diff(out, lines, path) | |
705 | + m = lines.assert_current(/^---.*\(rev (\d+)\)$/) | |
706 | prev_rev = m[1].to_i | |
707 | diff1 = lines.current | |
708 | m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) | |
709 | next_rev = m[1].to_i | |
710 | diff2 = lines.current | |
711 | out.puts "#V NONE,#{next_rev}" | |
712 | - out.puts "#A #{path}" | |
713 | + out.puts "#A #{$shortrepo}/#{path}" | |
714 | out.puts "#U #{diff1}" | |
715 | out.puts "#U #{diff2}" | |
716 | while lines.next_line && lines.current =~ /^[-\+ @\\]/ | |
717 | @@ -202,7 +236,7 @@ | |
718 | next_rev = m[1].to_i | |
719 | diff2 = lines.current | |
720 | out.puts "#V #{prev_rev},NONE" | |
721 | - out.puts "#R #{path}" | |
722 | + out.puts "#R #{$shortrepo}/#{path}" | |
723 | out.puts "#U #{diff1}" | |
724 | out.puts "#U #{diff2}" | |
725 | while lines.next_line && lines.current =~ /^[-\+ @\\]/ | |
726 | @@ -221,13 +255,23 @@ | |
727 | end | |
728 | ||
729 | def assert_prop_match(a, b) | |
730 | - if a != b | |
731 | + if !b.nil? && a != b | |
732 | raise "property mismatch: #{a.inspect}!=#{b.inspect}" | |
733 | end | |
734 | end | |
735 | ||
736 | +# We need to read the property change from the output of svnlook, but have | |
737 | +# a difficulty in that there's no unambiguous delimiter marking the end of | |
738 | +# a potentially multi-line property value. Therefore, we do a seperate | |
739 | +# svn propget on the given file to get the value of the property on its own, | |
740 | +# and then use that value as a guide as to how much data to read from the | |
741 | +# svnlook output. | |
742 | def munch_prop_text(path, prop_name, revision, lines, line0) | |
743 | prop = read_property_lines(path, prop_name, revision) | |
744 | + if prop.empty? | |
745 | + assert_prop_match(line0, "") | |
746 | + return | |
747 | + end | |
748 | assert_prop_match(line0, prop.shift) | |
749 | prop.each do |prop_line| | |
750 | lines.assert_next | |
751 | @@ -236,8 +280,16 @@ | |
752 | end | |
753 | ||
754 | def read_properties_changed(out, lines, path) | |
755 | + prev_rev= $revision-1 | |
756 | + next_rev= $revision | |
757 | lines.assert_next(/^_+$/) | |
758 | return unless lines.next_line | |
759 | + out.puts "#V #{prev_rev},#{next_rev}" | |
760 | + out.puts "#P #{$shortrepo}/#{path}" | |
761 | +# The first three get consumed and not highlighted | |
762 | + out.puts "#U " | |
763 | + out.puts "#U Property changes:" | |
764 | + out.puts "#U " | |
765 | while true | |
766 | break unless lines.current =~ /^Name: (.+)$/ | |
767 | prop_name = $1 | |
768 | @@ -254,13 +306,49 @@ | |
769 | munch_prop_text(path, prop_name, $revision, lines, line0) | |
770 | lines.next_line | |
771 | end | |
772 | + out.puts "#U #{m[1]} #{prop_name}:#{m[2]}" | |
773 | end | |
774 | + out.puts "#U " | |
775 | end | |
776 | ||
777 | def handle_copy(out, lines, path, from_ref, from_file) | |
778 | - # TODO: handle file copies in email | |
779 | + prev_rev= $revision-1 | |
780 | + next_rev= $revision | |
781 | + out.puts "#V #{$shortrepo}/#{from_file}:#{prev_rev},#{next_rev}" | |
782 | + out.puts "#C #{$shortrepo}/#{path}" | |
783 | + if lines.next_line && lines.current =~ /^=+$/ | |
784 | + m = lines.assert_next(/^---.*\(rev (\d+)\)$/) | |
785 | + prev_rev = m[1].to_i | |
786 | + diff1 = lines.current | |
787 | + m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) | |
788 | + next_rev = m[1].to_i | |
789 | + diff2 = lines.current | |
790 | + out.puts "#U #{diff1}" | |
791 | + out.puts "#U #{diff2}" | |
792 | + while lines.next_line && lines.current =~ /^[-\+ @\\]/ | |
793 | + out.puts "#U #{lines.current}" | |
794 | + end | |
795 | + else | |
796 | + out.puts "#U " | |
797 | + out.puts "#U Copied from #{$shortrepo}/#{from_file}:#{from_ref}" | |
798 | + out.puts "#U " | |
799 | + end | |
800 | end | |
801 | ||
802 | +def svnlook_author | |
803 | + svnlook("author", $revision) do |io| | |
804 | + return io.readline | |
805 | + end | |
806 | + nil | |
807 | +end | |
808 | + | |
809 | +def find_author | |
810 | + return if $passthrough_args.include?("--from") | |
811 | + author = svnlook_author | |
812 | + if author | |
813 | + $passthrough_args << "--from" << author | |
814 | + end | |
815 | +end | |
816 | ||
817 | def process_svnlook_log(file) | |
818 | svnlook("log", $revision) do |io| | |
819 | @@ -294,16 +382,17 @@ | |
820 | end | |
821 | ||
822 | def process_commit() | |
823 | - File.open("#{$datadir}/logfile", File::WRONLY|File::CREAT) do |file| | |
824 | - process_svnlook_log(file) | |
825 | - process_svnlook_diff(file) | |
826 | - end | |
827 | + File.open("#{$datadir}/logfile", File::WRONLY|File::CREAT) do |file| | |
828 | + process_svnlook_log(file) | |
829 | + process_svnlook_diff(file) | |
830 | + end | |
831 | end | |
832 | ||
833 | ||
834 | def main | |
835 | init() | |
836 | process_args() | |
837 | + find_author() | |
838 | process_commit() | |
839 | send_email() | |
840 | cleanup() |