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.
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.
47 change-rev-prop r<N> <REVPROP>
49 get-dir <PATH> r<N> text? props?
50 get-file <PATH> r<N> text? props?
51 lock (<PATH> ...) steal?
53 unlock (<PATH> ...) break?
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> ...)?
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?
77 # Valid words for _parse_depth and _parse_mergeinfo_inheritance
80 DEPTH_WORDS
= ['empty', 'files', 'immediates', 'infinity']
82 'explicit': svn
.core
.svn_mergeinfo_explicit
,
83 'inherited': svn
.core
.svn_mergeinfo_inherited
,
84 'nearest-ancestor': svn
.core
.svn_mergeinfo_nearest_ancestor
,
94 pPATHS
= r
'\(([^)]*)\)'
98 pREVRANGE
= r
'r(\d+):(\d+)'
100 pPATHREV
= pPATH
+ r
'@(\d+)'
103 pDEPTH
= 'depth=' + pWORD
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'
126 # TODO: Move to kitchensink.c like svn_depth_from_word?
128 from svn
.core
import svn_inheritance_from_word
130 def svn_inheritance_from_word(word
):
132 return INHERITANCE_WORDS
[word
]
134 # XXX svn_inheritance_to_word uses explicit as default so...
135 return svn
.core
.svn_mergeinfo_explicit
137 def _parse_depth(word
):
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.
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]
165 pattern
= r
'\s+'.join(patterns
)
166 pattern
+= ''.join([r
'(\s+' + x
+ ')?' for x
in optional
])
167 m
= re
.match(pattern
, line
)
169 raise MatchError(pattern
, line
)
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.
185 - line remaining after the svn action, if one was parsed
186 - whatever your handle_unknown implementation returns
189 BadDepthError -- for bad svn_depth_t values
190 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
192 Error -- any other parse error
195 words
= self
.split_line
= line
.split(' ')
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
):
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():]
247 def _parse_get_file_revs(self
, line
):
248 m
= _match(line
, pPATH
, pREVRANGE
, ['include-merged-revisions'])
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
):
258 pMERGEINFO_INHERITANCE
= pWORD
259 pINCLUDE_DESCENDANTS
= pWORD
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
):
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:
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':
289 if m
.group(11) is None:
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
)
300 revision
= int(m
.group(2))
301 self
.handle_replay(path
, revision
)
302 return line
[m
.end():]
306 def _parse_checkout_or_export(self
, line
):
307 m
= _match(line
, pPATH
, pREVNUM
, [pDEPTH
])
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.
317 m
= _match(line
, pPATH
, pREVRANGE
, [pDEPTH
, 'ignore-ancestry'])
318 f
= self
._parse
_diff
_1path
320 # OK, how about 2-path form?
321 m
= _match(line
, pPATHREV
, pPATHREV
, [pDEPTH
, 'ignore-ancestry'])
322 f
= self
._parse
_diff
_2paths
325 def _parse_diff_1path(self
, line
, m
):
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))
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
])
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)
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'])
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():]