Extract functions generate_edits_from_blocks() and write_edits().
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blobbfe98149b3dbe29d67422b4ae2777d4c6e00c7ea
1 # (Be in -*- python -*- mode.)
3 # ====================================================================
4 # Copyright (c) 2000-2008 CollabNet. All rights reserved.
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://subversion.tigris.org/license-1.html.
9 # If newer versions of this license are posted there, you may use a
10 # newer version instead, at your option.
12 # This software consists of voluntary contributions made by many
13 # individuals. For exact contribution history, see the revision
14 # history and logs, available at http://cvs2svn.tigris.org/.
15 # ====================================================================
17 """This module contains classes to set Subversion properties on files."""
20 import os
21 import re
22 import fnmatch
23 import ConfigParser
24 from cStringIO import StringIO
26 from cvs2svn_lib.common import warning_prefix
27 from cvs2svn_lib.log import Log
30 def _squash_case(s):
31 return s.lower()
34 def _preserve_case(s):
35 return s
38 class SVNPropertySetter:
39 """Abstract class for objects that can set properties on a SVNCommitItem."""
41 def set_properties(self, s_item):
42 """Set any properties that can be determined for S_ITEM.
44 S_ITEM is an instance of SVNCommitItem. This method should modify
45 S_ITEM.svn_props in place."""
47 raise NotImplementedError
50 class CVSRevisionNumberSetter(SVNPropertySetter):
51 """Store the CVS revision number to an SVN property."""
53 def __init__(self, propname='cvs2svn:cvs-rev'):
54 self.propname = propname
56 def set_properties(self, s_item):
57 if self.propname in s_item.svn_props:
58 return
60 s_item.svn_props[self.propname] = s_item.cvs_rev.rev
61 s_item.svn_props_changed = True
64 class ExecutablePropertySetter(SVNPropertySetter):
65 """Set the svn:executable property based on cvs_rev.cvs_file.executable."""
67 propname = 'svn:executable'
69 def set_properties(self, s_item):
70 if self.propname in s_item.svn_props:
71 return
73 if s_item.cvs_rev.cvs_file.executable:
74 s_item.svn_props[self.propname] = '*'
77 class DescriptionPropertySetter(SVNPropertySetter):
78 """Set the cvs:description property based on cvs_rev.cvs_file.description."""
80 def __init__(self, propname='cvs:description'):
81 self.propname = propname
83 def set_properties(self, s_item):
84 if self.propname in s_item.svn_props:
85 return
87 if s_item.cvs_rev.cvs_file.description:
88 s_item.svn_props[self.propname] = s_item.cvs_rev.cvs_file.description
91 class CVSBinaryFileEOLStyleSetter(SVNPropertySetter):
92 """Set the eol-style to None for files with CVS mode '-kb'."""
94 propname = 'svn:eol-style'
96 def set_properties(self, s_item):
97 if self.propname in s_item.svn_props:
98 return
100 if s_item.cvs_rev.cvs_file.mode == 'b':
101 s_item.svn_props[self.propname] = None
104 class MimeMapper(SVNPropertySetter):
105 """A class that provides mappings from file names to MIME types."""
107 propname = 'svn:mime-type'
109 def __init__(
110 self, mime_types_file=None, mime_mappings=None,
111 ignore_case=False
113 """Constructor.
115 Arguments:
117 mime_types_file -- a path to a MIME types file on disk. Each
118 line of the file should contain the MIME type, then a
119 whitespace-separated list of file extensions; e.g., one line
120 might be 'text/plain txt c h cpp hpp'.
122 mime_mappings -- a dictionary mapping a file extension to a MIME
123 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
125 ignore_case -- True iff case should be ignored in filename
126 extensions. Setting this option to True can be useful if
127 your CVS repository was used on systems with
128 case-insensitive filenames, in which case you might have a
129 mix of uppercase and lowercase filenames."""
131 self.mappings = { }
132 if ignore_case:
133 self.transform_case = _squash_case
134 else:
135 self.transform_case = _preserve_case
137 if mime_types_file is None and mime_mappings is None:
138 Log().error('Should specify MIME types file or dict.\n')
140 if mime_types_file is not None:
141 for line in file(mime_types_file):
142 if line.startswith("#"):
143 continue
145 # format of a line is something like
146 # text/plain c h cpp
147 extensions = line.split()
148 if len(extensions) < 2:
149 continue
150 type = extensions.pop(0)
151 for ext in extensions:
152 ext = self.transform_case(ext)
153 if ext in self.mappings and self.mappings[ext] != type:
154 Log().error(
155 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
156 % (warning_prefix, ext, self.mappings[ext], type)
158 self.mappings[ext] = type
160 if mime_mappings is not None:
161 for ext, type in mime_mappings.iteritems():
162 ext = self.transform_case(ext)
163 if ext in self.mappings and self.mappings[ext] != type:
164 Log().error(
165 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
166 % (warning_prefix, ext, self.mappings[ext], type)
168 self.mappings[ext] = type
170 def set_properties(self, s_item):
171 if self.propname in s_item.svn_props:
172 return
174 basename, extension = os.path.splitext(s_item.cvs_rev.cvs_file.basename)
176 # Extension includes the dot, so strip it (will leave extension
177 # empty if filename ends with a dot, which is ok):
178 extension = extension[1:]
180 # If there is no extension (or the file ends with a period), use
181 # the base name for mapping. This allows us to set mappings for
182 # files such as README or Makefile:
183 if not extension:
184 extension = basename
186 extension = self.transform_case(extension)
188 mime_type = self.mappings.get(extension, None)
189 if mime_type is not None:
190 s_item.svn_props[self.propname] = mime_type
193 class AutoPropsPropertySetter(SVNPropertySetter):
194 """Set arbitrary svn properties based on an auto-props configuration.
196 This class supports case-sensitive or case-insensitive pattern
197 matching. The command-line default is case-insensitive behavior,
198 consistent with Subversion (see
199 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
201 As a special extension to Subversion's auto-props handling, if a
202 property name is preceded by a '!' then that property is forced to
203 be left unset.
205 If a property specified in auto-props has already been set to a
206 different value, print a warning and leave the old property value
207 unchanged.
209 Python's treatment of whitespaces in the ConfigParser module is
210 buggy and inconsistent. Usually spaces are preserved, but if there
211 is at least one semicolon in the value, and the *first* semicolon is
212 preceded by a space, then that is treated as the start of a comment
213 and the rest of the line is silently discarded."""
215 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
216 property_unset_re = re.compile(
217 r'^\!\s*' + property_name_pattern + r'$'
219 property_set_re = re.compile(
220 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
222 property_novalue_re = re.compile(
223 r'^' + property_name_pattern + r'$'
226 quoted_re = re.compile(
227 r'^([\'\"]).*\1$'
229 comment_re = re.compile(r'\s;')
231 class Pattern:
232 """Describes the properties to be set for files matching a pattern."""
234 def __init__(self, pattern, propdict):
235 # A glob-like pattern:
236 self.pattern = pattern
237 # A dictionary of properties that should be set:
238 self.propdict = propdict
240 def match(self, basename):
241 """Does the file with the specified basename match pattern?"""
243 return fnmatch.fnmatch(basename, self.pattern)
245 def __init__(self, configfilename, ignore_case=True):
246 config = ConfigParser.ConfigParser()
247 if ignore_case:
248 self.transform_case = _squash_case
249 else:
250 config.optionxform = _preserve_case
251 self.transform_case = _preserve_case
253 configtext = open(configfilename).read()
254 if self.comment_re.search(configtext):
255 Log().warn(
256 '%s: Please be aware that a space followed by a\n'
257 'semicolon is sometimes treated as a comment in configuration\n'
258 'files. This pattern was seen in\n'
259 ' %s\n'
260 'Please make sure that you have not inadvertently commented\n'
261 'out part of an important line.'
262 % (warning_prefix, configfilename,)
265 config.readfp(StringIO(configtext), configfilename)
266 self.patterns = []
267 sections = config.sections()
268 sections.sort()
269 for section in sections:
270 if self.transform_case(section) == 'auto-props':
271 patterns = config.options(section)
272 patterns.sort()
273 for pattern in patterns:
274 value = config.get(section, pattern)
275 if value:
276 self._add_pattern(pattern, value)
278 def _add_pattern(self, pattern, props):
279 propdict = {}
280 if self.quoted_re.match(pattern):
281 Log().warn(
282 '%s: Quoting is not supported in auto-props; please verify rule\n'
283 'for %r. (Using pattern including quotation marks.)\n'
284 % (warning_prefix, pattern,)
286 for prop in props.split(';'):
287 prop = prop.strip()
288 m = self.property_unset_re.match(prop)
289 if m:
290 name = m.group('name')
291 Log().debug(
292 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
294 propdict[name] = None
295 continue
297 m = self.property_set_re.match(prop)
298 if m:
299 name = m.group('name')
300 value = m.group('value')
301 if self.quoted_re.match(value):
302 Log().warn(
303 '%s: Quoting is not supported in auto-props; please verify\n'
304 'rule %r for pattern %r. (Using value\n'
305 'including quotation marks.)\n'
306 % (warning_prefix, prop, pattern,)
308 Log().debug(
309 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
311 propdict[name] = value
312 continue
314 m = self.property_novalue_re.match(prop)
315 if m:
316 name = m.group('name')
317 Log().debug(
318 'auto-props: For %r, setting %r to the empty string'
319 % (pattern, name,)
321 propdict[name] = ''
322 continue
324 Log().warn(
325 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
326 % (warning_prefix, pattern, prop,)
329 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
331 def get_propdict(self, cvs_file):
332 basename = self.transform_case(cvs_file.basename)
333 propdict = {}
334 for pattern in self.patterns:
335 if pattern.match(basename):
336 for (key,value) in pattern.propdict.items():
337 if key in propdict:
338 if propdict[key] != value:
339 Log().warn(
340 "Contradictory values set for property '%s' for file %s."
341 % (key, cvs_file,))
342 else:
343 propdict[key] = value
345 return propdict
347 def set_properties(self, s_item):
348 propdict = self.get_propdict(s_item.cvs_rev.cvs_file)
349 for (k,v) in propdict.items():
350 if k in s_item.svn_props:
351 if s_item.svn_props[k] != v:
352 Log().warn(
353 "Property '%s' already set to %r for file %s; "
354 "auto-props value (%r) ignored."
355 % (k, s_item.svn_props[k], s_item.cvs_rev.cvs_path, v,))
356 else:
357 s_item.svn_props[k] = v
360 class CVSBinaryFileDefaultMimeTypeSetter(SVNPropertySetter):
361 """If the file is binary and its svn:mime-type property is not yet
362 set, set it to 'application/octet-stream'."""
364 propname = 'svn:mime-type'
366 def set_properties(self, s_item):
367 if self.propname in s_item.svn_props:
368 return
370 if s_item.cvs_rev.cvs_file.mode == 'b':
371 s_item.svn_props[self.propname] = 'application/octet-stream'
374 class EOLStyleFromMimeTypeSetter(SVNPropertySetter):
375 """Set svn:eol-style based on svn:mime-type.
377 If svn:mime-type is known but svn:eol-style is not, then set
378 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
379 starts with 'text/', then set svn:eol-style to native; otherwise,
380 force it to remain unset. See also issue #39."""
382 propname = 'svn:eol-style'
384 def set_properties(self, s_item):
385 if self.propname in s_item.svn_props:
386 return
388 if s_item.svn_props.get('svn:mime-type', None) is not None:
389 if s_item.svn_props['svn:mime-type'].startswith("text/"):
390 s_item.svn_props[self.propname] = 'native'
391 else:
392 s_item.svn_props[self.propname] = None
395 class DefaultEOLStyleSetter(SVNPropertySetter):
396 """Set the eol-style if one has not already been set."""
398 propname = 'svn:eol-style'
400 def __init__(self, value):
401 """Initialize with the specified default VALUE."""
403 self.value = value
405 def set_properties(self, s_item):
406 if self.propname in s_item.svn_props:
407 return
409 s_item.svn_props[self.propname] = self.value
412 class SVNBinaryFileKeywordsPropertySetter(SVNPropertySetter):
413 """Turn off svn:keywords for files with binary svn:eol-style."""
415 propname = 'svn:keywords'
417 def set_properties(self, s_item):
418 if self.propname in s_item.svn_props:
419 return
421 if not s_item.svn_props.get('svn:eol-style'):
422 s_item.svn_props[self.propname] = None
425 class KeywordsPropertySetter(SVNPropertySetter):
426 """If the svn:keywords property is not yet set, set it based on the
427 file's mode. See issue #2."""
429 propname = 'svn:keywords'
431 def __init__(self, value):
432 """Use VALUE for the value of the svn:keywords property if it is
433 to be set."""
435 self.value = value
437 def set_properties(self, s_item):
438 if self.propname in s_item.svn_props:
439 return
441 if s_item.cvs_rev.cvs_file.mode in [None, 'kv', 'kvl']:
442 s_item.svn_props[self.propname] = self.value