source: scripts/svn-scripts/mailer.py@ 2769

Last change on this file since 2769 was 2769, checked in by Александър Шопов, 13 years ago

svn-scripts: Скриптове за публикуване на информация

  • Property svn:executable set to *
File size: 25.5 KB
Line 
1#!/usr/bin/env python
2#
3# mailer.py: send email describing a commit
4#
5# $HeadURL: http://svn.collab.net/repos/svn/branches/1.1.x/tools/hook-scripts/mailer/mailer.py $
6# $LastChangedDate: 2005-03-26 12:14:01 -0800 (Sat, 26 Mar 2005) $
7# $LastChangedBy: maxb $
8# $LastChangedRevision: 13698 $
9#
10# USAGE: mailer.py commit REPOS-DIR REVISION [CONFIG-FILE]
11# mailer.py propchange REPOS-DIR REVISION AUTHOR PROPNAME [CONFIG-FILE]
12#
13# Using CONFIG-FILE, deliver an email describing the changes between
14# REV and REV-1 for the repository REPOS.
15#
16
17import os
18import sys
19import string
20import ConfigParser
21import time
22import popen2
23import cStringIO
24import smtplib
25import re
26import types
27
28import svn.fs
29import svn.delta
30import svn.repos
31import svn.core
32
33SEPARATOR = '=' * 78
34
35
36def main(pool, cmd, config_fname, repos_dir, rev, author, propname):
37 repos = Repository(repos_dir, rev, pool)
38
39 if cmd == 'commit':
40 cfg = Config(config_fname, repos, { 'author' : author or repos.author })
41 messenger = Commit(pool, cfg, repos)
42 elif cmd == 'propchange':
43 # Override the repos revision author with the author of the propchange
44 repos.author = author
45 cfg = Config(config_fname, repos, { 'author' : author })
46 messenger = PropChange(pool, cfg, repos, author, propname)
47 else:
48 raise UnknownSubcommand(cmd)
49
50 messenger.generate()
51
52
53# ============================================================================
54if sys.platform == "win32":
55 _escape_shell_arg_re = re.compile(r'(\\+)(\"|$)')
56
57 def escape_shell_arg(arg):
58 # The (very strange) parsing rules used by the C runtime library are
59 # described at:
60 # http://msdn.microsoft.com/library/en-us/vclang/html/_pluslang_Parsing_C.2b2b_.Command.2d.Line_Arguments.asp
61
62 # double up slashes, but only if they are followed by a quote character
63 arg = re.sub(_escape_shell_arg_re, r'\1\1\2', arg)
64
65 # surround by quotes and escape quotes inside
66 arg = '"' + string.replace(arg, '"', '"^""') + '"'
67 return arg
68
69
70 def argv_to_command_string(argv):
71 """Flatten a list of command line arguments into a command string.
72
73 The resulting command string is expected to be passed to the system
74 shell which os functions like popen() and system() invoke internally.
75 """
76
77 # According cmd's usage notes (cmd /?), it parses the command line by
78 # "seeing if the first character is a quote character and if so, stripping
79 # the leading character and removing the last quote character."
80 # So to prevent the argument string from being changed we add an extra set
81 # of quotes around it here.
82 return '"' + string.join(map(escape_shell_arg, argv), " ") + '"'
83
84else:
85 def escape_shell_arg(str):
86 return "'" + string.replace(str, "'", "'\\''") + "'"
87
88 def argv_to_command_string(argv):
89 """Flatten a list of command line arguments into a command string.
90
91 The resulting command string is expected to be passed to the system
92 shell which os functions like popen() and system() invoke internally.
93 """
94
95 return string.join(map(escape_shell_arg, argv), " ")
96# ============================================================================
97
98# Minimal, incomplete, versions of popen2.Popen[34] for those platforms
99# for which popen2 does not provide them.
100try:
101 Popen3 = popen2.Popen3
102 Popen4 = popen2.Popen4
103except AttributeError:
104 class Popen3:
105 def __init__(self, cmd, capturestderr = False):
106 if type(cmd) != types.StringType:
107 cmd = argv_to_command_string(cmd)
108 if capturestderr:
109 self.fromchild, self.tochild, self.childerr \
110 = popen2.popen3(cmd, mode='b')
111 else:
112 self.fromchild, self.tochild = popen2.popen2(cmd, mode='b')
113 self.childerr = None
114
115 def wait(self):
116 rv = self.fromchild.close()
117 rv = self.tochild.close() or rv
118 if self.childerr is not None:
119 rv = self.childerr.close() or rv
120 return rv
121
122 class Popen4:
123 def __init__(self, cmd):
124 if type(cmd) != types.StringType:
125 cmd = argv_to_command_string(cmd)
126 self.fromchild, self.tochild = popen2.popen4(cmd, mode='b')
127
128 def wait(self):
129 rv = self.fromchild.close()
130 rv = self.tochild.close() or rv
131 return rv
132
133class MailedOutput:
134 def __init__(self, cfg, repos, prefix_param):
135 self.cfg = cfg
136 self.repos = repos
137 self.prefix_param = prefix_param
138 self._CHUNKSIZE = 128 * 1024
139
140 def start(self, group, params):
141 # whitespace-separated list of addresses; split into a clean list:
142 self.to_addrs = \
143 filter(None, string.split(self.cfg.get('to_addr', group, params)))
144 self.from_addr = self.cfg.get('from_addr', group, params) \
145 or self.repos.author or 'no_author'
146 self.reply_to = self.cfg.get('reply_to', group, params)
147
148 def mail_headers(self, group, params):
149 prefix = self.cfg.get(self.prefix_param, group, params)
150 if prefix:
151 subject = prefix + ' ' + self.subject
152 else:
153 subject = self.subject
154 hdrs = 'From: %s\n' \
155 'To: %s\n' \
156 'Subject: %s\n' \
157 'MIME-Version: 1.0\n' \
158 'Content-Type: text/plain; charset=UTF-8\n' \
159 % (self.from_addr, string.join(self.to_addrs, ', '), subject)
160 if self.reply_to:
161 hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to)
162 return hdrs + '\n'
163
164 def run(self, cmd):
165 # By default we choose to incorporate child stderr into the output
166 pipe_ob = Popen4(cmd)
167
168 buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
169 while buf:
170 self.write(buf)
171 buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
172
173 # wait on the child so we don't end up with a billion zombies
174 pipe_ob.wait()
175
176
177class SMTPOutput(MailedOutput):
178 "Deliver a mail message to an MTA using SMTP."
179
180 def start(self, group, params, **args):
181 MailedOutput.start(self, group, params, **args)
182
183 self.buffer = cStringIO.StringIO()
184 self.write = self.buffer.write
185
186 self.write(self.mail_headers(group, params))
187
188 def finish(self):
189 server = smtplib.SMTP(self.cfg.general.smtp_hostname)
190 if self.cfg.is_set('general.smtp_username'):
191 server.login(self.cfg.general.smtp_username,
192 self.cfg.general.smtp_password)
193 server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue())
194 server.quit()
195
196
197class StandardOutput:
198 "Print the commit message to stdout."
199
200 def __init__(self, cfg, repos, prefix_param):
201 self.cfg = cfg
202 self.repos = repos
203 self._CHUNKSIZE = 128 * 1024
204
205 self.write = sys.stdout.write
206
207 def start(self, group, params, **args):
208 pass
209
210 def finish(self):
211 pass
212
213 def run(self, cmd):
214 # By default we choose to incorporate child stderr into the output
215 pipe_ob = Popen4(cmd)
216
217 buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
218 while buf:
219 self.write(buf)
220 buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
221
222 # wait on the child so we don't end up with a billion zombies
223 pipe_ob.wait()
224
225
226class PipeOutput(MailedOutput):
227 "Deliver a mail message to an MDA via a pipe."
228
229 def __init__(self, cfg, repos, prefix_param):
230 MailedOutput.__init__(self, cfg, repos, prefix_param)
231
232 # figure out the command for delivery
233 self.cmd = string.split(cfg.general.mail_command)
234
235 def start(self, group, params, **args):
236 MailedOutput.start(self, group, params, **args)
237
238 ### gotta fix this. this is pretty specific to sendmail and qmail's
239 ### mailwrapper program. should be able to use option param substitution
240 cmd = self.cmd + [ '-f', self.from_addr ] + self.to_addrs
241
242 # construct the pipe for talking to the mailer
243 self.pipe = Popen3(cmd)
244 self.write = self.pipe.tochild.write
245
246 # we don't need the read-from-mailer descriptor, so close it
247 self.pipe.fromchild.close()
248
249 # start writing out the mail message
250 self.write(self.mail_headers(group, params))
251
252 def finish(self):
253 # signal that we're done sending content
254 self.pipe.tochild.close()
255
256 # wait to avoid zombies
257 self.pipe.wait()
258
259
260class Messenger:
261 def __init__(self, pool, cfg, repos, prefix_param):
262 self.pool = pool
263 self.cfg = cfg
264 self.repos = repos
265 self.determine_output(cfg, repos, prefix_param)
266
267 def determine_output(self, cfg, repos, prefix_param):
268 if cfg.is_set('general.mail_command'):
269 cls = PipeOutput
270 elif cfg.is_set('general.smtp_hostname'):
271 cls = SMTPOutput
272 else:
273 cls = StandardOutput
274
275 self.output = cls(cfg, repos, prefix_param)
276
277
278class Commit(Messenger):
279 def __init__(self, pool, cfg, repos):
280 Messenger.__init__(self, pool, cfg, repos, 'commit_subject_prefix')
281
282 # get all the changes and sort by path
283 editor = svn.repos.RevisionChangeCollector(repos.fs_ptr, repos.rev,
284 self.pool)
285 e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
286 svn.repos.svn_repos_replay(repos.root_this, e_ptr, e_baton, self.pool)
287
288 self.changelist = editor.changes.items()
289 self.changelist.sort()
290
291 ### hunh. this code isn't actually needed for StandardOutput. refactor?
292 # collect the set of groups and the unique sets of params for the options
293 self.groups = { }
294 for path, change in self.changelist:
295 for (group, params) in self.cfg.which_groups(path):
296 # turn the params into a hashable object and stash it away
297 param_list = params.items()
298 param_list.sort()
299 self.groups[group, tuple(param_list)] = params
300
301 # figure out the changed directories
302 dirs = { }
303 for path, change in self.changelist:
304 if change.item_kind == svn.core.svn_node_dir:
305 dirs[path] = None
306 else:
307 idx = string.rfind(path, '/')
308 if idx == -1:
309 dirs[''] = None
310 else:
311 dirs[path[:idx]] = None
312
313 dirlist = dirs.keys()
314
315 # figure out the common portion of all the dirs. note that there is
316 # no "common" if only a single dir was changed, or the root was changed.
317 if len(dirs) == 1 or dirs.has_key(''):
318 commondir = ''
319 else:
320 common = string.split(dirlist.pop(), '/')
321 for d in dirlist:
322 parts = string.split(d, '/')
323 for i in range(len(common)):
324 if i == len(parts) or common[i] != parts[i]:
325 del common[i:]
326 break
327 commondir = string.join(common, '/')
328 if commondir:
329 # strip the common portion from each directory
330 l = len(commondir) + 1
331 dirlist = [ ]
332 for d in dirs.keys():
333 if d == commondir:
334 dirlist.append('.')
335 else:
336 dirlist.append(d[l:])
337 else:
338 # nothing in common, so reset the list of directories
339 dirlist = dirs.keys()
340
341 # compose the basic subject line. later, we can prefix it.
342 dirlist.sort()
343 dirlist = string.join(dirlist)
344 if commondir:
345 self.output.subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist)
346 else:
347 self.output.subject = 'r%d - %s' % (repos.rev, dirlist)
348
349 def generate(self):
350 "Generate email for the various groups and option-params."
351
352 ### the groups need to be further compressed. if the headers and
353 ### body are the same across groups, then we can have multiple To:
354 ### addresses. SMTPOutput holds the entire message body in memory,
355 ### so if the body doesn't change, then it can be sent N times
356 ### rather than rebuilding it each time.
357
358 subpool = svn.core.svn_pool_create(self.pool)
359
360 for (group, param_tuple), params in self.groups.items():
361 self.output.start(group, params)
362
363 # generate the content for this group and set of params
364 generate_content(self.output, self.cfg, self.repos, self.changelist,
365 group, params, subpool)
366
367 self.output.finish()
368 svn.core.svn_pool_clear(subpool)
369
370 svn.core.svn_pool_destroy(subpool)
371
372
373class PropChange(Messenger):
374 def __init__(self, pool, cfg, repos, author, propname):
375 Messenger.__init__(self, pool, cfg, repos, 'propchange_subject_prefix')
376 self.author = author
377 self.propname = propname
378
379 ### hunh. this code isn't actually needed for StandardOutput. refactor?
380 # collect the set of groups and the unique sets of params for the options
381 self.groups = { }
382 for (group, params) in self.cfg.which_groups(''):
383 # turn the params into a hashable object and stash it away
384 param_list = params.items()
385 param_list.sort()
386 self.groups[group, tuple(param_list)] = params
387
388 self.output.subject = 'r%d - %s' % (repos.rev, propname)
389
390 def generate(self):
391 for (group, param_tuple), params in self.groups.items():
392 self.output.start(group, params)
393 self.output.write('Author: %s\nRevision: %s\nProperty Name: %s\n\n'
394 % (self.author, self.repos.rev, self.propname))
395 propvalue = self.repos.get_rev_prop(self.propname)
396 self.output.write('New Property Value:\n')
397 self.output.write(propvalue)
398 self.output.finish()
399
400
401def generate_content(output, cfg, repos, changelist, group, params, pool):
402
403 svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE)
404 ### pick a different date format?
405 date = time.ctime(svn.core.secs_from_timestr(svndate, pool))
406
407 output.write('Author: %s\nDate: %s\nNew Revision: %s\n\n'
408 % (repos.author, date, repos.rev))
409
410 # print summary sections
411 generate_list(output, 'Added', changelist, _select_adds)
412 generate_list(output, 'Removed', changelist, _select_deletes)
413 generate_list(output, 'Modified', changelist, _select_modifies)
414
415 output.write('Log:\n%s\n'
416 % (repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or ''))
417
418 # these are sorted by path already
419 for path, change in changelist:
420 generate_diff(output, cfg, repos, date, change, group, params, pool)
421
422
423def _select_adds(change):
424 return change.added
425def _select_deletes(change):
426 return change.path is None
427def _select_modifies(change):
428 return not change.added and change.path is not None
429
430
431def generate_list(output, header, changelist, selection):
432 items = [ ]
433 for path, change in changelist:
434 if selection(change):
435 items.append((path, change))
436 if items:
437 output.write('%s:\n' % header)
438 for fname, change in items:
439 if change.item_kind == svn.core.svn_node_dir:
440 is_dir = '/'
441 else:
442 is_dir = ''
443 if change.prop_changes:
444 if change.text_changed:
445 props = ' (contents, props changed)'
446 else:
447 props = ' (props changed)'
448 else:
449 props = ''
450 output.write(' %s%s%s\n' % (fname, is_dir, props))
451 if change.added and change.base_path:
452 if is_dir:
453 text = ''
454 elif change.text_changed:
455 text = ', changed'
456 else:
457 text = ' unchanged'
458 output.write(' - copied%s from r%d, %s%s\n'
459 % (text, change.base_rev, change.base_path[1:], is_dir))
460
461
462def generate_diff(output, cfg, repos, date, change, group, params, pool):
463 if change.item_kind == svn.core.svn_node_dir:
464 # all changes were printed in the summary. nothing to do.
465 return
466
467 gen_diffs = cfg.get('generate_diffs', group, params)
468
469 ### Do a little dance for deprecated options. Note that even if you
470 ### don't have an option anywhere in your configuration file, it
471 ### still gets returned as non-None.
472 if len(gen_diffs):
473 diff_add = False
474 diff_copy = False
475 diff_delete = False
476 diff_modify = False
477 list = string.split(gen_diffs, " ")
478 for item in list:
479 if item == 'add':
480 diff_add = True
481 if item == 'copy':
482 diff_copy = True
483 if item == 'delete':
484 diff_delete = True
485 if item == 'modify':
486 diff_modify = True
487 else:
488 diff_add = True
489 diff_copy = True
490 diff_delete = True
491 diff_modify = True
492 ### These options are deprecated
493 suppress = cfg.get('suppress_deletes', group, params)
494 if suppress == 'yes':
495 diff_delete = False
496 suppress = cfg.get('suppress_adds', group, params)
497 if suppress == 'yes':
498 diff_add = False
499
500 if not change.path:
501 ### params is a bit silly here
502 if diff_delete == False:
503 # a record of the deletion is in the summary. no need to write
504 # anything further here.
505 return
506
507 output.write('\nDeleted: %s\n' % change.base_path)
508 diff = svn.fs.FileDiff(repos.get_root(change.base_rev),
509 change.base_path, None, None, pool)
510
511 label1 = '%s\t%s' % (change.base_path, date)
512 label2 = '(empty file)'
513 singular = True
514 elif change.added:
515 if change.base_path and (change.base_rev != -1):
516 # this file was copied.
517
518 if not change.text_changed:
519 # copies with no changes are reported in the header, so we can just
520 # skip them here.
521 return
522
523 if diff_copy == False:
524 # a record of the copy is in the summary, no need to write
525 # anything further here.
526 return
527
528 # note that we strip the leading slash from the base (copyfrom) path
529 output.write('\nCopied: %s (from r%d, %s)\n'
530 % (change.path, change.base_rev, change.base_path[1:]))
531 diff = svn.fs.FileDiff(repos.get_root(change.base_rev),
532 change.base_path[1:],
533 repos.root_this, change.path,
534 pool)
535 label1 = change.base_path[1:] + '\t(original)'
536 label2 = '%s\t%s' % (change.path, date)
537 singular = False
538 else:
539 if diff_add == False:
540 # a record of the addition is in the summary. no need to write
541 # anything further here.
542 return
543
544 output.write('\nAdded: %s\n' % change.path)
545 diff = svn.fs.FileDiff(None, None, repos.root_this, change.path, pool)
546 label1 = '(empty file)'
547 label2 = '%s\t%s' % (change.path, date)
548 singular = True
549 elif not change.text_changed:
550 # don't bother to show an empty diff. prolly just a prop change.
551 return
552 else:
553 if diff_modify == False:
554 # a record of the modification is in the summary, no need to write
555 # anything further here.
556 return
557
558 output.write('\nModified: %s\n' % change.path)
559 diff = svn.fs.FileDiff(repos.get_root(change.base_rev),
560 change.base_path[1:],
561 repos.root_this, change.path,
562 pool)
563 label1 = change.base_path[1:] + '\t(original)'
564 label2 = '%s\t%s' % (change.path, date)
565 singular = False
566
567 output.write(SEPARATOR + '\n')
568
569 if diff.either_binary():
570 if singular:
571 output.write('Binary file. No diff available.\n')
572 else:
573 output.write('Binary files. No diff available.\n')
574 return
575
576 ### do something with change.prop_changes
577
578 src_fname, dst_fname = diff.get_files()
579
580 output.run(cfg.get_diff_cmd({
581 'label_from' : label1,
582 'label_to' : label2,
583 'from' : src_fname,
584 'to' : dst_fname,
585 }))
586
587
588class Repository:
589 "Hold roots and other information about the repository."
590
591 def __init__(self, repos_dir, rev, pool):
592 self.repos_dir = repos_dir
593 self.rev = rev
594 self.pool = pool
595
596 self.repos_ptr = svn.repos.svn_repos_open(repos_dir, pool)
597 self.fs_ptr = svn.repos.svn_repos_fs(self.repos_ptr)
598
599 self.roots = { }
600
601 self.root_this = self.get_root(rev)
602
603 self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR)
604
605 def get_rev_prop(self, propname):
606 return svn.fs.revision_prop(self.fs_ptr, self.rev, propname, self.pool)
607
608 def get_root(self, rev):
609 try:
610 return self.roots[rev]
611 except KeyError:
612 pass
613 root = self.roots[rev] = svn.fs.revision_root(self.fs_ptr, rev, self.pool)
614 return root
615
616
617class Config:
618
619 # The predefined configuration sections. These are omitted from the
620 # set of groups.
621 _predefined = ('general', 'defaults')
622
623 def __init__(self, fname, repos, global_params):
624 cp = ConfigParser.ConfigParser()
625 cp.read(fname)
626
627 # record the (non-default) groups that we find
628 self._groups = [ ]
629
630 for section in cp.sections():
631 if not hasattr(self, section):
632 section_ob = _sub_section()
633 setattr(self, section, section_ob)
634 if section not in self._predefined:
635 self._groups.append((section, section_ob))
636 else:
637 section_ob = getattr(self, section)
638 for option in cp.options(section):
639 # get the raw value -- we use the same format for *our* interpolation
640 value = cp.get(section, option, raw=1)
641 setattr(section_ob, option, value)
642
643 ### do some better splitting to enable quoting of spaces
644 self._diff_cmd = string.split(self.general.diff)
645
646 # these params are always available, although they may be overridden
647 self._global_params = global_params.copy()
648
649 self._prep_groups(repos)
650
651 def get_diff_cmd(self, args):
652 cmd = [ ]
653 for part in self._diff_cmd:
654 cmd.append(part % args)
655 return cmd
656
657 def is_set(self, option):
658 """Return None if the option is not set; otherwise, its value is returned.
659
660 The option is specified as a dotted symbol, such as 'general.mail_command'
661 """
662 parts = string.split(option, '.')
663 ob = self
664 for part in string.split(option, '.'):
665 if not hasattr(ob, part):
666 return None
667 ob = getattr(ob, part)
668 return ob
669
670 def get(self, option, group, params):
671 if group:
672 sub = getattr(self, group)
673 if hasattr(sub, option):
674 return getattr(sub, option) % params
675 return getattr(self.defaults, option, '') % params
676
677 def _prep_groups(self, repos):
678 self._group_re = [ ]
679
680 repos_dir = os.path.abspath(repos.repos_dir)
681
682 # compute the default repository-based parameters. start with some
683 # basic parameters, then bring in the regex-based params.
684 default_params = self._global_params.copy()
685
686 try:
687 match = re.match(self.defaults.for_repos, repos_dir)
688 if match:
689 default_params.update(match.groupdict())
690 except AttributeError:
691 # there is no self.defaults.for_repos
692 pass
693
694 # select the groups that apply to this repository
695 for group, sub in self._groups:
696 params = default_params
697 if hasattr(sub, 'for_repos'):
698 match = re.match(sub.for_repos, repos_dir)
699 if not match:
700 continue
701 params = self._global_params.copy()
702 params.update(match.groupdict())
703
704 # if a matching rule hasn't been given, then use the empty string
705 # as it will match all paths
706 for_paths = getattr(sub, 'for_paths', '')
707 self._group_re.append((group, re.compile(for_paths), params))
708
709 # after all the groups are done, add in the default group
710 try:
711 self._group_re.append((None,
712 re.compile(self.defaults.for_paths),
713 default_params))
714 except AttributeError:
715 # there is no self.defaults.for_paths
716 pass
717
718 def which_groups(self, path):
719 "Return the path's associated groups."
720 groups = []
721 for group, pattern, repos_params in self._group_re:
722 match = pattern.match(path)
723 if match:
724 params = repos_params.copy()
725 params.update(match.groupdict())
726 groups.append((group, params))
727 if not groups:
728 groups.append((None, self._global_params))
729 return groups
730
731
732class _sub_section:
733 pass
734
735
736class MissingConfig(Exception):
737 pass
738
739class UnknownSubcommand(Exception):
740 pass
741
742
743# enable True/False in older vsns of Python
744try:
745 _unused = True
746except NameError:
747 True = 1
748 False = 0
749
750
751if __name__ == '__main__':
752 def usage():
753 sys.stderr.write(
754'''USAGE: %s commit REPOS-DIR REVISION [CONFIG-FILE]
755 %s propchange REPOS-DIR REVISION AUTHOR PROPNAME [CONFIG-FILE]
756'''
757 % (sys.argv[0], sys.argv[0]))
758 sys.exit(1)
759
760 if len(sys.argv) < 4:
761 usage()
762
763 cmd = sys.argv[1]
764 repos_dir = sys.argv[2]
765 revision = int(sys.argv[3])
766 config_fname = None
767 author = None
768 propname = None
769
770 if cmd == 'commit':
771 if len(sys.argv) > 5:
772 usage()
773 if len(sys.argv) > 4:
774 config_fname = sys.argv[4]
775 elif cmd == 'propchange':
776 if len(sys.argv) < 6 or len(sys.argv) > 7:
777 usage()
778 author = sys.argv[4]
779 propname = sys.argv[5]
780 if len(sys.argv) > 6:
781 config_fname = sys.argv[6]
782 else:
783 usage()
784
785 if config_fname is None:
786 # default to REPOS-DIR/conf/mailer.conf
787 config_fname = os.path.join(repos_dir, 'conf', 'mailer.conf')
788 if not os.path.exists(config_fname):
789 # okay. look for 'mailer.conf' as a sibling of this script
790 config_fname = os.path.join(os.path.dirname(sys.argv[0]), 'mailer.conf')
791
792 if not os.path.exists(config_fname):
793 raise MissingConfig(config_fname)
794
795 ### run some validation on these params
796 svn.core.run_app(main, cmd, config_fname, repos_dir, revision,
797 author, propname)
798
799# ------------------------------------------------------------------------
800# TODO
801#
802# * add configuration options
803# - default options [DONE]
804# - per-group overrides [DONE]
805# - group selection based on repos and on path [DONE]
806# - each group defines delivery info:
807# o how to construct From: [DONE]
808# o how to construct To: [DONE]
809# o subject line prefixes [DONE]
810# o whether to set Reply-To and/or Mail-Followup-To
811# (btw: it is legal do set Reply-To since this is the originator of the
812# mail; i.e. different from MLMs that munge it)
813# - each group defines content construction:
814# o max size of diff before trimming
815# o max size of entire commit message before truncation
816# o flag to disable generation of add/delete diffs
817# - per-repository configuration
818# o extra config living in repos
819# o how to construct a ViewCVS URL for the diff
820# o optional, non-mail log file
821# o look up authors (username -> email; for the From: header) in a
822# file(s) or DBM
823# - put the commit author into the params dict [DONE]
824# - if the subject line gets too long, then trim it. configurable?
825# * get rid of global functions that should properly be class methods
Note: See TracBrowser for help on using the repository browser.