Reorganize the output to "svnserve --help".
[svn.git] / tools / server-side / svn_dav_log_parse.py
blob8fb517eb642d5c11316d3a21fc69104e399b12fe
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 """Parse mod-dav-svn operational logs.
19 SVN-ACTION strings
20 ------------------
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.
45 General::
47 change-rev-prop r<N> <REVPROP>
48 commit r<N>
49 get-dir <PATH> r<N> text? props?
50 get-file <PATH> r<N> text? props?
51 lock <PATH> steal?
52 rev-proplist r<N>
53 unlock <PATH> break?
55 Reports::
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> ...)?
60 replay <PATH> r<N>
62 The update report::
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?
70 """
73 import re
74 import svn.core
77 # Valid words for _parse_depth and _parse_mergeinfo_inheritance
80 DEPTH_WORDS = ['empty', 'files', 'immediates', 'infinity']
81 INHERITANCE_WORDS = {
82 'explicit': svn.core.svn_mergeinfo_explicit,
83 'inherited': svn.core.svn_mergeinfo_inherited,
84 'nearest-ancestor': svn.core.svn_mergeinfo_nearest_ancestor,
88 # Patterns for _match
91 # <PATH>
92 pPATH = r'(/\S*)'
93 # (<PATH> ...)
94 pPATHS = r'\(([^)]*)\)'
95 # r<N>
96 pREVNUM = r'r(\d+)'
97 # r<N>:<M>
98 pREVRANGE = r'r(\d+):(\d+)'
99 # <PATH>@<N>
100 pPATHREV = pPATH + r'@(\d+)'
101 pWORD = r'(\S+)'
102 # depth=<D>?
103 pDEPTH = 'depth=' + pWORD
106 # Exceptions
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)
118 # Helper functions
121 # TODO: Move to kitchensink.c like svn_depth_from_word?
122 try:
123 from svn.core import svn_inheritance_from_word
124 except ImportError:
125 def svn_inheritance_from_word(word):
126 try:
127 return INHERITANCE_WORDS[word]
128 except KeyError:
129 # XXX svn_inheritance_to_word uses explicit as default so...
130 return svn.core.svn_mergeinfo_explicit
132 def _parse_depth(word):
133 if word is None:
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.
152 Raises:
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]
158 else:
159 optional = []
160 pattern = r'\s+'.join(patterns)
161 pattern += ''.join([r'(\s+' + x + ')?' for x in optional])
162 m = re.match(pattern, line)
163 if m is None:
164 raise Error
165 return m
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.
179 Returns one of:
180 - line remaining after the svn action, if one was parsed
181 - whatever your handle_unknown implementation returns
183 Raises:
184 BadDepthError -- for bad svn_depth_t values
185 BadMergeinfoInheritanceError -- for bad svn_mergeinfo_inheritance_t
186 values
187 Error -- any other parse error
189 self.line = line
190 words = self.split_line = line.split(' ')
191 try:
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):
222 # <REVPROP>
223 pPROPERTY = pWORD
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():]
238 # reports
240 def _parse_get_file_revs(self, line):
241 m = _match(line, pPATH, pREVRANGE, ['include-merged-revisions'])
242 path = m.group(1)
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):
250 # <I>
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):
259 # limit=<N>?
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:
270 limit = 0
271 else:
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':
277 revprops = None
278 else:
279 if m.group(11) is None:
280 revprops = []
281 else:
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)
289 path = m.group(1)
290 revision = int(m.group(2))
291 self.handle_replay(path, revision)
292 return line[m.end():]
294 # the update report
296 def _parse_checkout_or_export(self, line):
297 m = _match(line, pPATH, pREVNUM, [pDEPTH])
298 path = m.group(1)
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.
306 try:
307 m = _match(line, pPATH, pREVRANGE, [pDEPTH, 'ignore-ancestry'])
308 f = self._parse_diff_1path
309 except Error:
310 # OK, how about 2-path form?
311 m = _match(line, pPATHREV, pPATHREV, [pDEPTH, 'ignore-ancestry'])
312 f = self._parse_diff_2paths
313 return f(line, m)
315 def _parse_diff_1path(self, line, m):
316 path = m.group(1)
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))
328 to_path = m.group(3)
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])
338 path = m.group(1)
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))
348 to_path = m.group(3)
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'])
356 path = m.group(1)
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():]