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?
57 get-file-revs <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 <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>@<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_get_dir(self
, line
):
203 m
= _match(line
, pPATH
, pREVNUM
, ['text', 'props'])
204 self
.handle_get_dir(m
.group(1), int(m
.group(2)),
205 m
.group(3) is not None,
206 m
.group(4) is not None)
207 return line
[m
.end():]
209 def _parse_get_file(self
, line
):
210 m
= _match(line
, pPATH
, pREVNUM
, ['text', 'props'])
211 self
.handle_get_file(m
.group(1), int(m
.group(2)),
212 m
.group(3) is not None,
213 m
.group(4) is not None)
214 return line
[m
.end():]
216 def _parse_lock(self
, line
):
217 m
= _match(line
, pPATH
, ['steal'])
218 self
.handle_lock(m
.group(1), m
.group(2) is not None)
219 return line
[m
.end():]
221 def _parse_change_rev_prop(self
, line
):
224 m
= _match(line
, pREVNUM
, pPROPERTY
)
225 self
.handle_change_rev_prop(int(m
.group(1)), m
.group(2))
226 return line
[m
.end():]
228 def _parse_rev_proplist(self
, line
):
229 m
= _match(line
, pREVNUM
)
230 self
.handle_rev_proplist(int(m
.group(1)))
231 return line
[m
.end():]
233 def _parse_unlock(self
, line
):
234 m
= _match(line
, pPATH
, ['break'])
235 self
.handle_unlock(m
.group(1), m
.group(2) is not None)
236 return line
[m
.end():]
240 def _parse_get_file_revs(self
, line
):
241 m
= _match(line
, pPATH
, pREVRANGE
, ['include-merged-revisions'])
243 left
= int(m
.group(2))
244 right
= int(m
.group(3))
245 include_merged_revisions
= m
.group(4) is not None
246 self
.handle_get_file_revs(path
, left
, right
, include_merged_revisions
)
247 return line
[m
.end():]
249 def _parse_get_mergeinfo(self
, line
):
251 pMERGEINFO_INHERITANCE
= pWORD
252 m
= _match(line
, pPATHS
, pMERGEINFO_INHERITANCE
)
253 paths
= m
.group(1).split()
254 inheritance
= _parse_mergeinfo_inheritance(m
.group(2))
255 self
.handle_get_mergeinfo(paths
, inheritance
)
256 return line
[m
.end():]
258 def _parse_log(self
, line
):
260 pLIMIT
= r
'limit=(\d+)'
261 # revprops=all|(<REVPROP> ...)?
262 pREVPROPS
= r
'revprops=(all|\(([^)]+)\))'
263 m
= _match(line
, pPATHS
, pREVRANGE
,
264 [pLIMIT
, 'discover-changed-paths', 'strict',
265 'include-merged-revisions', pREVPROPS
])
266 paths
= m
.group(1).split()
267 left
= int(m
.group(2))
268 right
= int(m
.group(3))
269 if m
.group(5) is None:
272 limit
= int(m
.group(5))
273 discover_changed_paths
= m
.group(6) is not None
274 strict
= m
.group(7) is not None
275 include_merged_revisions
= m
.group(8) is not None
276 if m
.group(10) == 'all':
279 if m
.group(11) is None:
282 revprops
= m
.group(11).split()
283 self
.handle_log(paths
, left
, right
, limit
, discover_changed_paths
,
284 strict
, include_merged_revisions
, revprops
)
285 return line
[m
.end():]
287 def _parse_replay(self
, line
):
288 m
= _match(line
, pPATH
, pREVNUM
)
290 revision
= int(m
.group(2))
291 self
.handle_replay(path
, revision
)
292 return line
[m
.end():]
296 def _parse_checkout_or_export(self
, line
):
297 m
= _match(line
, pPATH
, pREVNUM
, [pDEPTH
])
299 revision
= int(m
.group(2))
300 depth
= _parse_depth(m
.group(4))
301 self
.handle_checkout_or_export(path
, revision
, depth
)
302 return line
[m
.end():]
304 def _parse_diff(self
, line
):
305 # First, try 1-path form.
307 m
= _match(line
, pPATH
, pREVRANGE
, [pDEPTH
, 'ignore-ancestry'])
308 f
= self
._parse
_diff
_1path
310 # OK, how about 2-path form?
311 m
= _match(line
, pPATHREV
, pPATHREV
, [pDEPTH
, 'ignore-ancestry'])
312 f
= self
._parse
_diff
_2paths
315 def _parse_diff_1path(self
, line
, m
):
317 left
= int(m
.group(2))
318 right
= int(m
.group(3))
319 depth
= _parse_depth(m
.group(5))
320 ignore_ancestry
= m
.group(6) is not None
321 self
.handle_diff_1path(path
, left
, right
,
322 depth
, ignore_ancestry
)
323 return line
[m
.end():]
325 def _parse_diff_2paths(self
, line
, m
):
326 from_path
= m
.group(1)
327 from_rev
= int(m
.group(2))
329 to_rev
= int(m
.group(4))
330 depth
= _parse_depth(m
.group(6))
331 ignore_ancestry
= m
.group(7) is not None
332 self
.handle_diff_2paths(from_path
, from_rev
, to_path
, to_rev
,
333 depth
, ignore_ancestry
)
334 return line
[m
.end():]
336 def _parse_status(self
, line
):
337 m
= _match(line
, pPATH
, pREVNUM
, [pDEPTH
])
339 revision
= int(m
.group(2))
340 depth
= _parse_depth(m
.group(4))
341 self
.handle_status(path
, revision
, depth
)
342 return line
[m
.end():]
344 def _parse_switch(self
, line
):
345 m
= _match(line
, pPATHREV
, pPATHREV
, [pDEPTH
])
346 from_path
= m
.group(1)
347 from_rev
= int(m
.group(2))
349 to_rev
= int(m
.group(4))
350 depth
= _parse_depth(m
.group(6))
351 self
.handle_switch(from_path
, from_rev
, to_path
, to_rev
, depth
)
352 return line
[m
.end():]
354 def _parse_update(self
, line
):
355 m
= _match(line
, pPATH
, pREVNUM
, [pDEPTH
, 'send-copyfrom-args'])
357 revision
= int(m
.group(2))
358 depth
= _parse_depth(m
.group(4))
359 send_copyfrom_args
= m
.group(5) is not None
360 self
.handle_update(path
, revision
, depth
, send_copyfrom_args
)
361 return line
[m
.end():]