Follow-up to r29036: Now that the "mergeinfo" transaction file is no
[svn.git] / tools / server-side / svn_dav_log_parse.py
blobbec6ace9c2c5ec79dd447b65eb1bad735531873e
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 commit r<N>
48 list-dir <PATH> r<N>
49 lock <PATH> steal?
50 prop-list <PATH>@<N>
51 revprop-change r<N> <REVPROP>
52 revprop-list r<N>
53 unlock <PATH> break?
55 Reports::
57 blame <PATH> r<N>:<M> include-merged-revisions?
58 get-mergeinfo (<PATH> ...) <I>
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-or-merge <FROM-PATH>@<N> <TO-PATH>@<M> depth=<D>? ignore-ancestry?
66 diff-or-merge <PATH> r<N>:<M> depth=<D>? ignore-ancestry?
67 remote-status <PATH> r<N> depth=<D>?
68 switch <FROM-PATH>@<N> <TO-PATH>@<M> 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)
118 # Helper functions
121 # TODO: Move to kitchensink.c like svn_depth_from_word?
122 try:
123 from svn.core import svn_inheritance_from_word
124 except ImportError:
125 def svn_inheritance_from_word(word):
126 try:
127 return INHERITANCE_WORDS[word]
128 except KeyError:
129 # XXX svn_inheritance_to_word uses explicit as default so...
130 return svn.core.svn_mergeinfo_explicit
132 def _parse_depth(word):
133 if word is None:
134 return svn.core.svn_depth_unknown
135 if word not in DEPTH_WORDS:
136 raise BadDepthError(word)
137 return svn.core.svn_depth_from_word(word)
139 def _parse_mergeinfo_inheritance(word):
140 if word not in INHERITANCE_WORDS:
141 raise BadMergeinfoInheritanceError(word)
142 return svn_inheritance_from_word(word)
144 def _match(line, *patterns):
145 """Return a re.match object from matching patterns against line.
147 All optional arguments must be strings suitable for ''.join()ing
148 into a single pattern string for re.match. The last optional
149 argument may instead be a list of such strings, which will be
150 joined into the final pattern as *optional* matches.
152 Raises:
153 Error -- if re.match returns None (i.e. no match)
155 if isinstance(patterns[-1], list):
156 optional = patterns[-1]
157 patterns = patterns[:-1]
158 else:
159 optional = []
160 pattern = r'\s+'.join(patterns)
161 pattern += ''.join([r'(\s+' + x + ')?' for x in optional])
162 m = re.match(pattern, line)
163 if m is None:
164 raise Error
165 return m
168 class Parser(object):
169 """Subclass this and define the handle_ methods according to the
170 "SVN-ACTION strings" section of this module's documentation. For
171 example, "lock <PATH> steal?" => def handle_lock(self, path, steal)
172 where steal will be True if "steal" was present.
174 See the end of test_svn_dav_log_parse.py for a complete example.
176 def parse(self, line):
177 """Parse line and call appropriate handle_ method.
179 Returns one of:
180 - line remaining after the svn action, if one was parsed
181 - whatever your handle_unknown implementation returns
183 Raises:
184 BadDepthError -- for bad svn_depth_t values
185 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
186 values
187 Error -- any other parse error
189 self.line = line
190 words = self.split_line = line.split(' ')
191 try:
192 method = getattr(self, '_parse_' + words[0].replace('-', '_'))
193 except AttributeError:
194 return self.handle_unknown(self.line)
195 return method(' '.join(words[1:]))
197 def _parse_commit(self, line):
198 m = _match(line, pREVNUM)
199 self.handle_commit(int(m.group(1)))
200 return line[m.end():]
202 def _parse_list_dir(self, line):
203 m = _match(line, pPATH, pREVNUM)
204 self.handle_list_dir(m.group(1), int(m.group(2)))
205 return line[m.end():]
207 def _parse_lock(self, line):
208 m = _match(line, pPATH, ['steal'])
209 self.handle_lock(m.group(1), m.group(2) is not None)
210 return line[m.end():]
212 def _parse_prop_list(self, line):
213 m = _match(line, pPATHREV)
214 self.handle_prop_list(m.group(1), int(m.group(2)))
215 return line[m.end():]
217 def _parse_revprop_change(self, line):
218 # <REVPROP>
219 pPROPERTY = pWORD
220 m = _match(line, pREVNUM, pPROPERTY)
221 self.handle_revprop_change(int(m.group(1)), m.group(2))
222 return line[m.end():]
224 def _parse_revprop_list(self, line):
225 m = _match(line, pREVNUM)
226 self.handle_revprop_list(int(m.group(1)))
227 return line[m.end():]
229 def _parse_unlock(self, line):
230 m = _match(line, pPATH, ['break'])
231 self.handle_unlock(m.group(1), m.group(2) is not None)
232 return line[m.end():]
234 # reports
236 def _parse_blame(self, line):
237 m = _match(line, pPATH, pREVRANGE, ['include-merged-revisions'])
238 path = m.group(1)
239 left = int(m.group(2))
240 right = int(m.group(3))
241 include_merged_revisions = m.group(4) is not None
242 self.handle_blame(path, left, right, include_merged_revisions)
243 return line[m.end():]
245 def _parse_get_mergeinfo(self, line):
246 # <I>
247 pMERGEINFO_INHERITANCE = pWORD
248 m = _match(line, pPATHS, pMERGEINFO_INHERITANCE)
249 paths = m.group(1).split()
250 inheritance = _parse_mergeinfo_inheritance(m.group(2))
251 self.handle_get_mergeinfo(paths, inheritance)
252 return line[m.end():]
254 def _parse_log(self, line):
255 # limit=<N>?
256 pLIMIT = r'limit=(\d+)'
257 # revprops=all|(<REVPROP> ...)?
258 pREVPROPS = r'revprops=(all|\(([^)]+)\))'
259 m = _match(line, pPATHS, pREVRANGE,
260 [pLIMIT, 'discover-changed-paths', 'strict',
261 'include-merged-revisions', pREVPROPS])
262 paths = m.group(1).split()
263 left = int(m.group(2))
264 right = int(m.group(3))
265 if m.group(5) is None:
266 limit = 0
267 else:
268 limit = int(m.group(5))
269 discover_changed_paths = m.group(6) is not None
270 strict = m.group(7) is not None
271 include_merged_revisions = m.group(8) is not None
272 if m.group(10) == 'all':
273 revprops = None
274 else:
275 if m.group(11) is None:
276 revprops = []
277 else:
278 revprops = m.group(11).split()
279 self.handle_log(paths, left, right, limit, discover_changed_paths,
280 strict, include_merged_revisions, revprops)
281 return line[m.end():]
283 def _parse_replay(self, line):
284 m = _match(line, pPATH, pREVNUM)
285 path = m.group(1)
286 revision = int(m.group(2))
287 self.handle_replay(path, revision)
288 return line[m.end():]
290 # the update report
292 def _parse_checkout_or_export(self, line):
293 m = _match(line, pPATH, pREVNUM, [pDEPTH])
294 path = m.group(1)
295 revision = int(m.group(2))
296 depth = _parse_depth(m.group(4))
297 self.handle_checkout_or_export(path, revision, depth)
298 return line[m.end():]
300 def _parse_diff_or_merge(self, line):
301 # First, try 1-path form.
302 try:
303 m = _match(line, pPATH, pREVRANGE, [pDEPTH, 'ignore-ancestry'])
304 f = self._parse_diff_or_merge_1path
305 except Error:
306 # OK, how about 2-path form?
307 m = _match(line, pPATHREV, pPATHREV, [pDEPTH, 'ignore-ancestry'])
308 f = self._parse_diff_or_merge_2paths
309 return f(line, m)
311 def _parse_diff_or_merge_1path(self, line, m):
312 path = m.group(1)
313 left = int(m.group(2))
314 right = int(m.group(3))
315 depth = _parse_depth(m.group(5))
316 ignore_ancestry = m.group(6) is not None
317 self.handle_diff_or_merge_1path(path, left, right,
318 depth, ignore_ancestry)
319 return line[m.end():]
321 def _parse_diff_or_merge_2paths(self, line, m):
322 from_path = m.group(1)
323 from_rev = int(m.group(2))
324 to_path = m.group(3)
325 to_rev = int(m.group(4))
326 depth = _parse_depth(m.group(6))
327 ignore_ancestry = m.group(7) is not None
328 self.handle_diff_or_merge_2paths(from_path, from_rev, to_path, to_rev,
329 depth, ignore_ancestry)
330 return line[m.end():]
332 def _parse_remote_status(self, line):
333 m = _match(line, pPATH, pREVNUM, [pDEPTH])
334 path = m.group(1)
335 revision = int(m.group(2))
336 depth = _parse_depth(m.group(4))
337 self.handle_remote_status(path, revision, depth)
338 return line[m.end():]
340 def _parse_switch(self, line):
341 m = _match(line, pPATHREV, pPATHREV, [pDEPTH])
342 from_path = m.group(1)
343 from_rev = int(m.group(2))
344 to_path = m.group(3)
345 to_rev = int(m.group(4))
346 depth = _parse_depth(m.group(6))
347 self.handle_switch(from_path, from_rev, to_path, to_rev, depth)
348 return line[m.end():]
350 def _parse_update(self, line):
351 m = _match(line, pPATH, pREVNUM, [pDEPTH, 'send-copyfrom-args'])
352 path = m.group(1)
353 revision = int(m.group(2))
354 depth = _parse_depth(m.group(4))
355 send_copyfrom_args = m.group(5) is not None
356 self.handle_update(path, revision, depth, send_copyfrom_args)
357 return line[m.end():]