* tools/server-side/svn_server_log_parse.py
[svn.git] / tools / server-side / svn_server_log_parse.py
blob87cd142c24474115aa51086aa1d0bf0803bb3177
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 # TODO: Decode URI-encoded log items before calling the callback.
19 """Parse mod-dav-svn operational logs.
21 SVN-ACTION strings
22 ------------------
24 Angle brackets denote a variable, e.g. 'commit r<N>' means you'll see
25 lines like 'commit r17' for this action.
27 <N> and <M> are revision numbers.
29 <PATH>, <FROM-PATH>, and <TO-PATH> mean a URI-encoded path relative to
30 the repository root, including a leading '/'.
32 <REVPROP> means a revision property, e.g. 'svn:log'.
34 <I> represents a svn_mergeinfo_inheritance_t value and is one of these
35 words: explicit inherited nearest-ancestor.
37 <D> represents a svn_depth_t value and is one of these words: empty
38 files immediates infinity. If the depth value for the operation was
39 svn_depth_unknown, the depth= portion is absent entirely.
41 The get-mergeinfo and log actions use lists for paths and revprops.
42 The lists are enclosed in parentheses and each item is separated by a
43 space (spaces in paths are encoded as %20).
45 The words will *always* be in this order, though some may be absent.
47 General::
49 change-rev-prop r<N> <REVPROP>
50 commit r<N>
51 get-dir <PATH> r<N> text? props?
52 get-file <PATH> r<N> text? props?
53 lock (<PATH> ...) steal?
54 rev-proplist r<N>
55 unlock (<PATH> ...) break?
57 Reports::
59 get-file-revs <PATH> r<N>:<M> include-merged-revisions?
60 get-mergeinfo (<PATH> ...) <I> include-descendants?
61 log (<PATH> ...) r<N>:<M> limit=<N>? discover-changed-paths? strict? include-merged-revisions? revprops=all|(<REVPROP> ...)?
62 replay <PATH> r<N>
64 The update report::
66 checkout-or-export <PATH> r<N> depth=<D>?
67 diff <FROM-PATH>@<N> <TO-PATH>@<M> depth=<D>? ignore-ancestry?
68 diff <PATH> r<N>:<M> depth=<D>? ignore-ancestry?
69 status <PATH> r<N> depth=<D>?
70 switch <FROM-PATH> <TO-PATH>@<N> depth=<D>?
71 update <PATH> r<N> depth=<D>? send-copyfrom-args?
72 """
75 import re
76 import svn.core
79 # Valid words for _parse_depth and _parse_mergeinfo_inheritance
82 DEPTH_WORDS = ['empty', 'files', 'immediates', 'infinity']
83 INHERITANCE_WORDS = {
84 'explicit': svn.core.svn_mergeinfo_explicit,
85 'inherited': svn.core.svn_mergeinfo_inherited,
86 'nearest-ancestor': svn.core.svn_mergeinfo_nearest_ancestor,
90 # Patterns for _match
93 # <PATH>
94 pPATH = r'(/\S*)'
95 # (<PATH> ...)
96 pPATHS = r'\(([^)]*)\)'
97 # r<N>
98 pREVNUM = r'r(\d+)'
99 # (<N> ...)
100 pREVNUMS = r'\(((\d+\s*)*)\)'
101 # r<N>:<M>
102 pREVRANGE = r'r(-?\d+):(-?\d+)'
103 # <PATH>@<N>
104 pPATHREV = pPATH + r'@(\d+)'
105 pWORD = r'(\S+)'
106 pPROPERTY = pWORD
107 # depth=<D>?
108 pDEPTH = 'depth=' + pWORD
111 # Exceptions
114 class Error(Exception): pass
115 class BadDepthError(Error):
116 def __init__(self, value):
117 Error.__init__(self, 'bad svn_depth_t value ' + value)
118 class BadMergeinfoInheritanceError(Error):
119 def __init__(self, value):
120 Error.__init__(self, 'bad svn_mergeinfo_inheritance_t value ' + value)
121 class MatchError(Error):
122 def __init__(self, pattern, line):
123 Error.__init__(self, '/%s/ does not match log line:\n%s'
124 % (pattern, line))
128 # Helper functions
131 # TODO: Move to kitchensink.c like svn_depth_from_word?
132 try:
133 from svn.core import svn_inheritance_from_word
134 except ImportError:
135 def svn_inheritance_from_word(word):
136 try:
137 return INHERITANCE_WORDS[word]
138 except KeyError:
139 # XXX svn_inheritance_to_word uses explicit as default so...
140 return svn.core.svn_mergeinfo_explicit
142 def _parse_depth(word):
143 if word is None:
144 return svn.core.svn_depth_unknown
145 if word not in DEPTH_WORDS:
146 raise BadDepthError(word)
147 return svn.core.svn_depth_from_word(word)
149 def _parse_mergeinfo_inheritance(word):
150 if word not in INHERITANCE_WORDS:
151 raise BadMergeinfoInheritanceError(word)
152 return svn_inheritance_from_word(word)
154 def _match(line, *patterns):
155 """Return a re.match object from matching patterns against line.
157 All optional arguments must be strings suitable for ''.join()ing
158 into a single pattern string for re.match. The last optional
159 argument may instead be a list of such strings, which will be
160 joined into the final pattern as *optional* matches.
162 Raises:
163 Error -- if re.match returns None (i.e. no match)
165 if isinstance(patterns[-1], list):
166 optional = patterns[-1]
167 patterns = patterns[:-1]
168 else:
169 optional = []
170 pattern = r'\s+'.join(patterns)
171 pattern += ''.join([r'(\s+' + x + ')?' for x in optional])
172 m = re.match(pattern, line)
173 if m is None:
174 raise MatchError(pattern, line)
175 return m
178 class Parser(object):
179 """Subclass this and define the handle_ methods according to the
180 "SVN-ACTION strings" section of this module's documentation. For
181 example, "lock <PATH> steal?" => def handle_lock(self, path, steal)
182 where steal will be True if "steal" was present.
184 See the end of test_svn_server_log_parse.py for a complete example.
186 def parse(self, line):
187 """Parse line and call appropriate handle_ method.
189 Returns one of:
190 - line remaining after the svn action, if one was parsed
191 - whatever your handle_unknown implementation returns
193 Raises:
194 BadDepthError -- for bad svn_depth_t values
195 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
196 values
197 Error -- any other parse error
199 self.line = line
200 words = self.split_line = line.split(' ')
201 try:
202 method = getattr(self, '_parse_' + words[0].replace('-', '_'))
203 except AttributeError:
204 return self.handle_unknown(self.line)
205 return method(' '.join(words[1:]))
207 def _parse_commit(self, line):
208 m = _match(line, pREVNUM)
209 self.handle_commit(int(m.group(1)))
210 return line[m.end():]
212 def _parse_reparent(self, line):
213 m = _match(line, pPATH)
214 self.handle_reparent(m.group(1))
215 return line[m.end():]
217 def _parse_get_latest_rev(self, line):
218 self.handle_get_latest_rev()
219 return line
221 def _parse_get_dated_rev(self, line):
222 m = _match(line, pWORD)
223 self.handle_get_dated_rev(m.group(1))
224 return line[m.end():]
226 def _parse_get_dir(self, line):
227 m = _match(line, pPATH, pREVNUM, ['text', 'props'])
228 self.handle_get_dir(m.group(1), int(m.group(2)),
229 m.group(3) is not None,
230 m.group(4) is not None)
231 return line[m.end():]
233 def _parse_get_file(self, line):
234 m = _match(line, pPATH, pREVNUM, ['text', 'props'])
235 self.handle_get_file(m.group(1), int(m.group(2)),
236 m.group(3) is not None,
237 m.group(4) is not None)
238 return line[m.end():]
240 def _parse_lock(self, line):
241 m = _match(line, pPATHS, ['steal'])
242 paths = m.group(1).split()
243 self.handle_lock(paths, m.group(2) is not None)
244 return line[m.end():]
246 def _parse_change_rev_prop(self, line):
247 m = _match(line, pREVNUM, pPROPERTY)
248 self.handle_change_rev_prop(int(m.group(1)), m.group(2))
249 return line[m.end():]
251 def _parse_rev_proplist(self, line):
252 m = _match(line, pREVNUM)
253 self.handle_rev_proplist(int(m.group(1)))
254 return line[m.end():]
256 def _parse_rev_prop(self, line):
257 m = _match(line, pREVNUM, pPROPERTY)
258 self.handle_rev_prop(int(m.group(1)), m.group(2))
259 return line[m.end():]
261 def _parse_unlock(self, line):
262 m = _match(line, pPATHS, ['break'])
263 paths = m.group(1).split()
264 self.handle_unlock(paths, m.group(2) is not None)
265 return line[m.end():]
267 def _parse_get_lock(self, line):
268 m = _match(line, pPATH)
269 self.handle_get_lock(m.group(1))
270 return line[m.end():]
272 def _parse_get_locks(self, line):
273 m = _match(line, pPATH)
274 self.handle_get_locks(m.group(1))
275 return line[m.end():]
277 def _parse_get_locations(self, line):
278 m = _match(line, pPATH, pREVNUMS)
279 path = m.group(1)
280 revnums = [int(x) for x in m.group(2).split()]
281 self.handle_get_locations(path, revnums)
282 return line[m.end():]
284 def _parse_get_location_segments(self, line):
285 m = _match(line, pPATHREV, pREVRANGE)
286 path = m.group(1)
287 peg = int(m.group(2))
288 left = int(m.group(3))
289 right = int(m.group(4))
290 self.handle_get_location_segments(path, peg, left, right)
291 return line[m.end():]
293 def _parse_get_file_revs(self, line):
294 m = _match(line, pPATH, pREVRANGE, ['include-merged-revisions'])
295 path = m.group(1)
296 left = int(m.group(2))
297 right = int(m.group(3))
298 include_merged_revisions = m.group(4) is not None
299 self.handle_get_file_revs(path, left, right, include_merged_revisions)
300 return line[m.end():]
302 def _parse_get_mergeinfo(self, line):
303 # <I>
304 pMERGEINFO_INHERITANCE = pWORD
305 pINCLUDE_DESCENDANTS = pWORD
306 m = _match(line,
307 pPATHS, pMERGEINFO_INHERITANCE, ['include-descendants'])
308 paths = m.group(1).split()
309 inheritance = _parse_mergeinfo_inheritance(m.group(2))
310 include_descendants = m.group(3) is not None
311 self.handle_get_mergeinfo(paths, inheritance, include_descendants)
312 return line[m.end():]
314 def _parse_log(self, line):
315 # limit=<N>?
316 pLIMIT = r'limit=(\d+)'
317 # revprops=all|(<REVPROP> ...)?
318 pREVPROPS = r'revprops=(all|\(([^)]+)\))'
319 m = _match(line, pPATHS, pREVRANGE,
320 [pLIMIT, 'discover-changed-paths', 'strict',
321 'include-merged-revisions', pREVPROPS])
322 paths = m.group(1).split()
323 left = int(m.group(2))
324 right = int(m.group(3))
325 if m.group(5) is None:
326 limit = 0
327 else:
328 limit = int(m.group(5))
329 discover_changed_paths = m.group(6) is not None
330 strict = m.group(7) is not None
331 include_merged_revisions = m.group(8) is not None
332 if m.group(10) == 'all':
333 revprops = None
334 else:
335 if m.group(11) is None:
336 revprops = []
337 else:
338 revprops = m.group(11).split()
339 self.handle_log(paths, left, right, limit, discover_changed_paths,
340 strict, include_merged_revisions, revprops)
341 return line[m.end():]
343 def _parse_check_path(self, line):
344 m = _match(line, pPATHREV)
345 path = m.group(1)
346 revnum = int(m.group(2))
347 self.handle_check_path(path, revnum)
348 return line[m.end():]
350 def _parse_stat(self, line):
351 m = _match(line, pPATHREV)
352 path = m.group(1)
353 revnum = int(m.group(2))
354 self.handle_stat(path, revnum)
355 return line[m.end():]
357 def _parse_replay(self, line):
358 m = _match(line, pPATH, pREVNUM)
359 path = m.group(1)
360 revision = int(m.group(2))
361 self.handle_replay(path, revision)
362 return line[m.end():]
364 # the update report
366 def _parse_checkout_or_export(self, line):
367 m = _match(line, pPATH, pREVNUM, [pDEPTH])
368 path = m.group(1)
369 revision = int(m.group(2))
370 depth = _parse_depth(m.group(4))
371 self.handle_checkout_or_export(path, revision, depth)
372 return line[m.end():]
374 def _parse_diff(self, line):
375 # First, try 1-path form.
376 try:
377 m = _match(line, pPATH, pREVRANGE, [pDEPTH, 'ignore-ancestry'])
378 f = self._parse_diff_1path
379 except Error:
380 # OK, how about 2-path form?
381 m = _match(line, pPATHREV, pPATHREV, [pDEPTH, 'ignore-ancestry'])
382 f = self._parse_diff_2paths
383 return f(line, m)
385 def _parse_diff_1path(self, line, m):
386 path = m.group(1)
387 left = int(m.group(2))
388 right = int(m.group(3))
389 depth = _parse_depth(m.group(5))
390 ignore_ancestry = m.group(6) is not None
391 self.handle_diff_1path(path, left, right,
392 depth, ignore_ancestry)
393 return line[m.end():]
395 def _parse_diff_2paths(self, line, m):
396 from_path = m.group(1)
397 from_rev = int(m.group(2))
398 to_path = m.group(3)
399 to_rev = int(m.group(4))
400 depth = _parse_depth(m.group(6))
401 ignore_ancestry = m.group(7) is not None
402 self.handle_diff_2paths(from_path, from_rev, to_path, to_rev,
403 depth, ignore_ancestry)
404 return line[m.end():]
406 def _parse_status(self, line):
407 m = _match(line, pPATH, pREVNUM, [pDEPTH])
408 path = m.group(1)
409 revision = int(m.group(2))
410 depth = _parse_depth(m.group(4))
411 self.handle_status(path, revision, depth)
412 return line[m.end():]
414 def _parse_switch(self, line):
415 m = _match(line, pPATH, pPATHREV, [pDEPTH])
416 from_path = m.group(1)
417 to_path = m.group(2)
418 to_rev = int(m.group(3))
419 depth = _parse_depth(m.group(5))
420 self.handle_switch(from_path, to_path, to_rev, depth)
421 return line[m.end():]
423 def _parse_update(self, line):
424 m = _match(line, pPATH, pREVNUM, [pDEPTH, 'send-copyfrom-args'])
425 path = m.group(1)
426 revision = int(m.group(2))
427 depth = _parse_depth(m.group(4))
428 send_copyfrom_args = m.group(5) is not None
429 self.handle_update(path, revision, depth, send_copyfrom_args)
430 return line[m.end():]