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.
51 revprop-change r<N> <REVPROP>
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> ...)?
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?
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
)
121 # TODO: Move to kitchensink.c like svn_depth_from_word?
123 from svn
.core
import svn_inheritance_from_word
125 def svn_inheritance_from_word(word
):
127 return INHERITANCE_WORDS
[word
]
129 # XXX svn_inheritance_to_word uses explicit as default so...
130 return svn
.core
.svn_mergeinfo_explicit
132 def _parse_depth(word
):
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.
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]
160 pattern
= r
'\s+'.join(patterns
)
161 pattern
+= ''.join([r
'(\s+' + x
+ ')?' for x
in optional
])
162 m
= re
.match(pattern
, line
)
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.
180 - line remaining after the svn action, if one was parsed
181 - whatever your handle_unknown implementation returns
184 BadDepthError -- for bad svn_depth_t values
185 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
187 Error -- any other parse error
190 words
= self
.split_line
= line
.split(' ')
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
):
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():]
236 def _parse_blame(self
, line
):
237 m
= _match(line
, pPATH
, pREVRANGE
, ['include-merged-revisions'])
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
):
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
):
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:
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':
275 if m
.group(11) is None:
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
)
286 revision
= int(m
.group(2))
287 self
.handle_replay(path
, revision
)
288 return line
[m
.end():]
292 def _parse_checkout_or_export(self
, line
):
293 m
= _match(line
, pPATH
, pREVNUM
, [pDEPTH
])
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.
303 m
= _match(line
, pPATH
, pREVRANGE
, [pDEPTH
, 'ignore-ancestry'])
304 f
= self
._parse
_diff
_or
_merge
_1path
306 # OK, how about 2-path form?
307 m
= _match(line
, pPATHREV
, pPATHREV
, [pDEPTH
, 'ignore-ancestry'])
308 f
= self
._parse
_diff
_or
_merge
_2paths
311 def _parse_diff_or_merge_1path(self
, line
, m
):
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))
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
])
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))
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'])
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():]