Change the 'svn status' letter for tree conflicts from 'T' to 'C', following
[svnrdump.git] / svntest / tree.py
blobbc700da3d0d485fbdde075317f106a0d33e509ef
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 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 print >> stream, " * Node name: ", self.name
188 print >> stream, " Path: ", 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 print >> stream, " Contents: N/A (node is a directory)"
193 else:
194 print >> stream, " Contents: ", self.contents
195 else:
196 print >> stream, " Contents: %d bytes (binary)" % len(self.contents)
197 print >> stream, " Properties:", self.props
198 print >> stream, " Attributes:", 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 print >> stream, " Children: ", len(self.children)
207 else:
208 print >> stream, " Children: None (node is probably a file)"
210 def print_script(self, stream = sys.stdout, subtree = ""):
211 """Python-script-print the meta data for this node to STREAM.
212 Print only those nodes whose path string starts with the string SUBTREE,
213 and print only the part of the path string that remains after SUBTREE."""
215 # remove some occurrences of root_node_name = "__SVN_ROOT_NODE",
216 # it is in the way when matching for a subtree, and looks bad.
217 path = self.path
218 if path.startswith(root_node_name + os.sep):
219 path = path[len(root_node_name + os.sep):]
221 # remove the subtree path, skip this node if necessary.
222 if path.startswith(subtree):
223 path = path[len(subtree):]
224 else:
225 return
227 line = " %-20s: Item(" % ("'%s'" % path)
228 comma = False
230 mime_type = self.props.get("svn:mime-type")
231 if not mime_type or mime_type.startswith("text/"):
232 if self.contents is not None:
233 # Escape some characters for nicer script and readability.
234 # (This is error output. I guess speed is no consideration here.)
235 line += "contents=\"%s\"" % (self.contents
236 .replace('\n','\\n')
237 .replace('"','\\"')
238 .replace('\r','\\r')
239 .replace('\t','\\t'))
240 comma = True
241 else:
242 line += 'content is binary data'
243 comma = True
245 if self.props:
246 if comma:
247 line += ", "
248 line += "props={"
249 comma = False
251 for name in self.props:
252 if comma:
253 line += ", "
254 line += "'%s':'%s'" % (name, self.props[name])
255 comma = True
257 line += "}"
258 comma = True
260 for name in self.atts:
261 if comma:
262 line += ", "
263 line += "%s='%s'" % (name, self.atts[name])
264 comma = True
266 line += "),"
267 print >> stream, line
270 def __str__(self):
271 import StringIO
272 s = StringIO.StringIO()
273 self.pprint(s)
274 return s.getvalue()
277 def __cmp__(self, other):
278 """Define a simple ordering of two nodes without regard to their full
279 path (i.e. position in the tree). This can be used for sorting the
280 children within a directory."""
281 return cmp(self.name, other.name)
284 # reserved name of the root of the tree
285 root_node_name = "__SVN_ROOT_NODE"
288 # helper func
289 def add_elements_as_path(top_node, element_list):
290 """Add the elements in ELEMENT_LIST as if they were a single path
291 below TOP_NODE."""
293 # The idea of this function is to take a list like so:
294 # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree
295 # like this:
297 # Z -> A -> B -> C
299 # where 1 -> 2 means 2 is a child of 1.
302 prev_node = top_node
303 for i in element_list:
304 new_node = SVNTreeNode(i, None)
305 prev_node.add_child(new_node)
306 prev_node = new_node
309 def compare_atts(a, b):
310 """Compare two dictionaries of attributes, A (actual) and B (expected).
311 If the attribute 'treeconflict' in B is missing or is 'None', ignore it.
312 Return 0 if the same, 1 otherwise."""
313 a = a.copy()
314 b = b.copy()
315 # Remove any attributes to ignore.
316 for att in ['treeconflict']:
317 if (att not in b) or (b[att] is None):
318 if att in a:
319 del a[att]
320 if att in b:
321 del b[att]
322 if a != b:
323 return 1
324 return 0
326 # Helper for compare_trees
327 def compare_file_nodes(a, b):
328 """Compare two nodes, A (actual) and B (expected). Compare their names,
329 contents, properties and attributes, ignoring children. Return 0 if the
330 same, 1 otherwise."""
331 if a.name != b.name:
332 return 1
333 if a.contents != b.contents:
334 return 1
335 if a.props != b.props:
336 return 1
337 return compare_atts(a.atts, b.atts)
340 # Internal utility used by most build_tree_from_foo() routines.
342 # (Take the output and .add_child() it to a root node.)
344 def create_from_path(path, contents=None, props={}, atts={}):
345 """Create and return a linked list of treenodes, given a PATH
346 representing a single entry into that tree. CONTENTS and PROPS are
347 optional arguments that will be deposited in the tail node."""
349 # get a list of all the names in the path
350 # each of these will be a child of the former
351 if os.sep != "/":
352 path = path.replace(os.sep, "/")
353 elements = path.split("/")
354 if len(elements) == 0:
355 ### we should raise a less generic error here. which?
356 raise SVNTreeError
358 root_node = None
360 # if this is Windows: if the path contains a drive name (X:), make it
361 # the root node.
362 if os.name == 'nt':
363 m = re.match("([a-zA-Z]:)(.+)", elements[0])
364 if m:
365 root_node = SVNTreeNode(m.group(1), None)
366 elements[0] = m.group(2)
367 add_elements_as_path(root_node, elements[0:])
369 if not root_node:
370 root_node = SVNTreeNode(elements[0], None)
371 add_elements_as_path(root_node, elements[1:])
373 # deposit contents in the very last node.
374 node = root_node
375 while 1:
376 if node.children is None:
377 node.contents = contents
378 node.props = props
379 node.atts = atts
380 break
381 node = node.children[0]
383 return root_node
386 # helper for handle_dir(), which is a helper for build_tree_from_wc()
387 def get_props(path):
388 """Return a hash of props for PATH, using the svn client. Convert each
389 embedded end-of-line to a single LF character."""
391 # It's not kosher to look inside .svn/ and try to read the internal
392 # property storage format. Instead, we use 'svn proplist'. After
393 # all, this is the only way the user can retrieve them, so we're
394 # respecting the black-box paradigm.
396 props = {}
397 exit_code, output, errput = main.run_svn(1, "proplist", path, "--verbose")
399 # Parse the output
400 for line in output:
401 line = line.rstrip('\r\n') # ignore stdout's EOL sequence
403 if line.startswith('Properties on '):
404 continue
406 elif line.startswith(' '):
407 # It's (part of) the value
408 props[name] += line[4:] + '\n' # strip the indentation
410 elif line.startswith(' '):
411 # It's the name
412 name = line[2:] # strip the indentation
413 props[name] = ''
415 else:
416 raise "Malformed line from proplist: '"+line+"'"
418 # Strip, from each property value, the final new-line that we added
419 for name in props:
420 props[name] = props[name][:-1]
422 return props
425 # helper for handle_dir(), which helps build_tree_from_wc()
426 def get_text(path):
427 "Return a string with the textual contents of a file at PATH."
429 # sanity check
430 if not os.path.isfile(path):
431 return None
433 fp = open(path, 'r')
434 contents = fp.read()
435 fp.close()
436 return contents
439 # main recursive helper for build_tree_from_wc()
440 def handle_dir(path, current_parent, load_props, ignore_svn):
442 # get a list of all the files
443 all_files = os.listdir(path)
444 files = []
445 dirs = []
447 # put dirs and files in their own lists, and remove SVN dirs
448 for f in all_files:
449 f = os.path.join(path, f)
450 if (os.path.isdir(f) and os.path.basename(f) != main.get_admin_name()):
451 dirs.append(f)
452 elif os.path.isfile(f):
453 files.append(f)
455 # add each file as a child of CURRENT_PARENT
456 for f in files:
457 fcontents = get_text(f)
458 if load_props:
459 fprops = get_props(f)
460 else:
461 fprops = {}
462 current_parent.add_child(SVNTreeNode(os.path.basename(f), None,
463 fcontents, fprops))
465 # for each subdir, create a node, walk its tree, add it as a child
466 for d in dirs:
467 if load_props:
468 dprops = get_props(d)
469 else:
470 dprops = {}
471 new_dir_node = SVNTreeNode(os.path.basename(d), None, None, dprops)
472 current_parent.add_child(new_dir_node)
473 handle_dir(d, new_dir_node, load_props, ignore_svn)
475 def get_child(node, name):
476 """If SVNTreeNode NODE contains a child named NAME, return child;
477 else, return None. If SVNTreeNode is not a directory, raise a
478 SVNTreeIsNotDirectory exception"""
479 if node.children == None:
480 raise SVNTreeIsNotDirectory
481 for n in node.children:
482 if (name == n.name):
483 return n
484 return None
487 # Helper for compare_trees
488 def default_singleton_handler(node, description):
489 """Print SVNTreeNode NODE's name, describing it with the string
490 DESCRIPTION, then raise SVNTreeUnequal."""
491 print "Couldn't find node '%s' in %s tree" % (node.name, description)
492 node.pprint()
493 raise SVNTreeUnequal
495 # A test helper function implementing the singleton_handler_a API.
496 def detect_conflict_files(node, extra_files):
497 """NODE has been discovered, an extra file on disk. Verify that it
498 matches one of the regular expressions in the EXTRA_FILES list. If
499 it matches, remove the match from the list. If it doesn't match,
500 raise an exception."""
502 for pattern in extra_files:
503 mo = re.match(pattern, node.name)
504 if mo:
505 extra_files.pop(extra_files.index(pattern)) # delete pattern from list
506 break
507 else:
508 msg = "Encountered unexpected disk path '" + node.name + "'"
509 print msg
510 node.pprint()
511 raise SVNTreeUnequal(msg)
513 ###########################################################################
514 ###########################################################################
515 # EXPORTED ROUTINES ARE BELOW
518 # Main tree comparison routine!
520 def compare_trees(label,
521 a, b,
522 singleton_handler_a = None,
523 a_baton = None,
524 singleton_handler_b = None,
525 b_baton = None):
526 """Compare SVNTreeNodes A (actual) and B (expected), expressing
527 differences using FUNC_A and FUNC_B. FUNC_A and FUNC_B are
528 functions of two arguments (a SVNTreeNode and a context baton), and
529 may raise exception SVNTreeUnequal, in which case they use the
530 string LABEL to describe the error (their return value is ignored).
531 LABEL is typically "output", "disk", "status", or some other word
532 that labels the trees being compared.
534 If A and B are both files, then return if their contents,
535 properties, and names are all the same; else raise a SVNTreeUnequal.
536 If A is a file and B is a directory, raise a SVNTreeUnequal; same
537 vice-versa. If both are directories, then for each entry that
538 exists in both, call compare_trees on the two entries; otherwise, if
539 the entry exists only in A, invoke FUNC_A on it, and likewise for
540 B with FUNC_B."""
542 def display_nodes(a, b):
543 'Display two nodes, expected and actual.'
544 print "============================================================="
545 print "Expected '%s' and actual '%s' in %s tree are different!" \
546 % (b.name, a.name, label)
547 print "============================================================="
548 print "EXPECTED NODE TO BE:"
549 print "============================================================="
550 b.pprint()
551 print "============================================================="
552 print "ACTUAL NODE FOUND:"
553 print "============================================================="
554 a.pprint()
556 # Setup singleton handlers
557 if (singleton_handler_a is None):
558 singleton_handler_a = default_singleton_handler
559 a_baton = "expected " + label
560 if (singleton_handler_b is None):
561 singleton_handler_b = default_singleton_handler
562 b_baton = "actual " + label
564 try:
565 # A and B are both files.
566 if ((a.children is None) and (b.children is None)):
567 if compare_file_nodes(a, b):
568 display_nodes(a, b)
569 raise SVNTreeUnequal
570 # One is a file, one is a directory.
571 elif (((a.children is None) and (b.children is not None))
572 or ((a.children is not None) and (b.children is None))):
573 display_nodes(a, b)
574 raise SVNTypeMismatch
575 # They're both directories.
576 else:
577 # First, compare the directories' two hashes.
578 if (a.props != b.props) or compare_atts(a.atts, b.atts):
579 display_nodes(a, b)
580 raise SVNTreeUnequal
582 accounted_for = []
583 # For each child of A, check and see if it's in B. If so, run
584 # compare_trees on the two children and add b's child to
585 # accounted_for. If not, run FUNC_A on the child. Next, for each
586 # child of B, check and see if it's in accounted_for. If it is,
587 # do nothing. If not, run FUNC_B on it.
588 for a_child in a.children:
589 b_child = get_child(b, a_child.name)
590 if b_child:
591 accounted_for.append(b_child)
592 compare_trees(label, a_child, b_child,
593 singleton_handler_a, a_baton,
594 singleton_handler_b, b_baton)
595 else:
596 singleton_handler_a(a_child, a_baton)
597 for b_child in b.children:
598 if (b_child not in accounted_for):
599 singleton_handler_b(b_child, b_baton)
600 except SVNTypeMismatch:
601 print 'Unequal Types: one Node is a file, the other is a directory'
602 raise SVNTreeUnequal
603 except SVNTreeIsNotDirectory:
604 print "Error: Foolish call to get_child."
605 sys.exit(1)
606 except IndexError:
607 print "Error: unequal number of children"
608 raise SVNTreeUnequal
609 except SVNTreeUnequal:
610 if a.name != root_node_name:
611 print "Unequal at node %s" % a.name
612 raise
616 # Visually show a tree's structure
618 def dump_tree(n,indent=""):
619 """Print out a nice representation of the structure of the tree in
620 the SVNTreeNode N. Prefix each line with the string INDENT."""
622 # Code partially stolen from Dave Beazley
623 tmp_children = n.children or []
624 tmp_children.sort()
626 if n.name == root_node_name:
627 print "%s%s" % (indent, "ROOT")
628 else:
629 print "%s%s" % (indent, n.name)
631 indent = indent.replace("-", " ")
632 indent = indent.replace("+", " ")
633 for i in range(len(tmp_children)):
634 c = tmp_children[i]
635 if i == len(tmp_children)-1:
636 dump_tree(c,indent + " +-- ")
637 else:
638 dump_tree(c,indent + " |-- ")
641 def dump_tree_script__crawler(n, subtree=""):
642 "Helper for dump_tree_script. See that comment."
644 # skip printing the root node.
645 if n.name != root_node_name:
646 n.print_script(subtree=subtree)
648 for child in n.children or []:
649 dump_tree_script__crawler(child, subtree)
652 def dump_tree_script(n, subtree=""):
653 """Print out a python script representation of the structure of the tree
654 in the SVNTreeNode N. Print only those nodes whose path string starts
655 with the string SUBTREE, and print only the part of the path string
656 that remains after SUBTREE."""
658 print "svntest.wc.State('%s', {" % subtree
659 dump_tree_script__crawler(n, subtree)
660 print "})"
663 ###################################################################
664 ###################################################################
665 # PARSERS that return trees made of SVNTreeNodes....
668 ###################################################################
669 # Build an "expected" static tree from a list of lists
672 # Create a list of lists, of the form:
674 # [ [path, contents, props, atts], ... ]
676 # and run it through this parser. PATH is a string, a path to the
677 # object. CONTENTS is either a string or None, and PROPS and ATTS are
678 # populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be
679 # attached to the basename-node of the associated PATH.
681 def build_generic_tree(nodelist):
682 "Given a list of lists of a specific format, return a tree."
684 root = SVNTreeNode(root_node_name)
686 for list in nodelist:
687 new_branch = create_from_path(list[0], list[1], list[2], list[3])
688 root.add_child(new_branch)
690 return root
693 ####################################################################
694 # Build trees from different kinds of subcommand output.
697 # Parse co/up output into a tree.
699 # Tree nodes will contain no contents, a 'status' att, and a
700 # 'writelocked' att.
702 def build_tree_from_checkout(lines, include_skipped=1):
703 "Return a tree derived by parsing the output LINES from 'co' or 'up'."
705 root = SVNTreeNode(root_node_name)
706 rm1 = re.compile ('^([RMAGCUDE_ ][MAGCUDE_ ])([B ])\s+(.+)')
707 if include_skipped:
708 rm2 = re.compile ('^(Restored|Skipped)\s+\'(.+)\'')
709 else:
710 rm2 = re.compile ('^(Restored)\s+\'(.+)\'')
712 for line in lines:
713 match = rm1.search(line)
714 if match and match.groups():
715 new_branch = create_from_path(match.group(3), None, {},
716 {'status' : match.group(1)})
717 root.add_child(new_branch)
718 else:
719 match = rm2.search(line)
720 if match and match.groups():
721 new_branch = create_from_path(match.group(2), None, {},
722 {'verb' : match.group(1)})
723 root.add_child(new_branch)
725 return root
728 # Parse ci/im output into a tree.
730 # Tree nodes will contain no contents, and only one 'verb' att.
732 def build_tree_from_commit(lines):
733 "Return a tree derived by parsing the output LINES from 'ci' or 'im'."
735 # Lines typically have a verb followed by whitespace then a path.
736 root = SVNTreeNode(root_node_name)
737 rm1 = re.compile ('^(\w+( \(bin\))?)\s+(.+)')
738 rm2 = re.compile ('^Transmitting')
740 for line in lines:
741 match = rm2.search(line)
742 if not match:
743 match = rm1.search(line)
744 if match and match.groups():
745 new_branch = create_from_path(match.group(3), None, {},
746 {'verb' : match.group(1)})
747 root.add_child(new_branch)
749 return root
752 # Parse status output into a tree.
754 # Tree nodes will contain no contents, and these atts:
756 # 'status', 'wc_rev',
757 # ... and possibly 'locked', 'copied', 'switched',
758 # 'writelocked' and 'treeconflict',
759 # IFF columns non-empty.
762 def build_tree_from_status(lines):
763 "Return a tree derived by parsing the output LINES from 'st -vuq'."
765 root = SVNTreeNode(root_node_name)
767 # 'status -v' output looks like this:
769 # "%c%c%c%c%c%c%c %c %6s %6s %-12s %s\n"
771 # (Taken from 'print_status' in subversion/svn/status.c.)
773 # Here are the parameters. The middle number in parens is the
774 # match.group(), followed by a brief description of the field:
776 # - text status (1) (single letter)
777 # - prop status (1) (single letter)
778 # - wc-lockedness flag (2) (single letter: "L" or " ")
779 # - copied flag (3) (single letter: "+" or " ")
780 # - switched flag (4) (single letter: "S" or " ")
781 # - repos lock status (5) (single letter: "K", "O", "B", "T", " ")
782 # - tree conflict flag (6) (single letter: "C" or " ")
784 # [one space]
786 # - out-of-date flag (7) (single letter: "*" or " ")
788 # [three spaces]
790 # - working revision (8) (either digits or "-")
792 # [one space]
794 # - last-changed revision (9) (either digits or "?")
796 # [one space]
798 # - last author (10) (string of non-whitespace characters)
800 # [one space]
802 # - path (11) (string of characters until newline)
804 # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU
805 rm = re.compile('^([!MACDRUG_ ][MACDRUG_ ])([L ])([+ ])([S ])([KOBT ])([C ]) ([* ]) [^0-9-]*(\d+|-|\?) +(\d|-|\?)+ +(\S+) +(.+)')
806 for line in lines:
808 # Quit when we hit an externals status announcement (### someday we can fix
809 # the externals tests to expect the additional flood of externals status
810 # data).
811 if re.match(r'^Performing', line):
812 break
814 match = rm.search(line)
815 if match and match.groups():
816 if match.group(10) != '-': # ignore items that only exist on repos
817 atthash = {'status' : match.group(1),
818 'wc_rev' : match.group(8)}
819 if match.group(2) != ' ':
820 atthash['locked'] = match.group(2)
821 if match.group(3) != ' ':
822 atthash['copied'] = match.group(3)
823 if match.group(4) != ' ':
824 atthash['switched'] = match.group(4)
825 if match.group(5) != ' ':
826 atthash['writelocked'] = match.group(5)
827 atthash['treeconflict'] = match.group(6)
828 new_branch = create_from_path(match.group(11), None, {}, atthash)
830 root.add_child(new_branch)
832 return root
835 # Parse merge "skipped" output
837 def build_tree_from_skipped(lines):
839 root = SVNTreeNode(root_node_name)
840 ### Will get confused by spaces in the filename
841 rm = re.compile ("^Skipped.* '([^ ]+)'\n")
843 for line in lines:
844 match = rm.search(line)
845 if match and match.groups():
846 new_branch = create_from_path(match.group(1))
847 root.add_child(new_branch)
849 return root
851 def build_tree_from_diff_summarize(lines):
852 "Build a tree from output of diff --summarize"
853 root = SVNTreeNode(root_node_name)
854 rm = re.compile ("^([MAD ][M ]) (.+)\n")
856 for line in lines:
857 match = rm.search(line)
858 if match and match.groups():
859 new_branch = create_from_path(match.group(2),
860 atts={'status': match.group(1)})
861 root.add_child(new_branch)
863 return root
865 ####################################################################
866 # Build trees by looking at the working copy
869 # The reason the 'load_props' flag is off by default is because it
870 # creates a drastic slowdown -- we spawn a new 'svn proplist'
871 # process for every file and dir in the working copy!
874 def build_tree_from_wc(wc_path, load_props=0, ignore_svn=1):
875 """Takes WC_PATH as the path to a working copy. Walks the tree below
876 that path, and creates the tree based on the actual found
877 files. If IGNORE_SVN is true, then exclude SVN admin dirs from the tree.
878 If LOAD_PROPS is true, the props will be added to the tree."""
880 root = SVNTreeNode(root_node_name, None)
882 # if necessary, store the root dir's props in a new child node '.'.
883 if load_props:
884 props = get_props(wc_path)
885 if props:
886 root_dir_node = SVNTreeNode(os.path.basename('.'), None, None, props)
887 root.add_child(root_dir_node)
889 # Walk the tree recursively
890 handle_dir(os.path.normpath(wc_path), root, load_props, ignore_svn)
892 return root
894 ### End of file.