1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Main Logic of the svnmailer
19 ===========================
20
21 This module is the central core of the svnmailer. It dispatches all work to be
22 done. It contains just one class (L{Main}), which reads the config file while
23 it is initialized. When the C{Main.run()} method is called, it selects the
24 groups to be notified, the notifiers to be run and runs all notifiers for
25 each group.
26
27 The Main class may raise several exceptions (which all inherit from L{Error}):
28 - L{ConfigError} occurs, if the configuration contains errors (like type
29 or value errors, unicode errors etc). The L{ConfigError} exception is
30 initialized with a string describing what kind of error occured.
31
32 - L{NotifierError} occurs, if one or more of the notifiers throw an
33 exception. The L{Main} class catches these exceptions (except
34 C{KeyboardInterrupt} and C{SystemExit}) and will initialize the
35 L{NotifierError} with the list of traceback strings, one for each
36 exception occured. (See the format_exception docs at
37 U{http://docs.python.org/lib/module-traceback.html}).
38
39 - L{svnmailer.subversion.RepositoryError} occurs, if something failed
40 while accessing the subversion repository. It contains some attributes
41 for identifying the error: C{svn_err_code}, C{svn_err_name} and
42 C{svn_err_str}
43 """
44 __author__ = "André Malo"
45 __docformat__ = "epytext en"
46 __all__ = ['Main', 'Error', 'ConfigError', 'NotifierError']
47
48
50 """ Base exception for this module """
51 pass
52
54 """ Configuration error occurred """
55 pass
56
58 """ An Notifier error occured """
59 pass
60
61
63 """ main svnmailer logic
64
65 @ivar _settings: The settings to use
66 @type _settings: C{svnmailer.settings.Settings}
67 """
68
69 - def __init__(self, options):
70 """ Initialization
71
72 @param options: Command line options
73 @type options: C{optparse.OptionParser}
74
75 @exception ConfigError: Configuration error
76 """
77 self._settings = self._getSettings(options)
78
79
81 """ Dispatches the work to be done
82
83 @exception svnmailer.subversion.RepositoryError: Error while
84 accessing the subversion repository
85 @exception NotifierError: One or more notifiers went crazy
86 """
87 from svnmailer import subversion
88
89 try:
90 try:
91 self._openRepository()
92
93 notifier_errors = []
94 throwables = (KeyboardInterrupt, SystemExit, subversion.Error)
95 selector = self._getNotifierSelector()
96
97 for groupset in self._getGroupSets():
98 notifiers = selector.selectNotifiers(groupset)
99 for notifier in notifiers:
100 try:
101 notifier.run()
102 except throwables:
103 raise
104 except:
105 import sys, traceback
106 info = sys.exc_info()
107 backtrace = traceback.format_exception(
108 info[0], info[1], info[2]
109 )
110 del info
111 backtrace[0] = "Notifier: %s.%s\nRevision: %s\n" \
112 "Groups: %r\n%s" % (
113 notifier.__module__,
114 notifier.__class__.__name__,
115 self._settings.runtime.revision,
116 [group._name for group in groupset.groups],
117 backtrace[0],
118 )
119 notifier_errors.append(''.join(backtrace))
120 if notifier_errors:
121 raise NotifierError(*notifier_errors)
122
123 except subversion.Error, exc:
124 import sys
125 raise subversion.RepositoryError, exc, sys.exc_info()[2]
126
127 finally:
128
129
130 self._closeRepository()
131
132
134 """ Returns the notifier selector
135
136 @return: The selector
137 @rtype: C{svnmailer.notifier.selector.Selector}
138 """
139 from svnmailer.notifier import selector
140 return selector.Selector(self._settings)
141
142
143 - def _getChanges(self):
144 """ Returns the list of changes for the requested revision
145
146 @return: The list of changes (C{[Descriptor, ...]})
147 @rtype: C{list}
148
149 @exception svnmailer.subversion.Error: Error while accessing the
150 subversion repository
151 """
152 from svnmailer import settings, subversion
153
154 modes = settings.modes
155 runtime = self._settings.runtime
156
157 if runtime.mode in (modes.commit, modes.propchange):
158 changes = runtime._repos.getChangesList(runtime.revision)
159 elif runtime.mode in (modes.lock, modes.unlock):
160 is_locked = bool(runtime.mode == modes.lock)
161 changes = [
162 subversion.LockedPathDescriptor(runtime._repos, path, is_locked)
163 for path in runtime.stdin.splitlines() if path
164 ]
165 changes.sort()
166 else:
167 raise AssertionError("Unknown runtime.mode %r" % (runtime.mode,))
168
169 return changes
170
171
172 - def _getGroupSets(self):
173 """ Returns the list of groupsets (grouped groups...) to notify
174
175 @return: The list (maybe empty). (C{[GroupSet, ...]})
176 @rtype: C{list}
177 """
178
179 group_changes = {}
180 group_cache = {}
181 changes = self._getChanges()
182 for change in changes:
183 for group in self._getGroupsByChange(change):
184 groupid = id(group)
185 try:
186 group_changes[groupid].append(change)
187 except KeyError:
188 group_cache[groupid] = group
189 group_changes[groupid] = [change]
190
191
192
193 group_sets = []
194 for groupid, changelist in group_changes.items():
195 group = group_cache[groupid]
196 for stored in group_sets:
197
198
199
200
201
202 if stored.changes == changelist and \
203 group._compare(stored.groups[0]):
204 stored.groups.append(group)
205 group = None
206 break
207
208 if group is not None:
209 group_sets.append(GroupSet([group], changelist, changes))
210
211 return group_sets
212
213
214 - def _getGroupsByChange(self, change):
215 """ Returns the matching groups for a particular change
216
217 @param change: The change to select
218 @type change: C{svnmailer.subversion.VersionedPathDescriptor}
219
220 @return: The group list
221 @rtype: C{list}
222 """
223 selected_groups = []
224 ignored_groups = []
225
226
227
228 repos_path = change.repos.path.decode("utf-8", "strict")
229
230
231 path = "%s%s" % (change.path, ["", "/"][change.isDirectory()])
232 path = path.decode("utf-8", "strict")
233
234 for group in self._settings.groups:
235 subst = self._getDefaultSubst(group, repos_path, path)
236
237
238 if group.for_repos:
239 match = group.for_repos.match(repos_path)
240 if match:
241 subst.update(match.groupdict())
242 else:
243 continue
244
245
246 if group.exclude_paths and group.exclude_paths.match(path):
247 continue
248
249
250 if group.for_paths:
251 match = group.for_paths.match(path)
252 if match:
253 subst.update(match.groupdict())
254 else:
255 continue
256
257
258 for name, value in subst.items():
259 group._sub_(name, value)
260
261 (selected_groups, ignored_groups)[
262 bool(group.ignore_if_other_matches)
263 ].append(group)
264
265
266
267
268
269 return selected_groups and selected_groups or ignored_groups
270
271
272 - def _getDefaultSubst(self, group, repos_path, path):
273 """ Returns the default substitution dict
274
275 @param group: The group to consider
276 @type group: C{svnmailer.settings.GroupSettingsContainer}
277
278 @param repos_path: The repository path
279 @type repos_path: C{unicode}
280
281 @param path: The change path
282 @type path: C{unicode}
283
284 @return: The initialized dictionary
285 @rtype: C{dict}
286
287 @exception svnmailer.subversion.Error: An error occured while
288 accessing the subversion repository
289 """
290 from svnmailer.settings import modes
291
292 runtime = self._settings.runtime
293 author = runtime.author
294 if not author and runtime.mode in (modes.commit, modes.propchange):
295 author = runtime._repos.getRevisionAuthor(runtime.revision)
296
297 subst = {
298 'author' : author or 'no_author',
299 'group' : group._name,
300 'property': runtime.propname,
301 'revision': runtime.revision and u"%d" % runtime.revision,
302 }
303
304 if group.extract_x509_author:
305 from svnmailer import util
306
307 x509 = util.extractX509User(author)
308 if x509:
309 from email import Header
310
311 realname, mail = x509
312 subst.update({
313 'x509_address': realname and "%s <%s>" % (
314 Header.Header(realname).encode(), mail) or mail,
315 'x509_CN': realname,
316 'x509_emailAddress': mail,
317 })
318
319 if group._def_for_repos:
320 match = group._def_for_repos.match(repos_path)
321 if match:
322 subst.update(match.groupdict())
323
324 if group._def_for_paths:
325 match = group._def_for_paths.match(path)
326 if match:
327 subst.update(match.groupdict())
328
329 return subst
330
331
332 - def _getSettings(self, options):
333 """ Returns the settings object
334
335 @param options: Command line options
336 @type options: C{svnmailer.cli.SvnmailerOptionParser}
337
338 @return: The settings object
339 @rtype: C{svnmailer.config.ConfigFileSettings}
340
341 @exception ConfigError: configuration error
342 """
343 from svnmailer import config
344
345 try:
346 return config.ConfigFileSettings(options)
347 except config.Error, exc:
348 import sys
349 raise ConfigError, str(exc), sys.exc_info()[2]
350
351
352 - def _openRepository(self):
353 """ Opens the repository
354
355 @exception svnmailer.subversion.Error: Error while accessing the
356 subversion repository
357 """
358 from svnmailer import subversion, util
359
360 config = self._settings.runtime
361 repos_path = util.filename.fromLocale(
362 config.repository, config.path_encoding
363 )
364 if isinstance(repos_path, str):
365
366
367
368
369
370
371 repos_path = repos_path.decode("iso-8859-1", "strict")
372
373 config._repos = subversion.Repository(repos_path)
374
375
377 """ Closes the repository """
378 try:
379 self._settings.runtime._repos.close()
380 except AttributeError:
381
382 pass
383
384
386 """ Container object for a single groupset
387
388 @ivar groups: The groups to process
389 @type groups: C{list}
390
391 @ivar changes: The changes that belong to the group
392 @type changes: C{list}
393
394 @ivar xchanges: The changes that don't belong to the
395 group (only filled if show_nonmatching_paths = yes)
396 @type xchanges: C{list}
397 """
398
399 - def __init__(self, groups, changes, allchanges):
400 """ Initialization
401
402 @param groups: The groups to process
403 @type groups: C{list}
404
405 @param changes: The changes that belong to the group
406 @type changes: C{list}
407
408 @param allchanges: All changes
409 @type allchanges: C{list}
410 """
411 from svnmailer.settings import xpath
412
413 self.groups = groups
414 self.changes = changes
415
416 nongroups = groups[0].show_nonmatching_paths
417 if nongroups == xpath.ignore:
418 self.xchanges = None
419 elif nongroups == xpath.yes:
420 self.xchanges = [
421 change for change in allchanges
422 if change not in changes
423 ]
424 else:
425
426 self.xchanges = []
427