Fix pool usage in mergeinfo elision code; the current code was leading
[svn.git] / tools / server-side / svn_dav_log_parse.py
blob2b2a3e35584a31b50672bcf04f6bed701d971ac7
1 #!/usr/bin/python
3 # ====================================================================
4 # Copyright (c) 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://subversion.tigris.org/.
15 # ====================================================================
17 """Parse mod-dav-svn operational logs.
19 SVN-ACTION strings
20 ------------------
22 Angle brackets denote a variable, e.g. 'commit r<N>' means you'll see
23 lines like 'commit r17' for this action.
25 <N> and <M> are revision numbers.
27 <PATH>, <FROM-PATH>, and <TO-PATH> mean a URI-encoded path relative to
28 the repository root, including a leading '/'.
30 <REVPROP> means a revision property, e.g. 'svn:log'.
32 <I> represents a svn_mergeinfo_inheritance_t value and is one of these
33 words: explicit inherited nearest-ancestor.
35 <D> represents a svn_depth_t value and is one of these words: empty
36 files immediates infinity. If the depth value for the operation was
37 svn_depth_unknown, the depth= portion is absent entirely.
39 The get-mergeinfo and log actions use lists for paths and revprops.
40 The lists are enclosed in parentheses and each item is separated by a
41 space (spaces in paths are encoded as %20).
43 The words will *always* be in this order, though some may be absent.
45 General::
47 change-rev-prop r<N> <REVPROP>
48 commit r<N>
49 get-dir <PATH> r<N> text? props?
50 get-file <PATH> r<N> text? props?
51 lock (<PATH> ...) steal?
52 rev-proplist r<N>
53 unlock (<PATH> ...) break?
55 Reports::
57 get-file-revs <PATH> r<N>:<M> include-merged-revisions?
58 get-mergeinfo (<PATH> ...) <I> include-descendants?
59 log (<PATH> ...) r<N>:<M> limit=<N>? discover-changed-paths? strict? include-merged-revisions? revprops=all|(<REVPROP> ...)?
60 replay <PATH> r<N>
62 The update report::
64 checkout-or-export <PATH> r<N> depth=<D>?
65 diff <FROM-PATH>@<N> <TO-PATH>@<M> depth=<D>? ignore-ancestry?
66 diff <PATH> r<N>:<M> depth=<D>? ignore-ancestry?
67 status <PATH> r<N> depth=<D>?
68 switch <FROM-PATH> <TO-PATH>@<N> depth=<D>?
69 update <PATH> r<N> depth=<D>? send-copyfrom-args?
70 """
73 import re
74 import svn.core
77 # Valid words for _parse_depth and _parse_mergeinfo_inheritance
80 DEPTH_WORDS = ['empty', 'files', 'immediates', 'infinity']
81 INHERITANCE_WORDS = {
82 'explicit': svn.core.svn_mergeinfo_explicit,
83 'inherited': svn.core.svn_mergeinfo_inherited,
84 'nearest-ancestor': svn.core.svn_mergeinfo_nearest_ancestor,
88 # Patterns for _match
91 # <PATH>
92 pPATH = r'(/\S*)'
93 # (<PATH> ...)
94 pPATHS = r'\(([^)]*)\)'
95 # r<N>
96 pREVNUM = r'r(\d+)'
97 # r<N>:<M>
98 pREVRANGE = r'r(\d+):(\d+)'
99 # <PATH>@<N>
100 pPATHREV = pPATH + r'@(\d+)'
101 pWORD = r'(\S+)'
102 # depth=<D>?
103 pDEPTH = 'depth=' + pWORD
106 # Exceptions
109 class Error(Exception): pass
110 class BadDepthError(Error):
111 def __init__(self, value):
112 Error.__init__(self, 'bad svn_depth_t value ' + value)
113 class BadMergeinfoInheritanceError(Error):
114 def __init__(self, value):
115 Error.__init__(self, 'bad svn_mergeinfo_inheritance_t value ' + value)
116 class MatchError(Error):
117 def __init__(self, pattern, line):
118 Error.__init__(self, '/%s/ does not match log line:\n%s'
119 % (pattern, line))
123 # Helper functions
126 # TODO: Move to kitchensink.c like svn_depth_from_word?
127 try:
128 from svn.core import svn_inheritance_from_word
129 except ImportError:
130 def svn_inheritance_from_word(word):
131 try:
132 return INHERITANCE_WORDS[word]
133 except KeyError:
134 # XXX svn_inheritance_to_word uses explicit as default so...
135 return svn.core.svn_mergeinfo_explicit
137 def _parse_depth(word):
138 if word is None:
139 return svn.core.svn_depth_unknown
140 if word not in DEPTH_WORDS:
141 raise BadDepthError(word)
142 return svn.core.svn_depth_from_word(word)
144 def _parse_mergeinfo_inheritance(word):
145 if word not in INHERITANCE_WORDS:
146 raise BadMergeinfoInheritanceError(word)
147 return svn_inheritance_from_word(word)
149 def _match(line, *patterns):
150 """Return a re.match object from matching patterns against line.
152 All optional arguments must be strings suitable for ''.join()ing
153 into a single pattern string for re.match. The last optional
154 argument may instead be a list of such strings, which will be
155 joined into the final pattern as *optional* matches.
157 Raises:
158 Error -- if re.match returns None (i.e. no match)
160 if isinstance(patterns[-1], list):
161 optional = patterns[-1]
162 patterns = patterns[:-1]
163 else:
164 optional = []
165 pattern = r'\s+'.join(patterns)
166 pattern += ''.join([r'(\s+' + x + ')?' for x in optional])
167 m = re.match(pattern, line)
168 if m is None:
169 raise MatchError(pattern, line)
170 return m
173 class Parser(object):
174 """Subclass this and define the handle_ methods according to the
175 "SVN-ACTION strings" section of this module's documentation. For
176 example, "lock <PATH> steal?" => def handle_lock(self, path, steal)
177 where steal will be True if "steal" was present.
179 See the end of test_svn_dav_log_parse.py for a complete example.
181 def parse(self, line):
182 """Parse line and call appropriate handle_ method.
184 Returns one of:
185 - line remaining after the svn action, if one was parsed
186 - whatever your handle_unknown implementation returns
188 Raises:
189 BadDepthError -- for bad svn_depth_t values
190 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
191 values
192 Error -- any other parse error
194 self.line = line
195 words = self.split_line = line.split(' ')
196 try:
197 method = getattr(self, '_parse_' + words[0].replace('-', '_'))
198 except AttributeError:
199 return self.handle_unknown(self.line)
200 return method(' '.join(words[1:]))
202 def _parse_commit(self, line):
203 m = _match(line, pREVNUM)
204 self.handle_commit(int(m.group(1)))
205 return line[m.end():]
207 def _parse_get_dir(self, line):
208 m = _match(line, pPATH, pREVNUM, ['text', 'props'])
209 self.handle_get_dir(m.group(1), int(m.group(2)),
210 m.group(3) is not None,
211 m.group(4) is not None)
212 return line[m.end():]
214 def _parse_get_file(self, line):
215 m = _match(line, pPATH, pREVNUM, ['text', 'props'])
216 self.handle_get_file(m.group(1), int(m.group(2)),
217 m.group(3) is not None,
218 m.group(4) is not None)
219 return line[m.end():]
221 def _parse_lock(self, line):
222 m = _match(line, pPATHS, ['steal'])
223 paths = m.group(1).split()
224 self.handle_lock(paths, m.group(2) is not None)
225 return line[m.end():]
227 def _parse_change_rev_prop(self, line):
228 # <REVPROP>
229 pPROPERTY = pWORD
230 m = _match(line, pREVNUM, pPROPERTY)
231 self.handle_change_rev_prop(int(m.group(1)), m.group(2))
232 return line[m.end():]
234 def _parse_rev_proplist(self, line):
235 m = _match(line, pREVNUM)
236 self.handle_rev_proplist(int(m.group(1)))
237 return line[m.end():]
239 def _parse_unlock(self, line):
240 m = _match(line, pPATHS, ['break'])
241 paths = m.group(1).split()
242 self.handle_unlock(paths, m.group(2) is not None)
243 return line[m.end():]
245 # reports
247 def _parse_get_file_revs(self, line):
248 m = _match(line, pPATH, pREVRANGE, ['include-merged-revisions'])
249 path = m.group(1)
250 left = int(m.group(2))
251 right = int(m.group(3))
252 include_merged_revisions = m.group(4) is not None
253 self.handle_get_file_revs(path, left, right, include_merged_revisions)
254 return line[m.end():]
256 def _parse_get_mergeinfo(self, line):
257 # <I>
258 pMERGEINFO_INHERITANCE = pWORD
259 pINCLUDE_DESCENDANTS = pWORD
260 m = _match(line,
261 pPATHS, pMERGEINFO_INHERITANCE, ['include-descendants'])
262 paths = m.group(1).split()
263 inheritance = _parse_mergeinfo_inheritance(m.group(2))
264 include_descendants = m.group(3) is not None
265 self.handle_get_mergeinfo(paths, inheritance, include_descendants)
266 return line[m.end():]
268 def _parse_log(self, line):
269 # limit=<N>?
270 pLIMIT = r'limit=(\d+)'
271 # revprops=all|(<REVPROP> ...)?
272 pREVPROPS = r'revprops=(all|\(([^)]+)\))'
273 m = _match(line, pPATHS, pREVRANGE,
274 [pLIMIT, 'discover-changed-paths', 'strict',
275 'include-merged-revisions', pREVPROPS])
276 paths = m.group(1).split()
277 left = int(m.group(2))
278 right = int(m.group(3))
279 if m.group(5) is None:
280 limit = 0
281 else:
282 limit = int(m.group(5))
283 discover_changed_paths = m.group(6) is not None
284 strict = m.group(7) is not None
285 include_merged_revisions = m.group(8) is not None
286 if m.group(10) == 'all':
287 revprops = None
288 else:
289 if m.group(11) is None:
290 revprops = []
291 else:
292 revprops = m.group(11).split()
293 self.handle_log(paths, left, right, limit, discover_changed_paths,
294 strict, include_merged_revisions, revprops)
295 return line[m.end():]
297 def _parse_replay(self, line):
298 m = _match(line, pPATH, pREVNUM)
299 path = m.group(1)
300 revision = int(m.group(2))
301 self.handle_replay(path, revision)
302 return line[m.end():]
304 # the update report
306 def _parse_checkout_or_export(self, line):
307 m = _match(line, pPATH, pREVNUM, [pDEPTH])
308 path = m.group(1)
309 revision = int(m.group(2))
310 depth = _parse_depth(m.group(4))
311 self.handle_checkout_or_export(path, revision, depth)
312 return line[m.end():]
314 def _parse_diff(self, line):
315 # First, try 1-path form.
316 try:
317 m = _match(line, pPATH, pREVRANGE, [pDEPTH, 'ignore-ancestry'])
318 f = self._parse_diff_1path
319 except Error:
320 # OK, how about 2-path form?
321 m = _match(line, pPATHREV, pPATHREV, [pDEPTH, 'ignore-ancestry'])
322 f = self._parse_diff_2paths
323 return f(line, m)
325 def _parse_diff_1path(self, line, m):
326 path = m.group(1)
327 left = int(m.group(2))
328 right = int(m.group(3))
329 depth = _parse_depth(m.group(5))
330 ignore_ancestry = m.group(6) is not None
331 self.handle_diff_1path(path, left, right,
332 depth, ignore_ancestry)
333 return line[m.end():]
335 def _parse_diff_2paths(self, line, m):
336 from_path = m.group(1)
337 from_rev = int(m.group(2))
338 to_path = m.group(3)
339 to_rev = int(m.group(4))
340 depth = _parse_depth(m.group(6))
341 ignore_ancestry = m.group(7) is not None
342 self.handle_diff_2paths(from_path, from_rev, to_path, to_rev,
343 depth, ignore_ancestry)
344 return line[m.end():]
346 def _parse_status(self, line):
347 m = _match(line, pPATH, pREVNUM, [pDEPTH])
348 path = m.group(1)
349 revision = int(m.group(2))
350 depth = _parse_depth(m.group(4))
351 self.handle_status(path, revision, depth)
352 return line[m.end():]
354 def _parse_switch(self, line):
355 m = _match(line, pPATH, pPATHREV, [pDEPTH])
356 from_path = m.group(1)
357 to_path = m.group(2)
358 to_rev = int(m.group(3))
359 depth = _parse_depth(m.group(5))
360 self.handle_switch(from_path, to_path, to_rev, depth)
361 return line[m.end():]
363 def _parse_update(self, line):
364 m = _match(line, pPATH, pREVNUM, [pDEPTH, 'send-copyfrom-args'])
365 path = m.group(1)
366 revision = int(m.group(2))
367 depth = _parse_depth(m.group(4))
368 send_copyfrom_args = m.group(5) is not None
369 self.handle_update(path, revision, depth, send_copyfrom_args)
370 return line[m.end():]