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.
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.
49 change-rev-prop r<N> <REVPROP>
51 get-dir <PATH> r<N> text? props?
52 get-file <PATH> r<N> text? props?
53 lock (<PATH> ...) steal?
55 unlock (<PATH> ...) break?
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> ...)?
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?
79 # Valid words for _parse_depth and _parse_mergeinfo_inheritance
82 DEPTH_WORDS
= ['empty', 'files', 'immediates', 'infinity']
84 'explicit': svn
.core
.svn_mergeinfo_explicit
,
85 'inherited': svn
.core
.svn_mergeinfo_inherited
,
86 'nearest-ancestor': svn
.core
.svn_mergeinfo_nearest_ancestor
,
96 pPATHS
= r
'\(([^)]*)\)'
100 pREVNUMS
= r
'\(((\d+\s*)*)\)'
102 pREVRANGE
= r
'r(-?\d+):(-?\d+)'
104 pPATHREV
= pPATH
+ r
'@(\d+)'
108 pDEPTH
= 'depth=' + pWORD
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'
131 # TODO: Move to kitchensink.c like svn_depth_from_word?
133 from svn
.core
import svn_inheritance_from_word
135 def svn_inheritance_from_word(word
):
137 return INHERITANCE_WORDS
[word
]
139 # XXX svn_inheritance_to_word uses explicit as default so...
140 return svn
.core
.svn_mergeinfo_explicit
142 def _parse_depth(word
):
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.
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]
170 pattern
= r
'\s+'.join(patterns
)
171 pattern
+= ''.join([r
'(\s+' + x
+ ')?' for x
in optional
])
172 m
= re
.match(pattern
, line
)
174 raise MatchError(pattern
, line
)
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.
190 - line remaining after the svn action, if one was parsed
191 - whatever your handle_unknown implementation returns
194 BadDepthError -- for bad svn_depth_t values
195 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
197 Error -- any other parse error
200 words
= self
.split_line
= line
.split(' ')
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()
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
)
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
)
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'])
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
):
304 pMERGEINFO_INHERITANCE
= pWORD
305 pINCLUDE_DESCENDANTS
= pWORD
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
):
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:
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':
335 if m
.group(11) is None:
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
)
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
)
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
)
360 revision
= int(m
.group(2))
361 self
.handle_replay(path
, revision
)
362 return line
[m
.end():]
366 def _parse_checkout_or_export(self
, line
):
367 m
= _match(line
, pPATH
, pREVNUM
, [pDEPTH
])
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.
377 m
= _match(line
, pPATH
, pREVRANGE
, [pDEPTH
, 'ignore-ancestry'])
378 f
= self
._parse
_diff
_1path
380 # OK, how about 2-path form?
381 m
= _match(line
, pPATHREV
, pPATHREV
, [pDEPTH
, 'ignore-ancestry'])
382 f
= self
._parse
_diff
_2paths
385 def _parse_diff_1path(self
, line
, m
):
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))
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
])
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)
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'])
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():]