1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 """Handling of Subversion properties."""
18 __author__ = "Jelmer Vernooij <jelmer@samba.org>"
19 __docformat__ = "restructuredText"
21 import bisect, time, urlparse
24 class InvalidExternalsDescription(Exception):
25 _fmt = """Unable to parse externals description."""
28 def is_valid_property_name(prop):
29 """Check the validity of a property name.
31 :param prop: Property name
32 :return: Whether prop is a valid property name
34 if not prop[0].isalnum() and not prop[0] in ":_":
37 if not c.isalnum() and not c in "-:._":
42 def time_to_cstring(timestamp):
43 """Determine string representation of a time.
45 :param timestamp: Timestamp
46 :return: string with date
48 tm_usec = timestamp % 1000000
49 (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
50 tm_sec, tm_wday, tm_yday, tm_isdst) = time.gmtime(timestamp / 1000000)
51 return "%04d-%02d-%02dT%02d:%02d:%02d.%06dZ" % (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_usec)
54 def time_from_cstring(text):
55 """Parse a time from a cstring.
57 :param text: Parse text
60 (basestr, usecstr) = text.split(".", 1)
61 assert usecstr[-1] == "Z"
62 tm_usec = int(usecstr[:-1])
63 tm = time.strptime(basestr, "%Y-%m-%dT%H:%M:%S")
64 return (long(time.mktime((tm[0], tm[1], tm[2], tm[3], tm[4], tm[5], tm[6], tm[7], -1)) - time.timezone) * 1000000 + tm_usec)
67 def parse_externals_description(base_url, val):
68 """Parse an svn:externals property value.
70 :param base_url: URL on which the property is set. Used for
73 :returns: dictionary with local names as keys, (revnum, url)
74 as value. revnum is the revision number and is
75 set to None if not applicable.
80 for l in val.splitlines():
81 if l == "" or l[0] == "#":
83 pts = l.rsplit(None, 3)
85 if pts[0] == "-r": # -r X URL DIR
89 elif pts[1] == "-r": # DIR -r X URL
94 raise InvalidExternalsDescription()
96 if pts[1].startswith("-r"): # DIR -rX URL
97 revno = int(pts[1][2:])
100 elif pts[0].startswith("-r"): # -rX URL DIR
101 revno = int(pts[0][2:])
105 raise InvalidExternalsDescription()
107 if not is_url(pts[0]):
115 raise InvalidExternalsDescription()
116 if relurl.startswith("//"):
117 raise NotImplementedError("Relative to the scheme externals not yet supported")
118 if relurl.startswith("^/"):
119 raise NotImplementedError("Relative to the repository root externals not yet supported")
120 ret[path] = (revno, urlparse.urljoin(base_url+"/", relurl))
124 def parse_mergeinfo_property(text):
125 """Parse a mergeinfo property.
127 :param text: Property contents
130 for l in text.splitlines():
131 (path, ranges) = l.rsplit(":", 1)
132 assert path.startswith("/")
134 for range in ranges.split(","):
141 (start, end) = range.split("-", 1)
142 ret[path].append((int(start), int(end), inheritable))
144 ret[path].append((int(range), int(range), inheritable))
149 def generate_mergeinfo_property(merges):
150 """Generate the contents of the svn:mergeinfo property
152 :param merges: dictionary mapping paths to lists of ranges
153 :return: Property contents
155 def formatrange((start, end, inheritable)):
160 return "%d%s" % (start, suffix)
162 return "%d-%d%s" % (start, end, suffix)
164 for (path, ranges) in merges.iteritems():
165 assert path.startswith("/")
166 text += "%s:%s\n" % (path, ",".join(map(formatrange, ranges)))
170 def range_includes_revnum(ranges, revnum):
171 """Check if the specified range contains the mentioned revision number.
173 :param ranges: list of ranges
174 :param revnum: revision number
175 :return: Whether or not the revision number is included
177 i = bisect.bisect(ranges, (revnum, revnum, True))
180 (start, end, inheritable) = ranges[i-1]
181 return (start <= revnum <= end)
184 def range_add_revnum(ranges, revnum, inheritable=True):
185 """Add revision number to a list of ranges
187 :param ranges: List of ranges
188 :param revnum: Revision number to add
189 :param inheritable: TODO
190 :return: New list of ranges
192 # TODO: Deal with inheritable
193 item = (revnum, revnum, inheritable)
197 i = bisect.bisect(ranges, item)
199 (start, end, inh) = ranges[i-1]
200 if (start <= revnum <= end):
204 # Extend previous range
205 ranges[i-1] = (start, end+1, inh)
208 (start, end, inh) = ranges[i]
209 if start-1 == revnum:
211 ranges[i] = (start-1, end, inh)
213 ranges.insert(i, item)
217 def mergeinfo_includes_revision(merges, path, revnum):
218 """Check if the specified mergeinfo contains a path in revnum.
220 :param merges: Dictionary with merges
221 :param path: Merged path
222 :param revnum: Revision number
223 :return: Whether the revision is included
225 assert path.startswith("/")
227 ranges = merges[path]
231 return range_includes_revnum(ranges, revnum)
234 def mergeinfo_add_revision(mergeinfo, path, revnum):
235 """Add a revision to a mergeinfo dictionary
237 :param mergeinfo: Merginfo dictionary
238 :param path: Merged path to add
239 :param revnum: Merged revision to add
240 :return: Updated dictionary
242 assert path.startswith("/")
243 mergeinfo[path] = range_add_revnum(mergeinfo.get(path, []), revnum)
247 PROP_EXECUTABLE = 'svn:executable'
248 PROP_EXECUTABLE_VALUE = '*'
249 PROP_EXTERNALS = 'svn:externals'
250 PROP_IGNORE = 'svn:ignore'
251 PROP_KEYWORDS = 'svn:keywords'
252 PROP_MIME_TYPE = 'svn:mime-type'
253 PROP_MERGEINFO = 'svn:mergeinfo'
254 PROP_NEEDS_LOCK = 'svn:needs-lock'
255 PROP_NEEDS_LOCK_VALUE = '*'
257 PROP_SPECIAL = 'svn:special'
258 PROP_SPECIAL_VALUE = '*'
259 PROP_WC_PREFIX = 'svn:wc:'
260 PROP_ENTRY_PREFIX = 'svn:entry'
261 PROP_ENTRY_COMMITTED_DATE = 'svn:entry:committed-date'
262 PROP_ENTRY_COMMITTED_REV = 'svn:entry:committed-rev'
263 PROP_ENTRY_LAST_AUTHOR = 'svn:entry:last-author'
264 PROP_ENTRY_LOCK_TOKEN = 'svn:entry:lock-token'
265 PROP_ENTRY_UUID = 'svn:entry:uuid'
267 PROP_REVISION_LOG = "svn:log"
268 PROP_REVISION_AUTHOR = "svn:author"
269 PROP_REVISION_DATE = "svn:date"
271 def diff(current, previous):
272 """Find the differences between two property dictionaries.
274 :param current: Dictionary with current (new) properties
275 :param previous: Dictionary with previous (old) properties
276 :return: Dictionary that contains an entry for
277 each property that was changed. Value is a tuple
278 with the old and the new property value.
281 for key, newval in current.iteritems():
282 oldval = previous.get(key)
284 ret[key] = (oldval, newval)