[svn-commits] mmichelson: branch mmichelson/conversion_script r401015 - /team/mmichelson/co...
SVN commits to the Digium repositories
svn-commits at lists.digium.com
Tue Oct 15 12:45:09 CDT 2013
Author: mmichelson
Date: Tue Oct 15 12:45:07 2013
New Revision: 401015
URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=401015
Log:
Address review board comments.
Since the review comments affected code I initially was not
planning to modify, I decided that if I'm going to modify that
code, I'll go ahead and make it all PEP-8 compliant.
Modified:
team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/astconfigparser.py
team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py
Modified: team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/astconfigparser.py
URL: http://svnview.digium.com/svn/asterisk/team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/astconfigparser.py?view=diff&rev=401015&r1=401014&r2=401015
==============================================================================
--- team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/astconfigparser.py (original)
+++ team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/astconfigparser.py Tue Oct 15 12:45:07 2013
@@ -2,12 +2,13 @@
from astdicts import OrderedDict
from astdicts import MultiOrderedDict
+
def merge_values(left, right, key):
"""Merges values from right into left."""
if isinstance(left, list):
vals0 = left
- else: # assume dictionary
+ else: # assume dictionary
vals0 = left[key] if key in left else []
vals1 = right[key] if key in right else []
@@ -15,14 +16,16 @@
###############################################################################
+
class Section(MultiOrderedDict):
- """A Section is a MultiOrderedDict itself that maintains a list of
- key/value options. However, in the case of an Asterisk config
- file a section may have other defaults sections that is can pull
- data from (i.e. templates). So when an option is looked up by key
- it first checks the base section and if not found looks in the
- added default sections. If not found at that point then a 'KeyError'
- exception is raised.
+ """
+ A Section is a MultiOrderedDict itself that maintains a list of
+ key/value options. However, in the case of an Asterisk config
+ file a section may have other defaults sections that is can pull
+ data from (i.e. templates). So when an option is looked up by key
+ it first checks the base section and if not found looks in the
+ added default sections. If not found at that point then a 'KeyError'
+ exception is raised.
"""
count = 0
@@ -35,9 +38,24 @@
self._templates = [] if templates is None else templates
def __cmp__(self, other):
+ """
+ Use self.id as means of determining equality
+ """
return cmp(self.id, other.id)
- def get(self, key, from_self=True, from_templates=True, from_defaults=True):
+ def get(self, key, from_self=True, from_templates=True,
+ from_defaults=True):
+ """
+ Get the values corresponding to a given key. The parameters to this
+ function form a hierarchy that determines priority of the search.
+ from_self takes priority over from_templates, and from_templates takes
+ priority over from_defaults.
+
+ Parameters:
+ from_self - If True, search within the given section.
+ from_templates - If True, search in this section's templates.
+ from_defaults - If True, search within this section's defaults.
+ """
if from_self and key in self:
return MultiOrderedDict.__getitem__(self, key)
@@ -62,13 +80,19 @@
raise KeyError(key)
def __getitem__(self, key):
- """Get the value for the given key. If it is not found in the 'self'
- then check inside templates and defaults before declaring raising
- a KeyError exception.
+ """
+ Get the value for the given key. If it is not found in the 'self'
+ then check inside templates and defaults before declaring raising
+ a KeyError exception.
"""
return self.get(key)
def keys(self, self_only=False):
+ """
+ Get the keys from this section. If self_only is True, then
+ keys from this section's defaults and templates are not
+ included in the returned value
+ """
res = MultiOrderedDict.keys(self)
if self_only:
return res
@@ -85,13 +109,21 @@
return res
def add_defaults(self, defaults):
+ """
+ Add a list of defaults to the section. Defaults are
+ sections such as 'general'
+ """
defaults.sort()
for i in defaults:
self._defaults.insert(0, i)
def add_templates(self, templates):
- templates.sort(reverse=True);
- self._templates.extend(templates)
+ """
+ Add a list of templates to the section.
+ """
+ templates.sort()
+ for i in templates:
+ self._templates.insert(0, i)
def get_merged(self, key):
"""Return a list of values for a given key merged from default(s)"""
@@ -120,9 +152,11 @@
DEFAULTSECT = 'general'
+
def remove_comment(line, is_comment):
"""Remove any commented elements from the line."""
- if not line: return line, is_comment
+ if not line:
+ return line, is_comment
if is_comment:
part = line.partition(COMMENT_END)
@@ -152,17 +186,21 @@
# check for eol comment
return line.partition(COMMENT)[0].strip(), False
+
def try_include(line):
- """Checks to see if the given line is an include. If so return the
- included filename, otherwise None.
+ """
+ Checks to see if the given line is an include. If so return the
+ included filename, otherwise None.
"""
match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
return match.group(1) if match else None
+
def try_section(line):
- """Checks to see if the given line is a section. If so return the section
- name, otherwise return 'None'.
+ """
+ Checks to see if the given line is a section. If so return the section
+ name, otherwise return 'None'.
"""
# leading spaces were stripped when checking for comments
if not line.startswith('['):
@@ -182,6 +220,7 @@
except:
return section[1:], False, templates
+
def try_option(line):
"""Parses the line as an option, returning the key/value pair."""
data = re.split('=>?', line)
@@ -190,30 +229,12 @@
###############################################################################
-def find_value(sections, key):
- """Given a list of sections, try to find value(s) for the given key."""
- # always start looking in the last one added
- sections.sort(reverse=True);
- for s in sections:
- try:
- # try to find in section and section's templates
- return s.get(key, from_defaults=False)
- except KeyError:
- pass
-
- # wasn't found in sections or a section's templates so check in defaults
- for s in sections:
- try:
- # try to find in section's defaultsects
- return s.get(key, from_self=False, from_templates=False)
- except KeyError:
- pass
-
- raise KeyError(key)
def find_dict(mdicts, key, val):
- """Given a list of mult-dicts, return the multi-dict that contains
- the given key/value pair."""
+ """
+ Given a list of mult-dicts, return the multi-dict that contains
+ the given key/value pair.
+ """
def found(d):
return key in d and val in d[key]
@@ -224,43 +245,24 @@
raise LookupError("Dictionary not located for key = %s, value = %s"
% (key, val))
-def get_sections(parser, key, attr='_sections', searched=None):
- if searched is None:
- searched = []
- if parser is None or parser in searched:
- return []
-
- try:
- sections = getattr(parser, attr)
- res = sections[key] if key in sections else []
- searched.append(parser)
- return res + get_sections(parser._includes, key, attr, searched) \
- + get_sections(parser._parent, key, attr, searched)
- except:
- # assume ordereddict of parsers
- res = []
- for p in parser.itervalues():
- res.extend(get_sections(p, key, attr, searched))
- return res
-
-def get_defaults(parser, key):
- return get_sections(parser, key, '_defaults')
-
-def write_dicts(file, mdicts):
+
+def write_dicts(config_file, mdicts):
+ """Write the contents of the mdicts to the specified config file"""
for section, sect_list in mdicts.iteritems():
# every section contains a list of dictionaries
for sect in sect_list:
- file.write("[%s]\n" % section)
+ config_file.write("[%s]\n" % section)
for key, val_list in sect.iteritems():
# every value is also a list
for v in val_list:
key_val = key
if v is not None:
key_val += " = " + str(v)
- file.write("%s\n" % (key_val))
- file.write("\n")
+ config_file.write("%s\n" % (key_val))
+ config_file.write("\n")
###############################################################################
+
class MultiOrderedConfigParser:
def __init__(self, parent=None):
@@ -269,16 +271,39 @@
self._sections = MultiOrderedDict()
self._includes = OrderedDict()
+ def find_value(self, sections, key):
+ """Given a list of sections, try to find value(s) for the given key."""
+ # always start looking in the last one added
+ sections.sort(reverse=True)
+ for s in sections:
+ try:
+ # try to find in section and section's templates
+ return s.get(key, from_defaults=False)
+ except KeyError:
+ pass
+
+ # wasn't found in sections or a section's templates so check in
+ # defaults
+ for s in sections:
+ try:
+ # try to find in section's defaultsects
+ return s.get(key, from_self=False, from_templates=False)
+ except KeyError:
+ pass
+
+ raise KeyError(key)
+
def defaults(self):
return self._defaults
def default(self, key):
"""Retrieves a list of dictionaries for a default section."""
- return get_defaults(self, key)
+ return self.get_defaults(key)
def add_default(self, key, template_keys=None):
- """Adds a default section to defaults, returning the
- default Section object.
+ """
+ Adds a default section to defaults, returning the
+ default Section object.
"""
if template_keys is None:
template_keys = []
@@ -289,17 +314,47 @@
def section(self, key):
"""Retrieves a list of dictionaries for a section."""
- return get_sections(self, key)
+ return self.get_sections(key)
+
+ def get_sections(self, key, attr='_sections', searched=None):
+ """
+ Retrieve a list of sections that have values for the given key.
+ The attr parameter can be used to control what part of the parser
+ to retrieve values from.
+ """
+ if searched is None:
+ searched = []
+ if self in searched:
+ return []
+
+ sections = getattr(self, attr)
+ res = sections[key] if key in sections else []
+ searched.append(self)
+ if self._includes:
+ res += self._includes.get_sections(key, attr, searched)
+ if self._parent:
+ res += self._parent.get_sections(key, attr, searched)
+ return res
+
+ def get_defaults(self, key):
+ """
+ Retrieve a list of defaults that have values for the given key.
+ """
+ return self.get_sections(key, '_defaults')
def add_section(self, key, template_keys=None, mdicts=None):
+ """
+ Create a new section in the configuration. The name of the
+ new section is the 'key' parameter.
+ """
if template_keys is None:
template_keys = []
if mdicts is None:
mdicts = self._sections
res = Section()
for t in template_keys:
- res.add_templates(get_defaults(self, t))
- res.add_defaults(get_defaults(self, DEFAULTSECT))
+ res.add_templates(self.get_defaults(t))
+ res.add_defaults(self.get_defaults(DEFAULTSECT))
mdicts.insert(0, key, res)
return res
@@ -307,35 +362,40 @@
return self._includes
def add_include(self, filename, parser=None):
+ """
+ Add a new #include file to the configuration.
+ """
if filename in self._includes:
return self._includes[filename]
self._includes[filename] = res = \
- MultiOrderedConfigParser(self) if parser is None else parser
- return res;
+ MultiOrderedConfigParser(self) if parser is None else parser
+ return res
def get(self, section, key):
"""Retrieves the list of values from a section for a key."""
try:
# search for the value in the list of sections
- return find_value(self.section(section), key)
+ return self.find_value(self.section(section), key)
except KeyError:
pass
try:
# section may be a default section so, search
# for the value in the list of defaults
- return find_value(self.default(section), key)
+ return self.find_value(self.default(section), key)
except KeyError:
raise LookupError("key %r not found for section %r"
% (key, section))
def multi_get(self, section, key_list):
- """Retrieves the list of values from a section for a list of keys.
+ """
+ Retrieves the list of values from a section for a list of keys.
This method is intended to be used for equivalent keys. Thus, as soon
as any match is found for any key in the key_list, the match is
returned. This does not concatenate the lookups of all of the keys
- together."""
+ together.
+ """
for i in key_list:
try:
return self.get(section, i)
@@ -343,7 +403,8 @@
pass
# Making it here means all lookups failed.
- raise LookupError("keys %r not found for section %r" % (key_list, section))
+ raise LookupError("keys %r not found for section %r" %
+ (key_list, section))
def set(self, section, key, val):
"""Sets an option in the given section."""
@@ -355,15 +416,17 @@
self.defaults(section)[0][key] = val
def read(self, filename):
+ """Parse configuration information from a file"""
try:
- with open(filename, 'rt') as file:
- self._read(file, filename)
+ with open(filename, 'rt') as config_file:
+ self._read(config_file)
except IOError:
print "Could not open file ", filename, " for reading"
- def _read(self, file, filename):
- is_comment = False # used for multi-lined comments
- for line in file:
+ def _read(self, config_file):
+ """Parse configuration information from the config_file"""
+ is_comment = False # used for multi-lined comments
+ for line in config_file:
line, is_comment = remove_comment(line, is_comment)
if not line:
# line was empty or was a comment
@@ -386,18 +449,19 @@
key, val = try_option(line)
sect[key] = val
- def write(self, f):
+ def write(self, config_file):
+ """Write configuration information out to a file"""
try:
for key, val in self._includes.iteritems():
val.write(key)
- f.write('#include "%s"\n' % key)
-
- f.write('\n')
- write_dicts(f, self._defaults)
- write_dicts(f, self._sections)
+ config_file.write('#include "%s"\n' % key)
+
+ config_file.write('\n')
+ write_dicts(config_file, self._defaults)
+ write_dicts(config_file, self._sections)
except:
try:
- with open(f, 'wt') as fp:
+ with open(config_file, 'wt') as fp:
self.write(fp)
except IOError:
- print "Could not open file ", f, " for writing"
+ print "Could not open file ", config_file, " for writing"
Modified: team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py
URL: http://svnview.digium.com/svn/asterisk/team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py?view=diff&rev=401015&r1=401014&r2=401015
==============================================================================
--- team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py (original)
+++ team/mmichelson/conversion_script/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py Tue Oct 15 12:45:07 2013
@@ -1,13 +1,4 @@
#!/usr/bin/python
-
-###############################################################################
-# TODO:
-# (1) There is more work to do here, at least for the sip.conf items that
-# aren't currently parsed. An issue will be created for that.
-# (2) All of the scripts should probably be passed through pylint and have
-# as many PEP8 issues fixed as possible
-# (3) A public review is probably warranted at that point of the entire script
-###############################################################################
import optparse
import astdicts
@@ -20,16 +11,31 @@
###############################################################################
### some utility functions
###############################################################################
+
+
def section_by_type(section, pjsip, type):
"""Finds a section based upon the given type, adding it if not found."""
- try:
- return astconfigparser.find_dict(
- pjsip.section(section), 'type', type)
+ def __find_dict(mdicts, key, val):
+ """Given a list of mult-dicts, return the multi-dict that contains
+ the given key/value pair."""
+
+ def found(d):
+ return key in d and val in d[key]
+
+ try:
+ return [d for d in mdicts if found(d)][0]
+ except IndexError:
+ raise LookupError("Dictionary not located for key = %s, value = %s"
+ % (key, val))
+
+ try:
+ return __find_dict(pjsip.section(section), 'type', type)
except LookupError:
# section for type doesn't exist, so add
sect = pjsip.add_section(section)
sect['type'] = type
return sect
+
def set_value(key=None, val=None, section=None, pjsip=None,
nmapped=None, type='endpoint'):
@@ -46,6 +52,7 @@
section_by_type(section, pjsip, type)[key] = \
val[0] if isinstance(val, list) else val
+
def merge_value(key=None, val=None, section=None, pjsip=None,
nmapped=None, type='endpoint', section_to=None):
"""Merge values from the given section with those from the default."""
@@ -67,11 +74,9 @@
set_value(key, i, section_to if section_to else section,
pjsip, nmapped, type)
-def is_in(s, sub):
- """Returns true if 'sub' is in 's'"""
- return s.find(sub) != -1
def non_mapped(nmapped):
+ """Write non-mapped sip.conf values to the non-mapped object"""
def _non_mapped(section, key, val):
"""Writes a non-mapped value from sip.conf to the non-mapped object."""
if section not in nmapped:
@@ -91,9 +96,11 @@
### write to given section in pjsip.conf
###############################################################################
+
def set_dtmfmode(key, val, section, pjsip, nmapped):
- """Sets the dtmfmode value. If value matches allowable option in pjsip
- then map it, otherwise set it to none.
+ """
+ Sets the dtmfmode value. If value matches allowable option in pjsip
+ then map it, otherwise set it to none.
"""
# available pjsip.conf values: rfc4733, inband, info, none
if val == 'inband' or val == 'info':
@@ -104,22 +111,25 @@
nmapped(section, key, val + " ; did not fully map - set to none")
set_value(key, 'none', section, pjsip, nmapped)
+
def from_nat(key, val, section, pjsip, nmapped):
"""Sets values from nat into the appropriate pjsip.conf options."""
# nat from sip.conf can be comma separated list of values:
# yes/no, [auto_]force_rport, [auto_]comedia
- if is_in(val, 'yes'):
+ if 'yes' in val:
set_value('rtp_symmetric', 'yes', section, pjsip, nmapped)
set_value('rewrite_contact', 'yes', section, pjsip, nmapped)
- if is_in(val, 'comedia'):
+ if 'comedia' in val:
set_value('rtp_symmetric', 'yes', section, pjsip, nmapped)
- if is_in(val, 'force_rport'):
+ if 'force_rport' in val:
set_value('force_rport', 'yes', section, pjsip, nmapped)
set_value('rewrite_contact', 'yes', section, pjsip, nmapped)
+
def set_timers(key, val, section, pjsip, nmapped):
- """Sets the timers in pjsip.conf from the session-timers option
- found in sip.conf.
+ """
+ Sets the timers in pjsip.conf from the session-timers option
+ found in sip.conf.
"""
# pjsip.conf values can be yes/no, required, always
if val == 'originate':
@@ -131,20 +141,25 @@
else:
set_value('timers', 'yes', section, pjsip, nmapped)
+
def set_direct_media(key, val, section, pjsip, nmapped):
- """Maps values from the sip.conf comma separated direct_media option
- into pjsip.conf direct_media options.
- """
- if is_in(val, 'yes'):
+ """
+ Maps values from the sip.conf comma separated direct_media option
+ into pjsip.conf direct_media options.
+ """
+ if 'yes' in val:
set_value('direct_media', 'yes', section, pjsip, nmapped)
- if is_in(val, 'update'):
+ if 'update' in val:
set_value('direct_media_method', 'update', section, pjsip, nmapped)
- if is_in(val, 'outgoing'):
- set_value('directed_media_glare_mitigation', 'outgoing', section, pjsip, nmapped)
- if is_in(val, 'nonat'):
- set_value('disable_directed_media_on_nat','yes', section, pjsip, nmapped)
- if (val == 'no'):
+ if 'outgoing' in val:
+ set_value('directed_media_glare_mitigation', 'outgoing', section,
+ pjsip, nmapped)
+ if 'nonat' in val:
+ set_value('disable_directed_media_on_nat', 'yes', section, pjsip,
+ nmapped)
+ if 'no' in val:
set_value('direct_media', 'no', section, pjsip, nmapped)
+
def from_sendrpid(key, val, section, pjsip, nmapped):
"""Sets the send_rpid/pai values in pjsip.conf."""
@@ -153,6 +168,7 @@
elif val == 'pai':
set_value('send_pai', 'yes', section, pjsip, nmapped)
+
def set_media_encryption(key, val, section, pjsip, nmapped):
"""Sets the media_encryption value in pjsip.conf"""
try:
@@ -166,12 +182,15 @@
if val == 'yes':
set_value('media_encryption', 'sdes', section, pjsip, nmapped)
+
def from_recordfeature(key, val, section, pjsip, nmapped):
- """If record on/off feature is set to automixmon then set
- one_touch_recording, otherwise it can't be mapped.
+ """
+ If record on/off feature is set to automixmon then set
+ one_touch_recording, otherwise it can't be mapped.
"""
set_value('one_touch_recording', 'yes', section, pjsip, nmapped)
set_value(key, val, section, pjsip, nmapped)
+
def from_progressinband(key, val, section, pjsip, nmapped):
"""Sets the inband_progress value in pjsip.conf"""
@@ -215,8 +234,9 @@
def from_host(key, val, section, pjsip, nmapped):
- """Sets contact info in an AOR section in pjsip.conf using 'host'
- and 'port' data from sip.conf
+ """
+ Sets contact info in an AOR section in pjsip.conf using 'host'
+ and 'port' data from sip.conf
"""
# all aors have the same name as the endpoint so makes
# it easy to set endpoint's 'aors' value
@@ -247,10 +267,11 @@
def from_mailbox(key, val, section, pjsip, nmapped):
- """Determines whether a mailbox configured in sip.conf should map to
- an endpoint or aor in pjsip.conf. If subscribemwi is true, then the
- mailboxes are set on an aor. Otherwise the mailboxes are set on the
- endpoint.
+ """
+ Determines whether a mailbox configured in sip.conf should map to
+ an endpoint or aor in pjsip.conf. If subscribemwi is true, then the
+ mailboxes are set on an aor. Otherwise the mailboxes are set on the
+ endpoint.
"""
try:
@@ -518,6 +539,12 @@
def split_hostport(addr):
+ """
+ Given an address in the form 'addr:port' separate the addr and port
+ components.
+ Returns a two-tuple of strings, (addr, port). If no port is present in the
+ string, then the port section of the tuple is None.
+ """
try:
socket.inet_pton(socket.AF_INET6, addr)
if not addr.startswith('['):
@@ -760,7 +787,8 @@
def map_transports(sip, pjsip, nmapped):
- """Finds options in sip.conf general section pertaining to
+ """
+ Finds options in sip.conf general section pertaining to
transport configuration and creates appropriate transport
configuration sections in pjsip.conf.
@@ -1003,6 +1031,10 @@
def map_peer(sip, section, pjsip, nmapped):
+ """
+ Map the options from a peer section in sip.conf into the appropriate
+ sections in pjsip.conf
+ """
for i in peer_map:
try:
# coming from sip.conf the values should mostly be a list with a
@@ -1010,25 +1042,38 @@
# function (see merge_value) is used to retrieve the values.
i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped)
except LookupError:
- pass # key not found in sip.conf
+ pass # key not found in sip.conf
+
def find_non_mapped(sections, nmapped):
+ """
+ Determine sip.conf options that were not properly mapped to pjsip.conf
+ options.
+ """
for section, sect in sections.iteritems():
try:
# since we are pulling from sip.conf this should always
# be a single value list
sect = sect[0]
- # loop through the section and store any values that were not mapped
+ # loop through the section and store any values that were not
+ # mapped
for key in sect.keys(True):
for i in peer_map:
if i[0] == key:
- break;
+ break
else:
nmapped(section, key, sect[key])
except LookupError:
pass
+
def convert(sip, filename, non_mappings, include):
+ """
+ Entry point for configuration file conversion. This
+ function will create a pjsip.conf object and begin to
+ map specific sections from sip.conf into it.
+ Returns the new pjsip.conf object once completed
+ """
pjsip = astconfigparser.MultiOrderedConfigParser()
non_mappings[filename] = astdicts.MultiOrderedDict()
nmapped = non_mapped(non_mappings[filename])
@@ -1047,11 +1092,15 @@
find_non_mapped(sip.sections(), nmapped)
for key, val in sip.includes().iteritems():
- pjsip.add_include(PREFIX + key, convert(val, PREFIX + key, non_mappings,
- True)[0])
+ pjsip.add_include(PREFIX + key, convert(val, PREFIX + key,
+ non_mappings, True)[0])
return pjsip, non_mappings
+
def write_pjsip(filename, pjsip, non_mappings):
+ """
+ Write pjsip.conf file to disk
+ """
try:
with open(filename, 'wt') as fp:
fp.write(';--\n')
@@ -1064,20 +1113,19 @@
fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
fp.write('--;\n\n')
# write out include file(s)
- for key, val in pjsip.includes().iteritems():
- write_pjsip(key, val, non_mappings)
- fp.write('#include "%s"\n' % key)
- fp.write('\n')
- # write out mapped data elements
- astconfigparser.write_dicts(fp, pjsip.defaults())
- astconfigparser.write_dicts(fp, pjsip.sections())
+ pjsip.write(fp)
except IOError:
print "Could not open file ", filename, " for writing"
###############################################################################
+
def cli_options():
+ """
+ Parse command line options and apply them. If invalid input is given,
+ print usage information
+ """
global PREFIX
usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
"input-file defaults to 'sip.conf'\n" \
More information about the svn-commits
mailing list