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 ######################################################################
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 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)"
194 print >> stream
, " Contents: ", self
.contents
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
)
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.
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
):]
227 line
= " %-20s: Item(" % ("'%s'" % path
)
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
239 .replace('\t','\\t'))
242 line
+= 'content is binary data'
251 for name
in self
.props
:
254 line
+= "'%s':'%s'" % (name
, self
.props
[name
])
260 for name
in self
.atts
:
263 line
+= "%s='%s'" % (name
, self
.atts
[name
])
267 print >> stream
, line
272 s
= StringIO
.StringIO()
277 # reserved name of the root of the tree
278 root_node_name
= "__SVN_ROOT_NODE"
282 def add_elements_as_path(top_node
, element_list
):
283 """Add the elements in ELEMENT_LIST as if they were a single path
286 # The idea of this function is to take a list like so:
287 # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree
292 # where 1 -> 2 means 2 is a child of 1.
296 for i
in element_list
:
297 new_node
= SVNTreeNode(i
, None)
298 prev_node
.add_child(new_node
)
302 # Sorting function -- sort 2 nodes by their names.
303 def node_is_greater(a
, b
):
304 "Sort the names of two nodes."
314 # Helper for compare_trees
315 def compare_file_nodes(a
, b
):
316 """Compare two nodes' names, contents, and properties, ignoring
317 children. Return 0 if the same, 1 otherwise."""
320 if a
.contents
!= b
.contents
:
322 if a
.props
!= b
.props
:
328 # Internal utility used by most build_tree_from_foo() routines.
330 # (Take the output and .add_child() it to a root node.)
332 def create_from_path(path
, contents
=None, props
={}, atts
={}):
333 """Create and return a linked list of treenodes, given a PATH
334 representing a single entry into that tree. CONTENTS and PROPS are
335 optional arguments that will be deposited in the tail node."""
337 # get a list of all the names in the path
338 # each of these will be a child of the former
340 path
= path
.replace(os
.sep
, "/")
341 elements
= path
.split("/")
342 if len(elements
) == 0:
343 ### we should raise a less generic error here. which?
348 # if this is Windows: if the path contains a drive name (X:), make it
351 m
= re
.match("([a-zA-Z]:)(.+)", elements
[0])
353 root_node
= SVNTreeNode(m
.group(1), None)
354 elements
[0] = m
.group(2)
355 add_elements_as_path(root_node
, elements
[0:])
358 root_node
= SVNTreeNode(elements
[0], None)
359 add_elements_as_path(root_node
, elements
[1:])
361 # deposit contents in the very last node.
364 if node
.children
is None:
365 node
.contents
= contents
369 node
= node
.children
[0]
374 # helper for handle_dir(), which is a helper for build_tree_from_wc()
376 """Return a hash of props for PATH, using the svn client. Convert each
377 embedded end-of-line to a single LF character."""
379 # It's not kosher to look inside .svn/ and try to read the internal
380 # property storage format. Instead, we use 'svn proplist'. After
381 # all, this is the only way the user can retrieve them, so we're
382 # respecting the black-box paradigm.
385 exit_code
, output
, errput
= main
.run_svn(1, "proplist", path
, "--verbose")
389 line
= line
.rstrip('\r\n') # ignore stdout's EOL sequence
391 if line
.startswith('Properties on '):
394 elif line
.startswith(' '):
395 # It's (part of) the value
396 props
[name
] += line
[4:] + '\n' # strip the indentation
398 elif line
.startswith(' '):
400 name
= line
[2:] # strip the indentation
404 raise "Malformed line from proplist: '"+line
+"'"
406 # Strip, from each property value, the final new-line that we added
408 props
[name
] = props
[name
][:-1]
413 # helper for handle_dir(), which helps build_tree_from_wc()
415 "Return a string with the textual contents of a file at PATH."
418 if not os
.path
.isfile(path
):
427 # main recursive helper for build_tree_from_wc()
428 def handle_dir(path
, current_parent
, load_props
, ignore_svn
):
430 # get a list of all the files
431 all_files
= os
.listdir(path
)
435 # put dirs and files in their own lists, and remove SVN dirs
437 f
= os
.path
.join(path
, f
)
438 if (os
.path
.isdir(f
) and os
.path
.basename(f
) != main
.get_admin_name()):
440 elif os
.path
.isfile(f
):
443 # add each file as a child of CURRENT_PARENT
445 fcontents
= get_text(f
)
447 fprops
= get_props(f
)
450 current_parent
.add_child(SVNTreeNode(os
.path
.basename(f
), None,
453 # for each subdir, create a node, walk its tree, add it as a child
456 dprops
= get_props(d
)
459 new_dir_node
= SVNTreeNode(os
.path
.basename(d
), None, None, dprops
)
460 current_parent
.add_child(new_dir_node
)
461 handle_dir(d
, new_dir_node
, load_props
, ignore_svn
)
463 def get_child(node
, name
):
464 """If SVNTreeNode NODE contains a child named NAME, return child;
465 else, return None. If SVNTreeNode is not a directory, raise a
466 SVNTreeIsNotDirectory exception"""
467 if node
.children
== None:
468 raise SVNTreeIsNotDirectory
469 for n
in node
.children
:
475 # Helper for compare_trees
476 def default_singleton_handler(node
, description
):
477 """Print SVNTreeNode NODE's name, describing it with the string
478 DESCRIPTION, then raise SVNTreeUnequal."""
479 print "Couldn't find node '%s' in %s tree" % (node
.name
, description
)
483 # A test helper function implementing the singleton_handler_a API.
484 def detect_conflict_files(node
, extra_files
):
485 """NODE has been discovered, an extra file on disk. Verify that it
486 matches one of the regular expressions in the EXTRA_FILES list. If
487 it matches, remove the match from the list. If it doesn't match,
488 raise an exception."""
490 for pattern
in extra_files
:
491 mo
= re
.match(pattern
, node
.name
)
493 extra_files
.pop(extra_files
.index(pattern
)) # delete pattern from list
496 msg
= "Encountered unexpected disk path '" + node
.name
+ "'"
499 raise SVNTreeUnequal(msg
)
501 ###########################################################################
502 ###########################################################################
503 # EXPORTED ROUTINES ARE BELOW
506 # Main tree comparison routine!
508 def compare_trees(label
,
510 singleton_handler_a
= None,
512 singleton_handler_b
= None,
514 """Compare SVNTreeNodes A (actual) and B (expected), expressing
515 differences using FUNC_A and FUNC_B. FUNC_A and FUNC_B are
516 functions of two arguments (a SVNTreeNode and a context baton), and
517 may raise exception SVNTreeUnequal, in which case they use the
518 string LABEL to describe the error (their return value is ignored).
519 LABEL is typically "output", "disk", "status", or some other word
520 that labels the trees being compared.
522 If A and B are both files, then return if their contents,
523 properties, and names are all the same; else raise a SVNTreeUnequal.
524 If A is a file and B is a directory, raise a SVNTreeUnequal; same
525 vice-versa. If both are directories, then for each entry that
526 exists in both, call compare_trees on the two entries; otherwise, if
527 the entry exists only in A, invoke FUNC_A on it, and likewise for
530 def display_nodes(a
, b
):
531 'Display two nodes, expected and actual.'
532 print "============================================================="
533 print "Expected '%s' and actual '%s' in %s tree are different!" \
534 % (b
.name
, a
.name
, label
)
535 print "============================================================="
536 print "EXPECTED NODE TO BE:"
537 print "============================================================="
539 print "============================================================="
540 print "ACTUAL NODE FOUND:"
541 print "============================================================="
544 # Setup singleton handlers
545 if (singleton_handler_a
is None):
546 singleton_handler_a
= default_singleton_handler
547 a_baton
= "expected " + label
548 if (singleton_handler_b
is None):
549 singleton_handler_b
= default_singleton_handler
550 b_baton
= "actual " + label
553 # A and B are both files.
554 if ((a
.children
is None) and (b
.children
is None)):
555 if compare_file_nodes(a
, b
):
558 # One is a file, one is a directory.
559 elif (((a
.children
is None) and (b
.children
is not None))
560 or ((a
.children
is not None) and (b
.children
is None))):
562 raise SVNTypeMismatch
563 # They're both directories.
565 # First, compare the directories' two hashes.
566 if (a
.props
!= b
.props
) or (a
.atts
!= b
.atts
):
571 # For each child of A, check and see if it's in B. If so, run
572 # compare_trees on the two children and add b's child to
573 # accounted_for. If not, run FUNC_A on the child. Next, for each
574 # child of B, check and see if it's in accounted_for. If it is,
575 # do nothing. If not, run FUNC_B on it.
576 for a_child
in a
.children
:
577 b_child
= get_child(b
, a_child
.name
)
579 accounted_for
.append(b_child
)
580 compare_trees(label
, a_child
, b_child
,
581 singleton_handler_a
, a_baton
,
582 singleton_handler_b
, b_baton
)
584 singleton_handler_a(a_child
, a_baton
)
585 for b_child
in b
.children
:
586 if (b_child
not in accounted_for
):
587 singleton_handler_b(b_child
, b_baton
)
588 except SVNTypeMismatch
:
589 print 'Unequal Types: one Node is a file, the other is a directory'
591 except SVNTreeIsNotDirectory
:
592 print "Error: Foolish call to get_child."
595 print "Error: unequal number of children"
597 except SVNTreeUnequal
:
598 if a
.name
!= root_node_name
:
599 print "Unequal at node %s" % a
.name
604 # Visually show a tree's structure
606 def dump_tree(n
,indent
=""):
607 """Print out a nice representation of the structure of the tree in
608 the SVNTreeNode N. Prefix each line with the string INDENT."""
610 # Code partially stolen from Dave Beazley
611 if n
.children
is None:
614 tmp_children
= n
.children
616 if n
.name
== root_node_name
:
617 print "%s%s" % (indent
, "ROOT")
619 print "%s%s" % (indent
, n
.name
)
621 indent
= indent
.replace("-", " ")
622 indent
= indent
.replace("+", " ")
623 for i
in range(len(tmp_children
)):
625 if i
== len(tmp_children
627 dump_tree(c
,indent
+ " +-- ")
629 dump_tree(c
,indent
+ " |-- ")
632 def dump_tree_script__crawler(n
, subtree
=""):
633 "Helper for dump_tree_script. See that comment."
635 # skip printing the root node.
636 if n
.name
!= root_node_name
:
637 n
.print_script(subtree
=subtree
)
639 for child
in n
.children
or []:
640 dump_tree_script__crawler(child
, subtree
)
643 def dump_tree_script(n
, subtree
=""):
644 """Print out a python script representation of the structure of the tree
645 in the SVNTreeNode N. Print only those nodes whose path string starts
646 with the string SUBTREE, and print only the part of the path string
647 that remains after SUBTREE."""
649 print "svntest.wc.State('%s', {" % subtree
650 dump_tree_script__crawler(n
, subtree
)
654 ###################################################################
655 ###################################################################
656 # PARSERS that return trees made of SVNTreeNodes....
659 ###################################################################
660 # Build an "expected" static tree from a list of lists
663 # Create a list of lists, of the form:
665 # [ [path, contents, props, atts], ... ]
667 # and run it through this parser. PATH is a string, a path to the
668 # object. CONTENTS is either a string or None, and PROPS and ATTS are
669 # populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be
670 # attached to the basename-node of the associated PATH.
672 def build_generic_tree(nodelist
):
673 "Given a list of lists of a specific format, return a tree."
675 root
= SVNTreeNode(root_node_name
)
677 for list in nodelist
:
678 new_branch
= create_from_path(list[0], list[1], list[2], list[3])
679 root
.add_child(new_branch
)
684 ####################################################################
685 # Build trees from different kinds of subcommand output.
688 # Parse co/up output into a tree.
690 # Tree nodes will contain no contents, a 'status' att, and a
693 def build_tree_from_checkout(lines
, include_skipped
=1):
694 "Return a tree derived by parsing the output LINES from 'co' or 'up'."
696 root
= SVNTreeNode(root_node_name
)
697 rm1
= re
.compile ('^([RMAGCUDE_ ][MAGCUDE_ ])([B ])\s+(.+)')
699 rm2
= re
.compile ('^(Restored|Skipped)\s+\'(.+)\'')
701 rm2
= re
.compile ('^(Restored)\s+\'(.+)\'')
704 match
= rm1
.search(line
)
705 if match
and match
.groups():
706 new_branch
= create_from_path(match
.group(3), None, {},
707 {'status' : match
.group(1)})
708 root
.add_child(new_branch
)
710 match
= rm2
.search(line
)
711 if match
and match
.groups():
712 new_branch
= create_from_path(match
.group(2), None, {},
713 {'verb' : match
.group(1)})
714 root
.add_child(new_branch
)
719 # Parse ci/im output into a tree.
721 # Tree nodes will contain no contents, and only one 'verb' att.
723 def build_tree_from_commit(lines
):
724 "Return a tree derived by parsing the output LINES from 'ci' or 'im'."
726 # Lines typically have a verb followed by whitespace then a path.
727 root
= SVNTreeNode(root_node_name
)
728 rm1
= re
.compile ('^(\w+( \(bin\))?)\s+(.+)')
729 rm2
= re
.compile ('^Transmitting')
732 match
= rm2
.search(line
)
734 match
= rm1
.search(line
)
735 if match
and match
.groups():
736 new_branch
= create_from_path(match
.group(3), None, {},
737 {'verb' : match
.group(1)})
738 root
.add_child(new_branch
)
743 # Parse status output into a tree.
745 # Tree nodes will contain no contents, and these atts:
747 # 'status', 'wc_rev',
748 # ... and possibly 'locked', 'copied', 'writelocked',
749 # IFF columns non-empty.
752 def build_tree_from_status(lines
):
753 "Return a tree derived by parsing the output LINES from 'st -vuq'."
755 root
= SVNTreeNode(root_node_name
)
757 # 'status -v' output looks like this:
759 # "%c%c%c%c%c%c %c %6s %6s %-12s %s\n"
761 # (Taken from 'print_status' in subversion/svn/status.c.)
763 # Here are the parameters. The middle number in parens is the
764 # match.group(), followed by a brief description of the field:
766 # - text status (1) (single letter)
767 # - prop status (1) (single letter)
768 # - wc-lockedness flag (2) (single letter: "L" or " ")
769 # - copied flag (3) (single letter: "+" or " ")
770 # - switched flag (4) (single letter: "S" or " ")
771 # - repos lock status (5) (single letter: "K", "O", "B", "T", " ")
775 # - out-of-date flag (6) (single letter: "*" or " ")
779 # - working revision (7) (either digits or "-")
783 # - last-changed revision (8) (either digits or "?")
787 # - last author (9) (string of non-whitespace characters)
791 # - path (10) (string of characters until newline)
793 # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU
794 rm
= re
.compile('^([!MACDRUG_ ][MACDRUG_ ])([L ])([+ ])([S ])([KOBT ]) ([* ]) [^0-9-]*(\d+|-|\?) +(\d|-|\?)+ +(\S+) +(.+)')
797 # Quit when we hit an externals status announcement (### someday we can fix
798 # the externals tests to expect the additional flood of externals status
800 if re
.match(r
'^Performing', line
):
803 match
= rm
.search(line
)
804 if match
and match
.groups():
805 if match
.group(9) != '-': # ignore items that only exist on repos
806 atthash
= {'status' : match
.group(1),
807 'wc_rev' : match
.group(7)}
808 if match
.group(2) != ' ':
809 atthash
['locked'] = match
.group(2)
810 if match
.group(3) != ' ':
811 atthash
['copied'] = match
.group(3)
812 if match
.group(4) != ' ':
813 atthash
['switched'] = match
.group(4)
814 if match
.group(5) != ' ':
815 atthash
['writelocked'] = match
.group(5)
816 new_branch
= create_from_path(match
.group(10), None, {}, atthash
)
818 root
.add_child(new_branch
)
823 # Parse merge "skipped" output
825 def build_tree_from_skipped(lines
):
827 root
= SVNTreeNode(root_node_name
)
828 ### Will get confused by spaces in the filename
829 rm
= re
.compile ("^Skipped.* '([^ ]+)'\n")
832 match
= rm
.search(line
)
833 if match
and match
.groups():
834 new_branch
= create_from_path(match
.group(1))
835 root
.add_child(new_branch
)
839 def build_tree_from_diff_summarize(lines
):
840 "Build a tree from output of diff --summarize"
841 root
= SVNTreeNode(root_node_name
)
842 rm
= re
.compile ("^([MAD ][M ]) (.+)\n")
845 match
= rm
.search(line
)
846 if match
and match
.groups():
847 new_branch
= create_from_path(match
.group(2),
848 atts
={'status': match
.group(1)})
849 root
.add_child(new_branch
)
853 ####################################################################
854 # Build trees by looking at the working copy
857 # The reason the 'load_props' flag is off by default is because it
858 # creates a drastic slowdown -- we spawn a new 'svn proplist'
859 # process for every file and dir in the working copy!
862 def build_tree_from_wc(wc_path
, load_props
=0, ignore_svn
=1):
863 """Takes WC_PATH as the path to a working copy. Walks the tree below
864 that path, and creates the tree based on the actual found
865 files. If IGNORE_SVN is true, then exclude SVN admin dirs from the tree.
866 If LOAD_PROPS is true, the props will be added to the tree."""
868 root
= SVNTreeNode(root_node_name
, None)
870 # if necessary, store the root dir's props in a new child node '.'.
872 props
= get_props(wc_path
)
874 root_dir_node
= SVNTreeNode(os
.path
.basename('.'), None, None, props
)
875 root
.add_child(root_dir_node
)
877 # Walk the tree recursively
878 handle_dir(os
.path
.normpath(wc_path
), root
, load_props
, ignore_svn
)