?? mailer.py
字號:
class TextCommitRenderer: "This class will render the commit mail in plain text." def __init__(self, output): self.output = output def render(self, data): "Render the commit defined by 'data'." w = self.output.write w('Author: %s\nDate: %s\nNew Revision: %s\n' % (data.author, data.date, data.rev)) if data.commit_url: w('URL: %s\n\n' % data.commit_url) else: w('\n') w('Log:\n%s\n\n' % data.log) # print summary sections self._render_list('Added', data.added_data) self._render_list('Removed', data.removed_data) self._render_list('Modified', data.modified_data) if data.other_added_data or data.other_removed_data \ or data.other_modified_data: if data.show_nonmatching_paths: w('\nChanges in other areas also in this revision:\n') self._render_list('Added', data.other_added_data) self._render_list('Removed', data.other_removed_data) self._render_list('Modified', data.other_modified_data) else: w('and changes in other areas\n') self._render_diffs(data.diffs, '') if data.other_diffs: self._render_diffs(data.other_diffs, '\nDiffs of changes in other areas also' ' in this revision:\n') def _render_list(self, header, data_list): if not data_list: return w = self.output.write w(header + ':\n') for d in data_list: if d.is_dir: is_dir = '/' else: is_dir = '' if d.props_changed: if d.text_changed: props = ' (contents, props changed)' else: props = ' (props changed)' else: props = '' w(' %s%s%s\n' % (d.path, is_dir, props)) if d.copied: if is_dir: text = '' elif d.text_changed: text = ', changed' else: text = ' unchanged' w(' - copied%s from r%d, %s%s\n' % (text, d.base_rev, d.base_path, is_dir)) def _render_diffs(self, diffs, section_header): """Render diffs. Write the SECTION_HEADER iff there are actually any diffs to render.""" w = self.output.write section_header_printed = False for diff in diffs: if not diff.diff and not diff.diff_url: continue if not section_header_printed: w(section_header) section_header_printed = True if diff.kind == 'D': w('\nDeleted: %s\n' % diff.base_path) elif diff.kind == 'C': w('\nCopied: %s (from r%d, %s)\n' % (diff.path, diff.base_rev, diff.base_path)) elif diff.kind == 'A': w('\nAdded: %s\n' % diff.path) else: # kind == 'M' w('\nModified: %s\n' % diff.path) if diff.diff_url: w('URL: %s\n' % diff.diff_url) if not diff.diff: continue w(SEPARATOR + '\n') if diff.binary: if diff.singular: w('Binary file. No diff available.\n') else: w('Binary files. No diff available.\n') continue for line in diff.content: w(line.raw)class Repository: "Hold roots and other information about the repository." def __init__(self, repos_dir, rev, pool): self.repos_dir = repos_dir self.rev = rev self.pool = pool self.repos_ptr = svn.repos.open(repos_dir, pool) self.fs_ptr = svn.repos.fs(self.repos_ptr) self.roots = { } self.root_this = self.get_root(rev) self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR) def get_rev_prop(self, propname): return svn.fs.revision_prop(self.fs_ptr, self.rev, propname, self.pool) def get_root(self, rev): try: return self.roots[rev] except KeyError: pass root = self.roots[rev] = svn.fs.revision_root(self.fs_ptr, rev, self.pool) return rootclass Config: # The predefined configuration sections. These are omitted from the # set of groups. _predefined = ('general', 'defaults', 'maps') def __init__(self, fname, repos, global_params): cp = ConfigParser.ConfigParser() cp.read(fname) # record the (non-default) groups that we find self._groups = [ ] for section in cp.sections(): if not hasattr(self, section): section_ob = _sub_section() setattr(self, section, section_ob) if section not in self._predefined: self._groups.append(section) else: section_ob = getattr(self, section) for option in cp.options(section): # get the raw value -- we use the same format for *our* interpolation value = cp.get(section, option, raw=1) setattr(section_ob, option, value) # be compatible with old format config files if hasattr(self.general, 'diff') and not hasattr(self.defaults, 'diff'): self.defaults.diff = self.general.diff if not hasattr(self, 'maps'): self.maps = _sub_section() # these params are always available, although they may be overridden self._global_params = global_params.copy() # prepare maps. this may remove sections from consideration as a group. self._prep_maps() # process all the group sections. self._prep_groups(repos) def is_set(self, option): """Return None if the option is not set; otherwise, its value is returned. The option is specified as a dotted symbol, such as 'general.mail_command' """ ob = self for part in string.split(option, '.'): if not hasattr(ob, part): return None ob = getattr(ob, part) return ob def get(self, option, group, params): "Get a config value with appropriate substitutions and value mapping." # find the right value value = None if group: sub = getattr(self, group) value = getattr(sub, option, None) if value is None: value = getattr(self.defaults, option, '') # parameterize it if params is not None: value = value % params # apply any mapper mapper = getattr(self.maps, option, None) if mapper is not None: value = mapper(value) # Apply any parameters that may now be available for # substitution that were not before the mapping. if value is not None and params is not None: value = value % params return value def get_diff_cmd(self, group, args): "Get a diff command as a list of argv elements." ### do some better splitting to enable quoting of spaces diff_cmd = string.split(self.get('diff', group, None)) cmd = [ ] for part in diff_cmd: cmd.append(part % args) return cmd def _prep_maps(self): "Rewrite the [maps] options into callables that look up values." mapsections = [] for optname, mapvalue in vars(self.maps).items(): if mapvalue[:1] == '[': # a section is acting as a mapping sectname = mapvalue[1:-1] if not hasattr(self, sectname): raise UnknownMappingSection(sectname) # construct a lambda to look up the given value as an option name, # and return the option's value. if the option is not present, # then just return the value unchanged. setattr(self.maps, optname, lambda value, sect=getattr(self, sectname): getattr(sect, value, value)) # mark for removal when all optnames are done if sectname not in mapsections: mapsections.append(sectname) # elif test for other mapper types. possible examples: # dbm:filename.db # file:two-column-file.txt # ldap:some-query-spec # just craft a mapper function and insert it appropriately else: raise UnknownMappingSpec(mapvalue) # remove each mapping section from consideration as a group for sectname in mapsections: self._groups.remove(sectname) def _prep_groups(self, repos): self._group_re = [ ] repos_dir = os.path.abspath(repos.repos_dir) # compute the default repository-based parameters. start with some # basic parameters, then bring in the regex-based params. self._default_params = self._global_params try: match = re.match(self.defaults.for_repos, repos_dir) if match: self._default_params = self._default_params.copy() self._default_params.update(match.groupdict()) except AttributeError: # there is no self.defaults.for_repos pass # select the groups that apply to this repository for group in self._groups: sub = getattr(self, group) params = self._default_params if hasattr(sub, 'for_repos'): match = re.match(sub.for_repos, repos_dir) if not match: continue params = params.copy() params.update(match.groupdict()) # if a matching rule hasn't been given, then use the empty string # as it will match all paths for_paths = getattr(sub, 'for_paths', '') exclude_paths = getattr(sub, 'exclude_paths', None) if exclude_paths: exclude_paths_re = re.compile(exclude_paths) else: exclude_paths_re = None self._group_re.append((group, re.compile(for_paths), exclude_paths_re, params)) # after all the groups are done, add in the default group try: self._group_re.append((None, re.compile(self.defaults.for_paths), None, self._default_params)) except AttributeError: # there is no self.defaults.for_paths pass def which_groups(self, path): "Return the path's associated groups." groups = [] for group, pattern, exclude_pattern, repos_params in self._group_re: match = pattern.match(path) if match: if exclude_pattern and exclude_pattern.match(path): continue params = repos_params.copy() params.update(match.groupdict()) groups.append((group, params)) if not groups: groups.append((None, self._default_params)) return groupsclass _sub_section: passclass _data: "Helper class to define an attribute-based hunk o' data." def __init__(self, **kw): vars(self).update(kw)class MissingConfig(Exception): passclass UnknownMappingSection(Exception): passclass UnknownMappingSpec(Exception): passclass UnknownSubcommand(Exception): pass# enable True/False in older vsns of Pythontry: _unused = Trueexcept NameError: True = 1 False = 0if __name__ == '__main__': def usage(): scriptname = os.path.basename(sys.argv[0]) sys.stderr.write("""USAGE: %s commit REPOS REVISION [CONFIG-FILE] %s propchange REPOS REVISION AUTHOR REVPROPNAME [CONFIG-FILE] %s propchange2 REPOS REVISION AUTHOR REVPROPNAME ACTION [CONFIG-FILE] %s lock REPOS AUTHOR [CONFIG-FILE] %s unlock REPOS AUTHOR [CONFIG-FILE]If no CONFIG-FILE is provided, the script will first search for a mailer.conffile in REPOS/conf/. Failing that, it will search the directory in whichthe script itself resides.ACTION was added as a fifth argument to the post-revprop-change hookin Subversion 1.2.0. Its value is one of 'A', 'M' or 'D' to indicateif the property was added, modified or deleted, respectively.""" % (scriptname, scriptname, scriptname, scriptname, scriptname)) sys.exit(1) # Command list: subcommand -> number of arguments expected (not including # the repository directory and config-file) cmd_list = {'commit' : 1, 'propchange' : 3, 'propchange2': 4, 'lock' : 1, 'unlock' : 1, } config_fname = None argc = len(sys.argv) if argc < 3: usage() cmd = sys.argv[1] repos_dir = svn.core.svn_path_canonicalize(sys.argv[2]) try: expected_args = cmd_list[cmd] except KeyError: usage() if argc < (expected_args + 3): usage() elif argc > expected_args + 4: usage() elif argc == (expected_args + 4): config_fname = sys.argv[expected_args + 3] # Settle on a config file location, and open it. if config_fname is None: # Default to REPOS-DIR/conf/mailer.conf. config_fname = os.path.join(repos_dir, 'conf', 'mailer.conf') if not os.path.exists(config_fname): # Okay. Look for 'mailer.conf' as a sibling of this script. config_fname = os.path.join(os.path.dirname(sys.argv[0]), 'mailer.conf') if not os.path.exists(config_fname): raise MissingConfig(config_fname) svn.core.run_app(main, cmd, config_fname, repos_dir, sys.argv[3:3+expected_args])# ------------------------------------------------------------------------# TODO## * add configuration options# - each group defines delivery info:# o whether to set Reply-To and/or Mail-Followup-To# (btw: it is legal do set Reply-To since this is the originator of the# mail; i.e. different from MLMs that munge it)# - each group defines content construction:# o max size of diff before trimming# o max size of entire commit message before truncation# - per-repository configuration# o extra config living in repos# o optional, non-mail log file# o look up authors (username -> email; for the From: header) in a# file(s) or DBM# * get rid of global functions that should properly be class methods
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -