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 # Licensed to the Apache Software Foundation (ASF) under one
9 # or more contributor license agreements. See the NOTICE file
10 # distributed with this work for additional information
11 # regarding copyright ownership. The ASF licenses this file
12 # to you under the Apache License, Version 2.0 (the
13 # "License"); you may not use this file except in compliance
14 # with the License. You may obtain a copy of the License at
16 # http://www.apache.org/licenses/LICENSE-2.0
18 # Unless required by applicable law or agreed to in writing,
19 # software distributed under the License is distributed on an
20 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21 # KIND, either express or implied. See the License for the
22 # specific language governing permissions and limitations
24 ######################################################################
29 if sys
.version_info
[0] >= 3:
31 from io
import StringIO
34 from StringIO
import StringIO
35 from xml
.dom
.minidom
import parseString
42 # All tree exceptions should inherit from SVNTreeError
43 class SVNTreeError(svntest
.Failure
):
44 "Exception raised if you screw up in the tree module."
47 class SVNTreeUnequal(SVNTreeError
):
48 "Exception raised if two trees are unequal."
51 class SVNTypeMismatch(SVNTreeError
):
52 "Exception raised if one node is file and other is dir"
55 #========================================================================
57 # ===> Overview of our Datastructures <===
59 # The general idea here is that many, many things can be represented by
62 # - a working copy's structure and contents
63 # - the output of 'svn status'
64 # - the output of 'svn checkout/update'
65 # - the output of 'svn commit'
67 # The idea is that a test function creates a "expected" tree of some
68 # kind, and is then able to compare it to an "actual" tree that comes
69 # from running the Subversion client. This is what makes a test
70 # automated; if an actual and expected tree match exactly, then the test
71 # has passed. (See compare_trees() below.)
73 # The SVNTreeNode class is the fundamental data type used to build tree
74 # structures. The class contains a method for "dropping" a new node
75 # into an ever-growing tree structure. (See also create_from_path()).
77 # We have four parsers in this file for the four use cases listed above:
78 # each parser examines some kind of input and returns a tree of
79 # SVNTreeNode objects. (See build_tree_from_checkout(),
80 # build_tree_from_commit(), build_tree_from_status(), and
81 # build_tree_from_wc()). These trees are the "actual" trees that result
82 # from running the Subversion client.
84 # Also necessary, of course, is a convenient way for a test to create an
85 # "expected" tree. The test *could* manually construct and link a bunch
86 # of SVNTreeNodes, certainly. But instead, all the tests are using the
87 # build_generic_tree() routine instead.
89 # build_generic_tree() takes a specially-formatted list of lists as
90 # input, and returns a tree of SVNTreeNodes. The list of lists has this
93 # [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}],
98 # You can see that each item in the list essentially defines an
99 # SVNTreeNode. build_generic_tree() instantiates a SVNTreeNode for each
100 # item, and then drops it into a tree by parsing each item's full path.
102 # So a typical test routine spends most of its time preparing lists of
103 # this format and sending them to build_generic_tree(), rather than
104 # building the "expected" trees directly.
106 # ### Note: in the future, we'd like to remove this extra layer of
107 # ### abstraction. We'd like the SVNTreeNode class to be more
108 # ### directly programmer-friendly, providing a number of accessor
109 # ### routines, so that tests can construct trees directly.
111 # The first three fields of each list-item are self-explanatory. It's
112 # the fourth field, the "attribute" hash, that needs some explanation.
113 # The att-hash is used to place extra information about the node itself,
114 # depending on the parsing context:
116 # - in the 'svn co/up' use-case, each line of output starts with two
117 # characters from the set of (A, D, G, U, C, _) or 'Restored'. The
118 # status code is stored in a attribute named 'status'. In the case
119 # of a restored file, the word 'Restored' is stored in an attribute
122 # - in the 'svn ci/im' use-case, each line of output starts with one
123 # of the words (Adding, Deleting, Sending). This verb is stored in
124 # an attribute named 'verb'.
126 # - in the 'svn status' use-case (which is always run with the -v
127 # (--verbose) flag), each line of output contains a working revision
128 # number and a two-letter status code similar to the 'svn co/up'
129 # case. This information is stored in attributes named 'wc_rev'
130 # and 'status'. The repository revision is also printed, but it
133 # - in the working-copy use-case, the att-hash is ignored.
136 # Finally, one last explanation: the file 'actions.py' contain a number
137 # of helper routines named 'run_and_verify_FOO'. These routines take
138 # one or more "expected" trees as input, then run some svn subcommand,
139 # then push the output through an appropriate parser to derive an
140 # "actual" tree. Then it runs compare_trees() and raises an exception
141 # on failure. This is why most tests typically end with a call to
142 # run_and_verify_FOO().
146 #========================================================================
150 # If CHILDREN is None, then the node is a file. Otherwise, CHILDREN
151 # is a list of the nodes making up that directory's children.
153 # NAME is simply the name of the file or directory. CONTENTS is a
154 # string that contains the file's contents (if a file), PROPS are
155 # properties attached to files or dirs, and ATTS is a dictionary of
156 # other metadata attached to the node.
160 def __init__(self
, name
, children
=None, contents
=None, props
={}, atts
={}):
162 self
.children
= children
163 self
.contents
= contents
168 # TODO: Check to make sure contents and children are mutually exclusive
170 def add_child(self
, newchild
):
171 child_already_exists
= 0
172 if self
.children
is None: # if you're a file,
173 self
.children
= [] # become an empty dir.
175 for a
in self
.children
:
176 if a
.name
== newchild
.name
:
177 child_already_exists
= 1
180 if child_already_exists
:
181 if newchild
.children
is None:
182 # this is the 'end' of the chain, so copy any content here.
183 a
.contents
= newchild
.contents
184 a
.props
= newchild
.props
185 a
.atts
= newchild
.atts
186 a
.path
= os
.path
.join(self
.path
, newchild
.name
)
188 # try to add dangling children to your matching node
189 for i
in newchild
.children
:
192 self
.children
.append(newchild
)
193 newchild
.path
= os
.path
.join(self
.path
, newchild
.name
)
196 def pprint(self
, stream
= sys
.stdout
):
197 "Pretty-print the meta data for this node to STREAM."
198 stream
.write(" * Node name: %s\n" % self
.name
)
199 stream
.write(" Path: %s\n" % self
.path
)
200 mime_type
= self
.props
.get("svn:mime-type")
201 if not mime_type
or mime_type
.startswith("text/"):
202 if self
.children
is not None:
203 stream
.write(" Contents: N/A (node is a directory)\n")
205 stream
.write(" Contents: %s\n" % self
.contents
)
207 stream
.write(" Contents: %d bytes (binary)\n" % len(self
.contents
))
208 stream
.write(" Properties: %s\n" % self
.props
)
209 stream
.write(" Attributes: %s\n" % self
.atts
)
210 ### FIXME: I'd like to be able to tell the difference between
211 ### self.children is None (file) and self.children == [] (empty
212 ### directory), but it seems that most places that construct
213 ### SVNTreeNode objects don't even try to do that. --xbc
215 ### See issue #1611 about this problem. -kfogel
216 if self
.children
is not None:
217 stream
.write(" Children: %s\n" % len(self
.children
))
219 stream
.write(" Children: None (node is probably a file)\n")
222 def get_printable_path(self
):
223 """Remove some occurrences of root_node_name = "__SVN_ROOT_NODE",
224 it is in the way when matching for a subtree, and looks bad."""
226 if path
.startswith(root_node_name
+ os
.sep
):
227 path
= path
[len(root_node_name
+ os
.sep
):]
230 def print_script(self
, stream
= sys
.stdout
, subtree
= "", prepend
="\n ",
231 drop_empties
= True):
232 """Python-script-print the meta data for this node to STREAM.
233 Print only those nodes whose path string starts with the string SUBTREE,
234 and print only the part of the path string that remains after SUBTREE.
235 PREPEND is a string prepended to each node printout (does the line
236 feed if desired, don't include a comma in PREPEND).
237 If DROP_EMPTIES is true, all dir nodes that have no data set in them
238 (no props, no atts) and that have children (so they are included
239 implicitly anyway) are not printed.
240 Return 1 if this node was printed, 0 otherwise (added up by
241 dump_tree_script())"""
243 # figure out if this node would be obsolete to print.
244 if drop_empties
and len(self
.props
) < 1 and len(self
.atts
) < 1 and \
245 self
.contents
is None and self
.children
is not None:
248 path
= self
.get_printable_path()
250 # remove the subtree path, skip this node if necessary.
251 if path
.startswith(subtree
):
252 path
= path
[len(subtree
):]
256 if path
.startswith(os
.sep
):
260 line
+= "%-20s: Item(" % ("'%s'" % path
.replace(os
.sep
, '/'))
263 mime_type
= self
.props
.get("svn:mime-type")
264 if not mime_type
or mime_type
.startswith("text/"):
265 if self
.contents
is not None:
266 # Escape some characters for nicer script and readability.
267 # (This is error output. I guess speed is no consideration here.)
268 line
+= "contents=\"%s\"" % (self
.contents
272 .replace('\t','\\t'))
275 line
+= 'content is binary data'
284 for name
in self
.props
:
287 line
+= "'%s':'%s'" % (name
, self
.props
[name
])
293 for name
in self
.atts
:
296 line
+= "%s='%s'" % (name
, self
.atts
[name
])
300 stream
.write("%s" % line
)
311 def __cmp__(self
, other
):
312 """Define a simple ordering of two nodes without regard to their full
313 path (i.e. position in the tree). This can be used for sorting the
314 children within a directory."""
315 return cmp(self
.name
, other
.name
)
317 def as_state(self
, prefix
=None):
318 """Return an svntest.wc.State instance that is equivalent to this tree."""
320 if self
.path
== root_node_name
:
321 assert prefix
is None
324 if root
is not self
: # don't prepend ROOT_NODE_NAME
325 wc_dir
= os
.path
.join(wc_dir
, root
.name
)
326 if root
.contents
or root
.props
or root
.atts
:
328 if not root
.children
or len(root
.children
) != 1:
330 root
= root
.children
[0]
331 state
= svntest
.wc
.State(wc_dir
, { })
332 if root
.contents
or root
.props
or root
.atts
:
333 state
.add({'': root
.as_item()})
336 assert prefix
is not None
339 if path
.startswith(root_node_name
):
340 path
= path
[len(root_node_name
)+1:]
341 # prefix should only be set on a recursion, which means a child,
342 # which means this path better not be the same as the prefix.
343 assert path
!= prefix
, 'not processing a child of the root'
346 assert path
[:l
] == prefix
, \
347 '"%s" is not a prefix of "%s"' % (prefix
, path
)
348 # return the portion after the separator
349 path
= path
[l
+1:].replace(os
.sep
, '/')
351 state
= svntest
.wc
.State('', {
356 for child
in root
.children
:
357 state
.add_state('', child
.as_state(prefix
))
362 return svntest
.wc
.StateItem(self
.contents
,
364 self
.atts
.get('status'),
365 self
.atts
.get('verb'),
366 self
.atts
.get('wc_rev'),
367 self
.atts
.get('locked'),
368 self
.atts
.get('copied'),
369 self
.atts
.get('switched'),
370 self
.atts
.get('writelocked'),
371 self
.atts
.get('treeconflict'))
373 def recurse(self
, function
):
375 results
+= [ function(self
) ]
377 for child
in self
.children
:
378 results
+= child
.recurse(function
)
381 def find_node(self
, path
):
382 if self
.get_printable_path() == path
:
385 for child
in self
.children
:
386 result
= child
.find_node(path
)
391 # reserved name of the root of the tree
392 root_node_name
= "__SVN_ROOT_NODE"
396 def add_elements_as_path(top_node
, element_list
):
397 """Add the elements in ELEMENT_LIST as if they were a single path
400 # The idea of this function is to take a list like so:
401 # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree
406 # where 1 -> 2 means 2 is a child of 1.
410 for i
in element_list
:
411 new_node
= SVNTreeNode(i
, None)
412 prev_node
.add_child(new_node
)
416 def compare_atts(a
, b
):
417 """Compare two dictionaries of attributes, A (actual) and B (expected).
418 If the attribute 'treeconflict' in B is missing or is 'None', ignore it.
419 Return 0 if the same, 1 otherwise."""
422 # Remove any attributes to ignore.
423 for att
in ['treeconflict']:
424 if (att
not in b
) or (b
[att
] is None):
433 # Helper for compare_trees
434 def compare_file_nodes(a
, b
):
435 """Compare two nodes, A (actual) and B (expected). Compare their names,
436 contents, properties and attributes, ignoring children. Return 0 if the
437 same, 1 otherwise."""
440 if a
.contents
!= b
.contents
:
442 if a
.props
!= b
.props
:
444 return compare_atts(a
.atts
, b
.atts
)
447 # Internal utility used by most build_tree_from_foo() routines.
449 # (Take the output and .add_child() it to a root node.)
451 def create_from_path(path
, contents
=None, props
={}, atts
={}):
452 """Create and return a linked list of treenodes, given a PATH
453 representing a single entry into that tree. CONTENTS and PROPS are
454 optional arguments that will be deposited in the tail node."""
456 # get a list of all the names in the path
457 # each of these will be a child of the former
459 path
= path
.replace(os
.sep
, "/")
460 elements
= path
.split("/")
461 if len(elements
) == 0:
462 ### we should raise a less generic error here. which?
467 # if this is Windows: if the path contains a drive name (X:), make it
470 m
= re
.match("([a-zA-Z]:)(.+)", elements
[0])
472 root_node
= SVNTreeNode(m
.group(1), None)
473 elements
[0] = m
.group(2)
474 add_elements_as_path(root_node
, elements
[0:])
477 root_node
= SVNTreeNode(elements
[0], None)
478 add_elements_as_path(root_node
, elements
[1:])
480 # deposit contents in the very last node.
483 if node
.children
is None:
484 node
.contents
= contents
488 node
= node
.children
[0]
493 eol_re
= re
.compile(r
'(\r\n|\r)')
495 # helper for build_tree_from_wc()
496 def get_props(paths
):
497 """Return a hash of hashes of props for PATHS, using the svn client. Convert
498 each embedded end-of-line to a single LF character."""
500 # It's not kosher to look inside .svn/ and try to read the internal
501 # property storage format. Instead, we use 'svn proplist'. After
502 # all, this is the only way the user can retrieve them, so we're
503 # respecting the black-box paradigm.
506 exit_code
, output
, errput
= svntest
.main
.run_svn(1,
512 output
= (line
for line
in output
if not line
.startswith('DBG:'))
513 dom
= parseString(''.join(output
))
514 target_nodes
= dom
.getElementsByTagName('target')
515 for target_node
in target_nodes
:
516 filename
= target_node
.attributes
['path'].nodeValue
518 for property_node
in target_node
.getElementsByTagName('property'):
519 name
= property_node
.attributes
['name'].nodeValue
520 if property_node
.hasChildNodes():
521 text_node
= property_node
.firstChild
522 value
= text_node
.nodeValue
526 encoding
= property_node
.attributes
['encoding'].nodeValue
527 if encoding
== 'base64':
528 value
= base64
.b64decode(value
)
530 raise Exception("Unknown encoding '%s' for file '%s' property '%s'"
531 % (encoding
, filename
, name
,))
534 # If the property value contained a CR, or if under Windows an
535 # "svn:*" property contains a newline, then the XML output
536 # contains a CR character XML-encoded as ' '. The XML
537 # parser converts it back into a CR character. So again convert
538 # all end-of-line variants into a single LF:
539 value
= eol_re
.sub('\n', value
)
540 file_props
[name
] = value
541 files
[filename
] = file_props
547 ### ridiculous function. callers should do this one line themselves.
549 "Return a string with the textual contents of a file at PATH."
552 if not os
.path
.isfile(path
):
555 return open(path
, 'r').read()
558 def get_child(node
, name
):
559 """If SVNTreeNode NODE contains a child named NAME, return child;
560 else, return None. If SVNTreeNode is not a directory, exit completely."""
561 if node
.children
== None:
562 print("Error: Foolish call to get_child.")
564 for n
in node
.children
:
570 # Helper for compare_trees
571 def default_singleton_handler(node
, description
):
572 """Print SVNTreeNode NODE's name, describing it with the string
573 DESCRIPTION, then raise SVNTreeUnequal."""
574 print("Couldn't find node '%s' in %s tree" % (node
.name
, description
))
578 # A test helper function implementing the singleton_handler_a API.
579 def detect_conflict_files(node
, extra_files
):
580 """NODE has been discovered, an extra file on disk. Verify that it
581 matches one of the regular expressions in the EXTRA_FILES list. If
582 it matches, remove the match from the list. If it doesn't match,
583 raise an exception."""
585 for pattern
in extra_files
:
586 mo
= re
.match(pattern
, node
.name
)
588 extra_files
.pop(extra_files
.index(pattern
)) # delete pattern from list
591 msg
= "Encountered unexpected disk path '" + node
.name
+ "'"
594 raise SVNTreeUnequal(msg
)
596 ###########################################################################
597 ###########################################################################
598 # EXPORTED ROUTINES ARE BELOW
601 # Main tree comparison routine!
603 def compare_trees(label
,
605 singleton_handler_a
= None,
607 singleton_handler_b
= None,
609 """Compare SVNTreeNodes A (actual) and B (expected), expressing
610 differences using FUNC_A and FUNC_B. FUNC_A and FUNC_B are
611 functions of two arguments (a SVNTreeNode and a context baton), and
612 may raise exception SVNTreeUnequal, in which case they use the
613 string LABEL to describe the error (their return value is ignored).
614 LABEL is typically "output", "disk", "status", or some other word
615 that labels the trees being compared.
617 If A and B are both files, then return if their contents,
618 properties, and names are all the same; else raise a SVNTreeUnequal.
619 If A is a file and B is a directory, raise a SVNTreeUnequal; same
620 vice-versa. If both are directories, then for each entry that
621 exists in both, call compare_trees on the two entries; otherwise, if
622 the entry exists only in A, invoke FUNC_A on it, and likewise for
625 def display_nodes(a
, b
):
626 'Display two nodes, expected and actual.'
627 print("=============================================================")
628 print("Expected '%s' and actual '%s' in %s tree are different!"
629 % (b
.name
, a
.name
, label
))
630 print("=============================================================")
631 print("EXPECTED NODE TO BE:")
632 print("=============================================================")
634 print("=============================================================")
635 print("ACTUAL NODE FOUND:")
636 print("=============================================================")
639 # Setup singleton handlers
640 if singleton_handler_a
is None:
641 singleton_handler_a
= default_singleton_handler
642 a_baton
= "expected " + label
643 if singleton_handler_b
is None:
644 singleton_handler_b
= default_singleton_handler
645 b_baton
= "actual " + label
648 # A and B are both files.
649 if (a
.children
is None) and (b
.children
is None):
650 if compare_file_nodes(a
, b
):
653 # One is a file, one is a directory.
654 elif (((a
.children
is None) and (b
.children
is not None))
655 or ((a
.children
is not None) and (b
.children
is None))):
657 raise SVNTypeMismatch
658 # They're both directories.
660 # First, compare the directories' two hashes.
661 if (a
.props
!= b
.props
) or compare_atts(a
.atts
, b
.atts
):
666 # For each child of A, check and see if it's in B. If so, run
667 # compare_trees on the two children and add b's child to
668 # accounted_for. If not, run FUNC_A on the child. Next, for each
669 # child of B, check and see if it's in accounted_for. If it is,
670 # do nothing. If not, run FUNC_B on it.
671 for a_child
in a
.children
:
672 b_child
= get_child(b
, a_child
.name
)
674 accounted_for
.append(b_child
)
675 compare_trees(label
, a_child
, b_child
,
676 singleton_handler_a
, a_baton
,
677 singleton_handler_b
, b_baton
)
679 singleton_handler_a(a_child
, a_baton
)
680 for b_child
in b
.children
:
681 if b_child
not in accounted_for
:
682 singleton_handler_b(b_child
, b_baton
)
683 except SVNTypeMismatch
:
684 print('Unequal Types: one Node is a file, the other is a directory')
687 print("Error: unequal number of children")
689 except SVNTreeUnequal
:
690 if a
.name
!= root_node_name
:
691 print("Unequal at node %s" % a
.name
)
696 # Visually show a tree's structure
698 def dump_tree(n
,indent
=""):
699 """Print out a nice representation of the structure of the tree in
700 the SVNTreeNode N. Prefix each line with the string INDENT."""
702 # Code partially stolen from Dave Beazley
703 tmp_children
= sorted(n
.children
or [])
705 if n
.name
== root_node_name
:
706 print("%s%s" % (indent
, "ROOT"))
708 print("%s%s" % (indent
, n
.name
))
710 indent
= indent
.replace("-", " ")
711 indent
= indent
.replace("+", " ")
712 for i
in range(len(tmp_children
)):
714 if i
== len(tmp_children
)-1:
715 dump_tree(c
,indent
+ " +-- ")
717 dump_tree(c
,indent
+ " |-- ")
720 def dump_tree_script__crawler(n
, subtree
="", stream
=sys
.stdout
):
721 "Helper for dump_tree_script. See that comment."
724 # skip printing the root node.
725 if n
.name
!= root_node_name
:
726 count
+= n
.print_script(stream
, subtree
)
728 for child
in n
.children
or []:
729 count
+= dump_tree_script__crawler(child
, subtree
, stream
)
734 def dump_tree_script(n
, subtree
="", stream
=sys
.stdout
, wc_varname
='wc_dir'):
735 """Print out a python script representation of the structure of the tree
736 in the SVNTreeNode N. Print only those nodes whose path string starts
737 with the string SUBTREE, and print only the part of the path string
738 that remains after SUBTREE.
739 The result is printed to STREAM.
740 The WC_VARNAME is inserted in the svntest.wc.State(wc_dir,{}) call
741 that is printed out (this is used by factory.py)."""
743 stream
.write("svntest.wc.State(" + wc_varname
+ ", {")
744 count
= dump_tree_script__crawler(n
, subtree
, stream
)
750 ###################################################################
751 ###################################################################
752 # PARSERS that return trees made of SVNTreeNodes....
755 ###################################################################
756 # Build an "expected" static tree from a list of lists
759 # Create a list of lists, of the form:
761 # [ [path, contents, props, atts], ... ]
763 # and run it through this parser. PATH is a string, a path to the
764 # object. CONTENTS is either a string or None, and PROPS and ATTS are
765 # populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be
766 # attached to the basename-node of the associated PATH.
768 def build_generic_tree(nodelist
):
769 "Given a list of lists of a specific format, return a tree."
771 root
= SVNTreeNode(root_node_name
)
773 for list in nodelist
:
774 new_branch
= create_from_path(list[0], list[1], list[2], list[3])
775 root
.add_child(new_branch
)
780 ####################################################################
781 # Build trees from different kinds of subcommand output.
784 # Parse co/up output into a tree.
786 # Tree nodes will contain no contents, a 'status' att, and a
787 # 'treeconflict' att.
789 def build_tree_from_checkout(lines
, include_skipped
=True):
790 "Return a tree derived by parsing the output LINES from 'co' or 'up'."
792 return svntest
.wc
.State
.from_checkout(lines
, include_skipped
).old_tree()
795 # Parse ci/im output into a tree.
797 # Tree nodes will contain no contents, and only one 'verb' att.
799 def build_tree_from_commit(lines
):
800 "Return a tree derived by parsing the output LINES from 'ci' or 'im'."
802 return svntest
.wc
.State
.from_commit(lines
).old_tree()
805 # Parse status output into a tree.
807 # Tree nodes will contain no contents, and these atts:
809 # 'status', 'wc_rev',
810 # ... and possibly 'locked', 'copied', 'switched',
811 # 'writelocked' and 'treeconflict',
812 # IFF columns non-empty.
815 def build_tree_from_status(lines
):
816 "Return a tree derived by parsing the output LINES from 'st -vuq'."
818 return svntest
.wc
.State
.from_status(lines
).old_tree()
821 # Parse merge "skipped" output
823 def build_tree_from_skipped(lines
):
825 return svntest
.wc
.State
.from_skipped(lines
).old_tree()
828 def build_tree_from_diff_summarize(lines
):
829 "Build a tree from output of diff --summarize"
831 return svntest
.wc
.State
.from_summarize(lines
).old_tree()
834 ####################################################################
835 # Build trees by looking at the working copy
838 # The reason the 'load_props' flag is off by default is because it
839 # creates a drastic slowdown -- we spawn a new 'svn proplist'
840 # process for every file and dir in the working copy!
843 def build_tree_from_wc(wc_path
, load_props
=0, ignore_svn
=1):
844 """Takes WC_PATH as the path to a working copy. Walks the tree below
845 that path, and creates the tree based on the actual found
846 files. If IGNORE_SVN is true, then exclude SVN admin dirs from the tree.
847 If LOAD_PROPS is true, the props will be added to the tree."""
849 return svntest
.wc
.State
.from_wc(wc_path
, load_props
, ignore_svn
).old_tree()