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 ######################################################################
22 import main
# the general svntest routines in this module.
23 from svntest
import Failure
27 # All tree exceptions should inherit from SVNTreeError
28 class SVNTreeError(Failure
):
29 "Exception raised if you screw up in the tree module."
32 class SVNTreeUnequal(SVNTreeError
):
33 "Exception raised if two trees are unequal."
36 class SVNTreeIsNotDirectory(SVNTreeError
):
37 "Exception raised if get_child is passed a file."
40 class SVNTypeMismatch(SVNTreeError
):
41 "Exception raised if one node is file and other is dir"
44 #========================================================================
46 # ===> Overview of our Datastructures <===
48 # The general idea here is that many, many things can be represented by
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
82 # [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}],
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
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
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 #========================================================================
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.
149 def __init__(self
, name
, children
=None, contents
=None, props
={}, atts
={}):
151 self
.children
= children
152 self
.contents
= contents
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.
164 for a
in self
.children
:
165 if a
.name
== newchild
.name
:
166 child_already_exists
= 1
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
)
177 # try to add dangling children to your matching node
178 for i
in newchild
.children
:
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")
194 stream
.write(" Contents: %s\n" % self
.contents
)
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
))
208 stream
.write(" Children: None (node is probably a file)\n")
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.
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
):]
228 line
= " %-20s: Item(" % ("'%s'" % path
)
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
240 .replace('\t','\\t'))
243 line
+= 'content is binary data'
252 for name
in self
.props
:
255 line
+= "'%s':'%s'" % (name
, self
.props
[name
])
261 for name
in self
.atts
:
264 line
+= "%s='%s'" % (name
, self
.atts
[name
])
268 stream
.write("%s\n" % line
)
274 s
= StringIO
.StringIO()
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"
291 def add_elements_as_path(top_node
, element_list
):
292 """Add the elements in ELEMENT_LIST as if they were a single path
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
301 # where 1 -> 2 means 2 is a child of 1.
305 for i
in element_list
:
306 new_node
= SVNTreeNode(i
, None)
307 prev_node
.add_child(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."""
317 # Remove any attributes to ignore.
318 for att
in ['treeconflict']:
319 if (att
not in b
) or (b
[att
] is None):
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."""
335 if a
.contents
!= b
.contents
:
337 if a
.props
!= b
.props
:
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
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?
362 # if this is Windows: if the path contains a drive name (X:), make it
365 m
= re
.match("([a-zA-Z]:)(.+)", elements
[0])
367 root_node
= SVNTreeNode(m
.group(1), None)
368 elements
[0] = m
.group(2)
369 add_elements_as_path(root_node
, elements
[0:])
372 root_node
= SVNTreeNode(elements
[0], None)
373 add_elements_as_path(root_node
, elements
[1:])
375 # deposit contents in the very last node.
378 if node
.children
is None:
379 node
.contents
= contents
383 node
= node
.children
[0]
388 # helper for handle_dir(), which is a helper for build_tree_from_wc()
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.
399 exit_code
, output
, errput
= main
.run_svn(1, "proplist", path
, "--verbose")
403 line
= line
.rstrip('\r\n') # ignore stdout's EOL sequence
405 if line
.startswith('Properties on '):
408 elif line
.startswith(' '):
409 # It's (part of) the value
410 props
[name
] += line
[4:] + '\n' # strip the indentation
412 elif line
.startswith(' '):
414 name
= line
[2:] # strip the indentation
418 raise "Malformed line from proplist: '"+line
+"'"
420 # Strip, from each property value, the final new-line that we added
422 props
[name
] = props
[name
][:-1]
427 # helper for handle_dir(), which helps build_tree_from_wc()
429 "Return a string with the textual contents of a file at PATH."
432 if not os
.path
.isfile(path
):
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
)
449 # put dirs and files in their own lists, and remove SVN dirs
451 f
= os
.path
.join(path
, f
)
452 if (os
.path
.isdir(f
) and os
.path
.basename(f
) != main
.get_admin_name()):
454 elif os
.path
.isfile(f
):
457 # add each file as a child of CURRENT_PARENT
459 fcontents
= get_text(f
)
461 fprops
= get_props(f
)
464 current_parent
.add_child(SVNTreeNode(os
.path
.basename(f
), None,
467 # for each subdir, create a node, walk its tree, add it as a child
470 dprops
= get_props(d
)
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
:
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
))
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
)
507 extra_files
.pop(extra_files
.index(pattern
)) # delete pattern from list
510 msg
= "Encountered unexpected disk path '" + node
.name
+ "'"
513 raise SVNTreeUnequal(msg
)
515 ###########################################################################
516 ###########################################################################
517 # EXPORTED ROUTINES ARE BELOW
520 # Main tree comparison routine!
522 def compare_trees(label
,
524 singleton_handler_a
= None,
526 singleton_handler_b
= 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
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("=============================================================")
553 print("=============================================================")
554 print("ACTUAL NODE FOUND:")
555 print("=============================================================")
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
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
):
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))):
576 raise SVNTypeMismatch
577 # They're both directories.
579 # First, compare the directories' two hashes.
580 if (a
.props
!= b
.props
) or compare_atts(a
.atts
, b
.atts
):
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
)
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
)
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')
605 except SVNTreeIsNotDirectory
:
606 print("Error: Foolish call to get_child.")
609 print("Error: unequal number of children")
611 except SVNTreeUnequal
:
612 if a
.name
!= root_node_name
:
613 print("Unequal at node %s" % a
.name
)
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 []
628 if n
.name
== root_node_name
:
629 print("%s%s" % (indent
, "ROOT"))
631 print("%s%s" % (indent
, n
.name
))
633 indent
= indent
.replace("-", " ")
634 indent
= indent
.replace("+", " ")
635 for i
in range(len(tmp_children
)):
637 if i
== len(tmp_children
)-1:
638 dump_tree(c
,indent
+ " +-- ")
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
)
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
)
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+(.+)')
710 rm2
= re
.compile ('^(Restored|Skipped)\s+\'(.+)\'')
712 rm2
= re
.compile ('^(Restored)\s+\'(.+)\'')
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
)
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
)
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')
745 match
= rm2
.search(line
)
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
)
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 " ")
790 # - out-of-date flag (7) (single letter: "*" or " ")
794 # - working revision ('wc_rev') (either digits or "-" or " ")
798 # - last-changed revision (either digits or "?" or " ")
802 # - last author (optional string of non-whitespace
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>.+)$')
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
819 if re
.match(r
'^Performing', line
):
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
)
845 # Parse merge "skipped" output
847 def build_tree_from_skipped(lines
):
849 root
= SVNTreeNode(root_node_name
)
850 rm
= re
.compile ("^Skipped.* '(.+)'\n")
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
)
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")
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
)
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 '.'.
893 props
= get_props(wc_path
)
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
)