Merge the tc-issue-3334 branch to trunk:
[svnrdump.git] / svntest / tree.py
blob4e2deb0dd493b72f995cc3a8e16c067743eed193
2 # tree.py: tools for comparing directory trees
4 # Subversion is a tool for revision control.
5 # See http://subversion.tigris.org for more information.
7 # ====================================================================
8 # Copyright (c) 2001, 2006, 2008 CollabNet. All rights reserved.
10 # This software is licensed as described in the file COPYING, which
11 # you should have received as part of this distribution. The terms
12 # are also available at http://subversion.tigris.org/license-1.html.
13 # If newer versions of this license are posted there, you may use a
14 # newer version instead, at your option.
16 ######################################################################
18 import re
19 import os
20 import sys
22 import main # the general svntest routines in this module.
23 from svntest import Failure
25 # Tree Exceptions.
27 # All tree exceptions should inherit from SVNTreeError
28 class SVNTreeError(Failure):
29 "Exception raised if you screw up in the tree module."
30 pass
32 class SVNTreeUnequal(SVNTreeError):
33 "Exception raised if two trees are unequal."
34 pass
36 class SVNTreeIsNotDirectory(SVNTreeError):
37 "Exception raised if get_child is passed a file."
38 pass
40 class SVNTypeMismatch(SVNTreeError):
41 "Exception raised if one node is file and other is dir"
42 pass
44 #========================================================================
46 # ===> Overview of our Datastructures <===
48 # The general idea here is that many, many things can be represented by
49 # a tree structure:
51 # - a working copy's structure and contents
52 # - the output of 'svn status'
53 # - the output of 'svn checkout/update'
54 # - the output of 'svn commit'
56 # The idea is that a test function creates a "expected" tree of some
57 # kind, and is then able to compare it to an "actual" tree that comes
58 # from running the Subversion client. This is what makes a test
59 # automated; if an actual and expected tree match exactly, then the test
60 # has passed. (See compare_trees() below.)
62 # The SVNTreeNode class is the fundamental data type used to build tree
63 # structures. The class contains a method for "dropping" a new node
64 # into an ever-growing tree structure. (See also create_from_path()).
66 # We have four parsers in this file for the four use cases listed above:
67 # each parser examines some kind of input and returns a tree of
68 # SVNTreeNode objects. (See build_tree_from_checkout(),
69 # build_tree_from_commit(), build_tree_from_status(), and
70 # build_tree_from_wc()). These trees are the "actual" trees that result
71 # from running the Subversion client.
73 # Also necessary, of course, is a convenient way for a test to create an
74 # "expected" tree. The test *could* manually construct and link a bunch
75 # of SVNTreeNodes, certainly. But instead, all the tests are using the
76 # build_generic_tree() routine instead.
78 # build_generic_tree() takes a specially-formatted list of lists as
79 # input, and returns a tree of SVNTreeNodes. The list of lists has this
80 # structure:
82 # [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}],
83 # [...],
84 # [...],
85 # ... ]
87 # You can see that each item in the list essentially defines an
88 # SVNTreeNode. build_generic_tree() instantiates a SVNTreeNode for each
89 # item, and then drops it into a tree by parsing each item's full path.
91 # So a typical test routine spends most of its time preparing lists of
92 # this format and sending them to build_generic_tree(), rather than
93 # building the "expected" trees directly.
95 # ### Note: in the future, we'd like to remove this extra layer of
96 # ### abstraction. We'd like the SVNTreeNode class to be more
97 # ### directly programmer-friendly, providing a number of accessor
98 # ### routines, so that tests can construct trees directly.
100 # The first three fields of each list-item are self-explanatory. It's
101 # the fourth field, the "attribute" hash, that needs some explanation.
102 # The att-hash is used to place extra information about the node itself,
103 # depending on the parsing context:
105 # - in the 'svn co/up' use-case, each line of output starts with two
106 # characters from the set of (A, D, G, U, C, _) or 'Restored'. The
107 # status code is stored in a attribute named 'status'. In the case
108 # of a restored file, the word 'Restored' is stored in an attribute
109 # named 'verb'.
111 # - in the 'svn ci/im' use-case, each line of output starts with one
112 # of the words (Adding, Deleting, Sending). This verb is stored in
113 # an attribute named 'verb'.
115 # - in the 'svn status' use-case (which is always run with the -v
116 # (--verbose) flag), each line of output contains a working revision
117 # number and a two-letter status code similar to the 'svn co/up'
118 # case. This information is stored in attributes named 'wc_rev'
119 # and 'status'. The repository revision is also printed, but it
120 # is ignored.
122 # - in the working-copy use-case, the att-hash is ignored.
125 # Finally, one last explanation: the file 'actions.py' contain a number
126 # of helper routines named 'run_and_verify_FOO'. These routines take
127 # one or more "expected" trees as input, then run some svn subcommand,
128 # then push the output through an appropriate parser to derive an
129 # "actual" tree. Then it runs compare_trees() and raises an exception
130 # on failure. This is why most tests typically end with a call to
131 # run_and_verify_FOO().
135 #========================================================================
137 # A node in a tree.
139 # If CHILDREN is None, then the node is a file. Otherwise, CHILDREN
140 # is a list of the nodes making up that directory's children.
142 # NAME is simply the name of the file or directory. CONTENTS is a
143 # string that contains the file's contents (if a file), PROPS are
144 # properties attached to files or dirs, and ATTS is a dictionary of
145 # other metadata attached to the node.
147 class SVNTreeNode:
149 def __init__(self, name, children=None, contents=None, props={}, atts={}):
150 self.name = name
151 self.children = children
152 self.contents = contents
153 self.props = props
154 self.atts = atts
155 self.path = name
157 # TODO: Check to make sure contents and children are mutually exclusive
159 def add_child(self, newchild):
160 child_already_exists = 0
161 if self.children is None: # if you're a file,
162 self.children = [] # become an empty dir.
163 else:
164 for a in self.children:
165 if a.name == newchild.name:
166 child_already_exists = 1
167 break
169 if child_already_exists:
170 if newchild.children is None:
171 # this is the 'end' of the chain, so copy any content here.
172 a.contents = newchild.contents
173 a.props = newchild.props
174 a.atts = newchild.atts
175 a.path = os.path.join (self.path, newchild.name)
176 else:
177 # try to add dangling children to your matching node
178 for i in newchild.children:
179 a.add_child(i)
180 else:
181 self.children.append(newchild)
182 newchild.path = os.path.join(self.path, newchild.name)
185 def pprint(self, stream = sys.stdout):
186 "Pretty-print the meta data for this node to STREAM."
187 stream.write(" * Node name: %s\n" % self.name)
188 stream.write(" Path: %s\n" % self.path)
189 mime_type = self.props.get("svn:mime-type")
190 if not mime_type or mime_type.startswith("text/"):
191 if self.children is not None:
192 stream.write(" Contents: N/A (node is a directory)\n")
193 else:
194 stream.write(" Contents: %s\n" % self.contents)
195 else:
196 stream.write(" Contents: %d bytes (binary)\n" % len(self.contents))
197 stream.write(" Properties: %s\n" % self.props)
198 stream.write(" Attributes: %s\n" % self.atts)
199 ### FIXME: I'd like to be able to tell the difference between
200 ### self.children is None (file) and self.children == [] (empty
201 ### directory), but it seems that most places that construct
202 ### SVNTreeNode objects don't even try to do that. --xbc
204 ### See issue #1611 about this problem. -kfogel
205 if self.children is not None:
206 stream.write(" Children: %s\n" % len(self.children))
207 else:
208 stream.write(" Children: None (node is probably a file)\n")
209 stream.flush()
211 def print_script(self, stream = sys.stdout, subtree = ""):
212 """Python-script-print the meta data for this node to STREAM.
213 Print only those nodes whose path string starts with the string SUBTREE,
214 and print only the part of the path string that remains after SUBTREE."""
216 # remove some occurrences of root_node_name = "__SVN_ROOT_NODE",
217 # it is in the way when matching for a subtree, and looks bad.
218 path = self.path
219 if path.startswith(root_node_name + os.sep):
220 path = path[len(root_node_name + os.sep):]
222 # remove the subtree path, skip this node if necessary.
223 if path.startswith(subtree):
224 path = path[len(subtree):]
225 else:
226 return
228 line = " %-20s: Item(" % ("'%s'" % path)
229 comma = False
231 mime_type = self.props.get("svn:mime-type")
232 if not mime_type or mime_type.startswith("text/"):
233 if self.contents is not None:
234 # Escape some characters for nicer script and readability.
235 # (This is error output. I guess speed is no consideration here.)
236 line += "contents=\"%s\"" % (self.contents
237 .replace('\n','\\n')
238 .replace('"','\\"')
239 .replace('\r','\\r')
240 .replace('\t','\\t'))
241 comma = True
242 else:
243 line += 'content is binary data'
244 comma = True
246 if self.props:
247 if comma:
248 line += ", "
249 line += "props={"
250 comma = False
252 for name in self.props:
253 if comma:
254 line += ", "
255 line += "'%s':'%s'" % (name, self.props[name])
256 comma = True
258 line += "}"
259 comma = True
261 for name in self.atts:
262 if comma:
263 line += ", "
264 line += "%s='%s'" % (name, self.atts[name])
265 comma = True
267 line += "),"
268 stream.write("%s\n" % line)
269 stream.flush()
272 def __str__(self):
273 import StringIO
274 s = StringIO.StringIO()
275 self.pprint(s)
276 return s.getvalue()
279 def __cmp__(self, other):
280 """Define a simple ordering of two nodes without regard to their full
281 path (i.e. position in the tree). This can be used for sorting the
282 children within a directory."""
283 return cmp(self.name, other.name)
286 # reserved name of the root of the tree
287 root_node_name = "__SVN_ROOT_NODE"
290 # helper func
291 def add_elements_as_path(top_node, element_list):
292 """Add the elements in ELEMENT_LIST as if they were a single path
293 below TOP_NODE."""
295 # The idea of this function is to take a list like so:
296 # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree
297 # like this:
299 # Z -> A -> B -> C
301 # where 1 -> 2 means 2 is a child of 1.
304 prev_node = top_node
305 for i in element_list:
306 new_node = SVNTreeNode(i, None)
307 prev_node.add_child(new_node)
308 prev_node = new_node
311 def compare_atts(a, b):
312 """Compare two dictionaries of attributes, A (actual) and B (expected).
313 If the attribute 'treeconflict' in B is missing or is 'None', ignore it.
314 Return 0 if the same, 1 otherwise."""
315 a = a.copy()
316 b = b.copy()
317 # Remove any attributes to ignore.
318 for att in ['treeconflict']:
319 if (att not in b) or (b[att] is None):
320 if att in a:
321 del a[att]
322 if att in b:
323 del b[att]
324 if a != b:
325 return 1
326 return 0
328 # Helper for compare_trees
329 def compare_file_nodes(a, b):
330 """Compare two nodes, A (actual) and B (expected). Compare their names,
331 contents, properties and attributes, ignoring children. Return 0 if the
332 same, 1 otherwise."""
333 if a.name != b.name:
334 return 1
335 if a.contents != b.contents:
336 return 1
337 if a.props != b.props:
338 return 1
339 return compare_atts(a.atts, b.atts)
342 # Internal utility used by most build_tree_from_foo() routines.
344 # (Take the output and .add_child() it to a root node.)
346 def create_from_path(path, contents=None, props={}, atts={}):
347 """Create and return a linked list of treenodes, given a PATH
348 representing a single entry into that tree. CONTENTS and PROPS are
349 optional arguments that will be deposited in the tail node."""
351 # get a list of all the names in the path
352 # each of these will be a child of the former
353 if os.sep != "/":
354 path = path.replace(os.sep, "/")
355 elements = path.split("/")
356 if len(elements) == 0:
357 ### we should raise a less generic error here. which?
358 raise SVNTreeError
360 root_node = None
362 # if this is Windows: if the path contains a drive name (X:), make it
363 # the root node.
364 if os.name == 'nt':
365 m = re.match("([a-zA-Z]:)(.+)", elements[0])
366 if m:
367 root_node = SVNTreeNode(m.group(1), None)
368 elements[0] = m.group(2)
369 add_elements_as_path(root_node, elements[0:])
371 if not root_node:
372 root_node = SVNTreeNode(elements[0], None)
373 add_elements_as_path(root_node, elements[1:])
375 # deposit contents in the very last node.
376 node = root_node
377 while 1:
378 if node.children is None:
379 node.contents = contents
380 node.props = props
381 node.atts = atts
382 break
383 node = node.children[0]
385 return root_node
388 # helper for handle_dir(), which is a helper for build_tree_from_wc()
389 def get_props(path):
390 """Return a hash of props for PATH, using the svn client. Convert each
391 embedded end-of-line to a single LF character."""
393 # It's not kosher to look inside .svn/ and try to read the internal
394 # property storage format. Instead, we use 'svn proplist'. After
395 # all, this is the only way the user can retrieve them, so we're
396 # respecting the black-box paradigm.
398 props = {}
399 exit_code, output, errput = main.run_svn(1, "proplist", path, "--verbose")
401 # Parse the output
402 for line in output:
403 line = line.rstrip('\r\n') # ignore stdout's EOL sequence
405 if line.startswith('Properties on '):
406 continue
408 elif line.startswith(' '):
409 # It's (part of) the value
410 props[name] += line[4:] + '\n' # strip the indentation
412 elif line.startswith(' '):
413 # It's the name
414 name = line[2:] # strip the indentation
415 props[name] = ''
417 else:
418 raise "Malformed line from proplist: '"+line+"'"
420 # Strip, from each property value, the final new-line that we added
421 for name in props:
422 props[name] = props[name][:-1]
424 return props
427 # helper for handle_dir(), which helps build_tree_from_wc()
428 def get_text(path):
429 "Return a string with the textual contents of a file at PATH."
431 # sanity check
432 if not os.path.isfile(path):
433 return None
435 fp = open(path, 'r')
436 contents = fp.read()
437 fp.close()
438 return contents
441 # main recursive helper for build_tree_from_wc()
442 def handle_dir(path, current_parent, load_props, ignore_svn):
444 # get a list of all the files
445 all_files = os.listdir(path)
446 files = []
447 dirs = []
449 # put dirs and files in their own lists, and remove SVN dirs
450 for f in all_files:
451 f = os.path.join(path, f)
452 if (os.path.isdir(f) and os.path.basename(f) != main.get_admin_name()):
453 dirs.append(f)
454 elif os.path.isfile(f):
455 files.append(f)
457 # add each file as a child of CURRENT_PARENT
458 for f in files:
459 fcontents = get_text(f)
460 if load_props:
461 fprops = get_props(f)
462 else:
463 fprops = {}
464 current_parent.add_child(SVNTreeNode(os.path.basename(f), None,
465 fcontents, fprops))
467 # for each subdir, create a node, walk its tree, add it as a child
468 for d in dirs:
469 if load_props:
470 dprops = get_props(d)
471 else:
472 dprops = {}
473 new_dir_node = SVNTreeNode(os.path.basename(d), None, None, dprops)
474 current_parent.add_child(new_dir_node)
475 handle_dir(d, new_dir_node, load_props, ignore_svn)
477 def get_child(node, name):
478 """If SVNTreeNode NODE contains a child named NAME, return child;
479 else, return None. If SVNTreeNode is not a directory, raise a
480 SVNTreeIsNotDirectory exception"""
481 if node.children == None:
482 raise SVNTreeIsNotDirectory
483 for n in node.children:
484 if (name == n.name):
485 return n
486 return None
489 # Helper for compare_trees
490 def default_singleton_handler(node, description):
491 """Print SVNTreeNode NODE's name, describing it with the string
492 DESCRIPTION, then raise SVNTreeUnequal."""
493 print("Couldn't find node '%s' in %s tree" % (node.name, description))
494 node.pprint()
495 raise SVNTreeUnequal
497 # A test helper function implementing the singleton_handler_a API.
498 def detect_conflict_files(node, extra_files):
499 """NODE has been discovered, an extra file on disk. Verify that it
500 matches one of the regular expressions in the EXTRA_FILES list. If
501 it matches, remove the match from the list. If it doesn't match,
502 raise an exception."""
504 for pattern in extra_files:
505 mo = re.match(pattern, node.name)
506 if mo:
507 extra_files.pop(extra_files.index(pattern)) # delete pattern from list
508 break
509 else:
510 msg = "Encountered unexpected disk path '" + node.name + "'"
511 print(msg)
512 node.pprint()
513 raise SVNTreeUnequal(msg)
515 ###########################################################################
516 ###########################################################################
517 # EXPORTED ROUTINES ARE BELOW
520 # Main tree comparison routine!
522 def compare_trees(label,
523 a, b,
524 singleton_handler_a = None,
525 a_baton = None,
526 singleton_handler_b = None,
527 b_baton = None):
528 """Compare SVNTreeNodes A (actual) and B (expected), expressing
529 differences using FUNC_A and FUNC_B. FUNC_A and FUNC_B are
530 functions of two arguments (a SVNTreeNode and a context baton), and
531 may raise exception SVNTreeUnequal, in which case they use the
532 string LABEL to describe the error (their return value is ignored).
533 LABEL is typically "output", "disk", "status", or some other word
534 that labels the trees being compared.
536 If A and B are both files, then return if their contents,
537 properties, and names are all the same; else raise a SVNTreeUnequal.
538 If A is a file and B is a directory, raise a SVNTreeUnequal; same
539 vice-versa. If both are directories, then for each entry that
540 exists in both, call compare_trees on the two entries; otherwise, if
541 the entry exists only in A, invoke FUNC_A on it, and likewise for
542 B with FUNC_B."""
544 def display_nodes(a, b):
545 'Display two nodes, expected and actual.'
546 print("=============================================================")
547 print("Expected '%s' and actual '%s' in %s tree are different!"
548 % (b.name, a.name, label))
549 print("=============================================================")
550 print("EXPECTED NODE TO BE:")
551 print("=============================================================")
552 b.pprint()
553 print("=============================================================")
554 print("ACTUAL NODE FOUND:")
555 print("=============================================================")
556 a.pprint()
558 # Setup singleton handlers
559 if (singleton_handler_a is None):
560 singleton_handler_a = default_singleton_handler
561 a_baton = "expected " + label
562 if (singleton_handler_b is None):
563 singleton_handler_b = default_singleton_handler
564 b_baton = "actual " + label
566 try:
567 # A and B are both files.
568 if ((a.children is None) and (b.children is None)):
569 if compare_file_nodes(a, b):
570 display_nodes(a, b)
571 raise SVNTreeUnequal
572 # One is a file, one is a directory.
573 elif (((a.children is None) and (b.children is not None))
574 or ((a.children is not None) and (b.children is None))):
575 display_nodes(a, b)
576 raise SVNTypeMismatch
577 # They're both directories.
578 else:
579 # First, compare the directories' two hashes.
580 if (a.props != b.props) or compare_atts(a.atts, b.atts):
581 display_nodes(a, b)
582 raise SVNTreeUnequal
584 accounted_for = []
585 # For each child of A, check and see if it's in B. If so, run
586 # compare_trees on the two children and add b's child to
587 # accounted_for. If not, run FUNC_A on the child. Next, for each
588 # child of B, check and see if it's in accounted_for. If it is,
589 # do nothing. If not, run FUNC_B on it.
590 for a_child in a.children:
591 b_child = get_child(b, a_child.name)
592 if b_child:
593 accounted_for.append(b_child)
594 compare_trees(label, a_child, b_child,
595 singleton_handler_a, a_baton,
596 singleton_handler_b, b_baton)
597 else:
598 singleton_handler_a(a_child, a_baton)
599 for b_child in b.children:
600 if (b_child not in accounted_for):
601 singleton_handler_b(b_child, b_baton)
602 except SVNTypeMismatch:
603 print('Unequal Types: one Node is a file, the other is a directory')
604 raise SVNTreeUnequal
605 except SVNTreeIsNotDirectory:
606 print("Error: Foolish call to get_child.")
607 sys.exit(1)
608 except IndexError:
609 print("Error: unequal number of children")
610 raise SVNTreeUnequal
611 except SVNTreeUnequal:
612 if a.name != root_node_name:
613 print("Unequal at node %s" % a.name)
614 raise
618 # Visually show a tree's structure
620 def dump_tree(n,indent=""):
621 """Print out a nice representation of the structure of the tree in
622 the SVNTreeNode N. Prefix each line with the string INDENT."""
624 # Code partially stolen from Dave Beazley
625 tmp_children = n.children or []
626 tmp_children.sort()
628 if n.name == root_node_name:
629 print("%s%s" % (indent, "ROOT"))
630 else:
631 print("%s%s" % (indent, n.name))
633 indent = indent.replace("-", " ")
634 indent = indent.replace("+", " ")
635 for i in range(len(tmp_children)):
636 c = tmp_children[i]
637 if i == len(tmp_children)-1:
638 dump_tree(c,indent + " +-- ")
639 else:
640 dump_tree(c,indent + " |-- ")
643 def dump_tree_script__crawler(n, subtree=""):
644 "Helper for dump_tree_script. See that comment."
646 # skip printing the root node.
647 if n.name != root_node_name:
648 n.print_script(subtree=subtree)
650 for child in n.children or []:
651 dump_tree_script__crawler(child, subtree)
654 def dump_tree_script(n, subtree=""):
655 """Print out a python script representation of the structure of the tree
656 in the SVNTreeNode N. Print only those nodes whose path string starts
657 with the string SUBTREE, and print only the part of the path string
658 that remains after SUBTREE."""
660 print("svntest.wc.State('%s', {" % subtree)
661 dump_tree_script__crawler(n, subtree)
662 print("})")
665 ###################################################################
666 ###################################################################
667 # PARSERS that return trees made of SVNTreeNodes....
670 ###################################################################
671 # Build an "expected" static tree from a list of lists
674 # Create a list of lists, of the form:
676 # [ [path, contents, props, atts], ... ]
678 # and run it through this parser. PATH is a string, a path to the
679 # object. CONTENTS is either a string or None, and PROPS and ATTS are
680 # populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be
681 # attached to the basename-node of the associated PATH.
683 def build_generic_tree(nodelist):
684 "Given a list of lists of a specific format, return a tree."
686 root = SVNTreeNode(root_node_name)
688 for list in nodelist:
689 new_branch = create_from_path(list[0], list[1], list[2], list[3])
690 root.add_child(new_branch)
692 return root
695 ####################################################################
696 # Build trees from different kinds of subcommand output.
699 # Parse co/up output into a tree.
701 # Tree nodes will contain no contents, a 'status' att, and a
702 # 'treeconflict' att.
704 def build_tree_from_checkout(lines, include_skipped=1):
705 "Return a tree derived by parsing the output LINES from 'co' or 'up'."
707 root = SVNTreeNode(root_node_name)
708 rm1 = re.compile ('^([RMAGCUDE_ ][MAGCUDE_ ])([B ])([C ])\s+(.+)')
709 if include_skipped:
710 rm2 = re.compile ('^(Restored|Skipped)\s+\'(.+)\'')
711 else:
712 rm2 = re.compile ('^(Restored)\s+\'(.+)\'')
714 for line in lines:
715 match = rm1.search(line)
716 if match and match.groups():
717 atts = {'status' : match.group(1)}
718 if match.group(3) == 'C':
719 atts['treeconflict'] = 'C'
720 new_branch = create_from_path(match.group(4), None, {}, atts)
721 root.add_child(new_branch)
722 else:
723 match = rm2.search(line)
724 if match and match.groups():
725 new_branch = create_from_path(match.group(2), None, {},
726 {'verb' : match.group(1)})
727 root.add_child(new_branch)
729 return root
732 # Parse ci/im output into a tree.
734 # Tree nodes will contain no contents, and only one 'verb' att.
736 def build_tree_from_commit(lines):
737 "Return a tree derived by parsing the output LINES from 'ci' or 'im'."
739 # Lines typically have a verb followed by whitespace then a path.
740 root = SVNTreeNode(root_node_name)
741 rm1 = re.compile ('^(\w+( \(bin\))?)\s+(.+)')
742 rm2 = re.compile ('^Transmitting')
744 for line in lines:
745 match = rm2.search(line)
746 if not match:
747 match = rm1.search(line)
748 if match and match.groups():
749 new_branch = create_from_path(match.group(3), None, {},
750 {'verb' : match.group(1)})
751 root.add_child(new_branch)
753 return root
756 # Parse status output into a tree.
758 # Tree nodes will contain no contents, and these atts:
760 # 'status', 'wc_rev',
761 # ... and possibly 'locked', 'copied', 'switched',
762 # 'writelocked' and 'treeconflict',
763 # IFF columns non-empty.
766 def build_tree_from_status(lines):
767 "Return a tree derived by parsing the output LINES from 'st -vuq'."
769 root = SVNTreeNode(root_node_name)
771 # 'status -v' output looks like this:
773 # "%c%c%c%c%c%c%c %c %6s %6s %-12s %s\n"
775 # (Taken from 'print_status' in subversion/svn/status.c.)
777 # Here are the parameters. The middle number or string in parens is the
778 # match.group(), followed by a brief description of the field:
780 # - text status (1) (single letter)
781 # - prop status (1) (single letter)
782 # - wc-lockedness flag (2) (single letter: "L" or " ")
783 # - copied flag (3) (single letter: "+" or " ")
784 # - switched flag (4) (single letter: "S" or " ")
785 # - repos lock status (5) (single letter: "K", "O", "B", "T", " ")
786 # - tree conflict flag (6) (single letter: "C" or " ")
788 # [one space]
790 # - out-of-date flag (7) (single letter: "*" or " ")
792 # [three spaces]
794 # - working revision ('wc_rev') (either digits or "-" or " ")
796 # [one space]
798 # - last-changed revision (either digits or "?" or " ")
800 # [one space]
802 # - last author (optional string of non-whitespace
803 # characters)
805 # [spaces]
807 # - path ('path') (string of characters until newline)
809 # Working revision, last-changed revision, and last author are whitespace
810 # only if the item is missing.
812 # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU
813 rm = re.compile('^([?!MACDRUG_ ][MACDRUG_ ])([L ])([+ ])([S ])([KOBT ])([C ]) ([* ]) +((?P<wc_rev>\d+|-|\?) +(\d|-|\?)+ +(\S+) +)?(?P<path>.+)$')
814 for line in lines:
816 # Quit when we hit an externals status announcement (### someday we can fix
817 # the externals tests to expect the additional flood of externals status
818 # data).
819 if re.match(r'^Performing', line):
820 break
822 match = rm.search(line)
823 if match and match.groups():
824 if match.group(10) != '-': # ignore items that only exist on repos
825 atthash = {'status' : match.group(1)}
826 if match.group(2) != ' ':
827 atthash['locked'] = match.group(2)
828 if match.group(3) != ' ':
829 atthash['copied'] = match.group(3)
830 if match.group(4) != ' ':
831 atthash['switched'] = match.group(4)
832 if match.group(5) != ' ':
833 atthash['writelocked'] = match.group(5)
834 if match.group(6) != ' ':
835 atthash['treeconflict'] = match.group(6)
836 if match.group('wc_rev'):
837 atthash['wc_rev'] = match.group('wc_rev')
838 new_branch = create_from_path(match.group('path'), None, {}, atthash)
840 root.add_child(new_branch)
842 return root
845 # Parse merge "skipped" output
847 def build_tree_from_skipped(lines):
849 root = SVNTreeNode(root_node_name)
850 rm = re.compile ("^Skipped.* '(.+)'\n")
852 for line in lines:
853 match = rm.search(line)
854 if match and match.groups():
855 new_branch = create_from_path(match.group(1))
856 root.add_child(new_branch)
858 return root
860 def build_tree_from_diff_summarize(lines):
861 "Build a tree from output of diff --summarize"
862 root = SVNTreeNode(root_node_name)
863 rm = re.compile ("^([MAD ][M ]) (.+)\n")
865 for line in lines:
866 match = rm.search(line)
867 if match and match.groups():
868 new_branch = create_from_path(match.group(2),
869 atts={'status': match.group(1)})
870 root.add_child(new_branch)
872 return root
874 ####################################################################
875 # Build trees by looking at the working copy
878 # The reason the 'load_props' flag is off by default is because it
879 # creates a drastic slowdown -- we spawn a new 'svn proplist'
880 # process for every file and dir in the working copy!
883 def build_tree_from_wc(wc_path, load_props=0, ignore_svn=1):
884 """Takes WC_PATH as the path to a working copy. Walks the tree below
885 that path, and creates the tree based on the actual found
886 files. If IGNORE_SVN is true, then exclude SVN admin dirs from the tree.
887 If LOAD_PROPS is true, the props will be added to the tree."""
889 root = SVNTreeNode(root_node_name, None)
891 # if necessary, store the root dir's props in a new child node '.'.
892 if load_props:
893 props = get_props(wc_path)
894 if props:
895 root_dir_node = SVNTreeNode(os.path.basename('.'), None, None, props)
896 root.add_child(root_dir_node)
898 # Walk the tree recursively
899 handle_dir(os.path.normpath(wc_path), root, load_props, ignore_svn)
901 return root
903 ### End of file.