Add some more documentation.
[jelmer/subvertpy.git] / subvertpy / properties.py
1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
2  
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.
7
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.
12
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/>.
15
16 """Handling of Subversion properties."""
17
18 __author__ = "Jelmer Vernooij <jelmer@samba.org>"
19 __docformat__ = "restructuredText"
20
21 import bisect, time, urlparse
22
23
24 class InvalidExternalsDescription(Exception):
25     _fmt = """Unable to parse externals description."""
26
27
28 def is_valid_property_name(prop):
29     """Check the validity of a property name.
30
31     :param prop: Property name
32     :return: Whether prop is a valid property name
33     """
34     if not prop[0].isalnum() and not prop[0] in ":_":
35         return False
36     for c in prop[1:]:
37         if not c.isalnum() and not c in "-:._":
38             return False
39     return True
40
41
42 def time_to_cstring(timestamp):
43     """Determine string representation of a time.
44
45     :param timestamp: Timestamp
46     :return: string with date
47     """
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)
52
53
54 def time_from_cstring(text):
55     """Parse a time from a cstring.
56     
57     :param text: Parse text
58     :return: timestamp
59     """
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)
65
66
67 def parse_externals_description(base_url, val):
68     """Parse an svn:externals property value.
69
70     :param base_url: URL on which the property is set. Used for 
71         relative externals.
72
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.
76     """
77     def is_url(u):
78         return ("://" in u)
79     ret = {}
80     for l in val.splitlines():
81         if l == "" or l[0] == "#":
82             continue
83         pts = l.rsplit(None, 3) 
84         if len(pts) == 4:
85             if pts[0] == "-r": # -r X URL DIR
86                 revno = int(pts[1])
87                 path = pts[3]
88                 relurl = pts[2]
89             elif pts[1] == "-r": # DIR -r X URL
90                 revno = int(pts[2])
91                 path = pts[0]
92                 relurl = pts[3]
93             else:
94                 raise InvalidExternalsDescription()
95         elif len(pts) == 3:
96             if pts[1].startswith("-r"): # DIR -rX URL
97                 revno = int(pts[1][2:])
98                 path = pts[0]
99                 relurl = pts[2]
100             elif pts[0].startswith("-r"): # -rX URL DIR
101                 revno = int(pts[0][2:])
102                 path = pts[2]
103                 relurl = pts[1]
104             else:
105                 raise InvalidExternalsDescription()
106         elif len(pts) == 2:
107             if not is_url(pts[0]):
108                 relurl = pts[1]
109                 path = pts[0]
110             else:
111                 relurl = pts[0]
112                 path = pts[1]
113             revno = None
114         else:
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))
121     return ret
122
123
124 def parse_mergeinfo_property(text):
125     """Parse a mergeinfo property.
126
127     :param text: Property contents
128     """
129     ret = {}
130     for l in text.splitlines():
131         (path, ranges) = l.rsplit(":", 1)
132         assert path.startswith("/")
133         ret[path] = []
134         for range in ranges.split(","):
135             if range[-1] == "*":
136                 inheritable = False
137                 range = range[:-1]
138             else:
139                 inheritable = True
140             try:
141                 (start, end) = range.split("-", 1)
142                 ret[path].append((int(start), int(end), inheritable))
143             except ValueError:
144                 ret[path].append((int(range), int(range), inheritable))
145
146     return ret
147
148
149 def generate_mergeinfo_property(merges):
150     """Generate the contents of the svn:mergeinfo property
151
152     :param merges: dictionary mapping paths to lists of ranges
153     :return: Property contents
154     """
155     def formatrange((start, end, inheritable)):
156         suffix = ""
157         if not inheritable:
158             suffix = "*"
159         if start == end:
160             return "%d%s" % (start, suffix)
161         else:
162             return "%d-%d%s" % (start, end, suffix)
163     text = ""
164     for (path, ranges) in merges.iteritems():
165         assert path.startswith("/")
166         text += "%s:%s\n" % (path, ",".join(map(formatrange, ranges)))
167     return text
168
169
170 def range_includes_revnum(ranges, revnum):
171     """Check if the specified range contains the mentioned revision number.
172
173     :param ranges: list of ranges
174     :param revnum: revision number
175     :return: Whether or not the revision number is included
176     """
177     i = bisect.bisect(ranges, (revnum, revnum, True))
178     if i == 0:
179         return False
180     (start, end, inheritable) = ranges[i-1]
181     return (start <= revnum <= end)
182
183
184 def range_add_revnum(ranges, revnum, inheritable=True):
185     """Add revision number to a list of ranges
186
187     :param ranges: List of ranges
188     :param revnum: Revision number to add
189     :param inheritable: TODO
190     :return: New list of ranges
191     """
192     # TODO: Deal with inheritable
193     item = (revnum, revnum, inheritable)
194     if len(ranges) == 0:
195         ranges.append(item)
196         return ranges
197     i = bisect.bisect(ranges, item)
198     if i > 0:
199         (start, end, inh) = ranges[i-1]
200         if (start <= revnum <= end):
201             # already there
202             return ranges
203         if end == revnum-1:
204             # Extend previous range
205             ranges[i-1] = (start, end+1, inh)
206             return ranges
207     if i < len(ranges):
208         (start, end, inh) = ranges[i]
209         if start-1 == revnum:
210             # Extend next range
211             ranges[i] = (start-1, end, inh)
212             return ranges
213     ranges.insert(i, item)
214     return ranges
215
216
217 def mergeinfo_includes_revision(merges, path, revnum):
218     """Check if the specified mergeinfo contains a path in revnum.
219
220     :param merges: Dictionary with merges
221     :param path: Merged path
222     :param revnum: Revision number
223     :return: Whether the revision is included
224     """
225     assert path.startswith("/")
226     try:
227         ranges = merges[path]
228     except KeyError:
229         return False
230
231     return range_includes_revnum(ranges, revnum)
232
233
234 def mergeinfo_add_revision(mergeinfo, path, revnum):
235     """Add a revision to a mergeinfo dictionary
236
237     :param mergeinfo: Merginfo dictionary
238     :param path: Merged path to add
239     :param revnum: Merged revision to add
240     :return: Updated dictionary
241     """
242     assert path.startswith("/")
243     mergeinfo[path] = range_add_revnum(mergeinfo.get(path, []), revnum)
244     return mergeinfo
245
246
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 = '*'
256 PROP_PREFIX = 'svn:'
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'
266
267 PROP_REVISION_LOG = "svn:log"
268 PROP_REVISION_AUTHOR = "svn:author"
269 PROP_REVISION_DATE = "svn:date"
270
271 def diff(current, previous):
272     """Find the differences between two property dictionaries.
273
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.
279     """
280     ret = {}
281     for key, newval in current.iteritems():
282         oldval = previous.get(key)
283         if oldval != newval:
284             ret[key] = (oldval, newval)
285     return ret