From 09dd885c970392e4da180baf34eadb89fe669c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurko=20Gospodneti=C4=87?= Date: Fri, 27 Jul 2012 22:40:48 +0000 Subject: [PATCH] Refactored internal Boost Build testing system's file system content modeling tree data structure. Used to be based on some old svn_tree.py Subversion project source code. Now unused parts removed and the svn_tree.py module merged into tree.py. Trees_difference class renamed to TreeDifference and corresponding factory function to tree_difference. Now correctly recognizes and reports files being converted to folders and vice versa. [SVN r79773] --- v2/test/BoostBuild.py | 2 +- v2/test/svn_tree.py | 668 ------------------------------------------ v2/test/tree.py | 266 ++++++++++++----- 3 files changed, 194 insertions(+), 742 deletions(-) delete mode 100644 v2/test/svn_tree.py diff --git a/v2/test/BoostBuild.py b/v2/test/BoostBuild.py index 79b2a9ec8..0cd5c68c8 100644 --- a/v2/test/BoostBuild.py +++ b/v2/test/BoostBuild.py @@ -476,7 +476,7 @@ class Tester(TestCmd.TestCmd): self.last_build_timestamp = self.__get_current_file_timestamp() self.tree = tree.build_tree(self.workdir) - self.difference = tree.trees_difference(self.previous_tree, self.tree) + self.difference = tree.tree_difference(self.previous_tree, self.tree) if self.difference.empty(): # If nothing has been changed by this build and sufficient time has # passed since the last build that actually changed something, diff --git a/v2/test/svn_tree.py b/v2/test/svn_tree.py deleted file mode 100644 index e5931955c..000000000 --- a/v2/test/svn_tree.py +++ /dev/null @@ -1,668 +0,0 @@ -#!/usr/bin/env python -# -# tree.py: tools for comparing directory trees -# -# Subversion is a tool for revision control. -# See http://subversion.tigris.org for more information. -# -# ==================================================================== -# Copyright (c) 2001 Sam Tobin-Hochstadt. All rights reserved. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. The terms -# are also available at http://subversion.tigris.org/license-1.html. -# If newer versions of this license are posted there, you may use a -# newer version instead, at your option. -# -###################################################################### - -# This file was modified by Vladimir Prus to store modification times in tree -# nodes. - -import re -import string -import os.path -import os -import stat - - -#======================================================================== - -# ===> Overview of our Datastructures <=== - -# The general idea here is that many, many things can be represented by -# a tree structure: - -# - a working copy's structure and contents -# - the output of 'svn status' -# - the output of 'svn checkout/update' -# - the output of 'svn commit' - -# The idea is that a test function creates a "expected" tree of some -# kind, and is then able to compare it to an "actual" tree that comes -# from running the Subversion client. This is what makes a test -# automated; if an actual and expected tree match exactly, then the test -# has passed. (See compare_trees() below.) - -# The SVNTreeNode class is the fundamental data type used to build tree -# structures. The class contains a method for "dropping" a new node -# into an ever-growing tree structure. (See also create_from_path()). - -# We have four parsers in this file for the four use cases listed above: -# each parser examines some kind of input and returns a tree of -# SVNTreeNode objects. (See build_tree_from_checkout(), -# build_tree_from_commit(), build_tree_from_status(), and -# build_tree_from_wc()). These trees are the "actual" trees that result -# from running the Subversion client. - -# Also necessary, of course, is a convenient way for a test to create an -# "expected" tree. The test *could* manually construct and link a bunch -# of SVNTreeNodes, certainly. But instead, all the tests are using the -# build_generic_tree() routine instead. - -# build_generic_tree() takes a specially-formatted list of lists as -# input, and returns a tree of SVNTreeNodes. The list of lists has this -# structure: - -# [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}], -# [...], -# [...], -# ... ] - -# You can see that each item in the list essentially defines an -# SVNTreeNode. build_generic_tree() instantiates a SVNTreeNode for each -# item, and then drops it into a tree by parsing each item's full path. - -# So a typical test routine spends most of its time preparing lists of -# this format and sending them to build_generic_tree(), rather than -# building the "expected" trees directly. - -# ### Note: in the future, we'd like to remove this extra layer of -# ### abstraction. We'd like the SVNTreeNode class to be more -# ### directly programmer-friendly, providing a number of accessor -# ### routines, so that tests can construct trees directly. - -# The first three fields of each list-item are self-explanatory. It's -# the fourth field, the "attribute" hash, that needs some explanation. -# The att-hash is used to place extra information about the node itself, -# depending on the parsing context: - -# - in the 'svn co/up' use-case, each line of output starts with two -# characters from the set of (A, D, G, U, C, _). This status code -# is stored in a attribute named 'status'. - -# - in the 'svn ci/im' use-case, each line of output starts with one -# of the words (Adding, Deleting, Sending). This verb is stored in -# an attribute named 'verb'. - -# - in the 'svn status' use-case (which is always run with the -v -# (--verbose) flag), each line of output contains a working revision -# number and a two-letter status code similar to the 'svn co/up' -# case. The repository revision is also printed. All of this -# information is stored in attributes named 'wc_rev', 'status', and -# 'repos_rev', respectively. - -# - in the working-copy use-case, the att-hash is ignored. - - -# Finally, one last explanation: the file 'actions.py' contain a number -# of helper routines named 'run_and_verify_FOO'. These routines take -# one or more "expected" trees as input, then run some svn subcommand, -# then push the output through an appropriate parser to derive an -# "actual" tree. Then it runs compare_trees() and returns the result. -# This is why most tests typically end with a call to -# run_and_verify_FOO(). - - - - -# A node in a tree. -# -# If CHILDREN is None, then the node is a file. Otherwise, CHILDREN -# is a list of the nodes making up that directory's children. -# -# NAME is simply the name of the file or directory. CONTENTS is a -# string that contains the file's contents (if a file), PROPS are -# properties attached to files or dirs, and ATTS is a dictionary of -# other metadata attached to the node. - -class SVNTreeNode: - - def __init__(self, name, children=None, contents=None, props={}, atts={}): - self.name = name - self.mtime = 0 - self.children = children - self.contents = contents - self.props = props - self.atts = atts - self.path = name - -# TODO: Check to make sure contents and children are mutually exclusive - - def add_child(self, newchild): - if self.children is None: # if you're a file, - self.children = [] # become an empty dir. - child_already_exists = 0 - for a in self.children: - if a.name == newchild.name: - child_already_exists = 1 - break - if child_already_exists == 0: - self.children.append(newchild) - newchild.path = os.path.join (self.path, newchild.name) - - # If you already have the node, - else: - if newchild.children is None: - # this is the 'end' of the chain, so copy any content here. - a.contents = newchild.contents - a.props = newchild.props - a.atts = newchild.atts - a.path = os.path.join (self.path, newchild.name) - else: - # try to add dangling children to your matching node - for i in newchild.children: - a.add_child(i) - - - def pprint(self): - print " * Node name: ", self.name - print " Path: ", self.path - print " Contents: ", self.contents - print " Properties:", self.props - print " Attributes:", self.atts - ### FIXME: I'd like to be able to tell the difference between - ### self.children is None (file) and self.children == [] (empty - ### diretory), but it seems that most places that construct - ### SVNTreeNode objects don't even try to do that. --xbc - if self.children is not None: - print " Children: ", len(self.children) - else: - print " Children: is a file." - -# reserved name of the root of the tree - -root_node_name = "__SVN_ROOT_NODE" - -# Exception raised if you screw up in this module. - -class SVNTreeError(Exception): pass - -# Exception raised if two trees are unequal - -class SVNTreeUnequal(Exception): pass - -# Exception raised if one node is file and other is dir - -class SVNTypeMismatch(Exception): pass - -# Exception raised if get_child is passed a file. - -class SVNTreeIsNotDirectory(Exception): pass - - -# Some attributes 'stack' on each other if the same node is added -# twice to a tree. Place all such special cases in here. -def attribute_merge(orighash, newhash): - "Merge the attributes in NEWHASH into ORIGHASH." - - if orighash.has_key('verb') and newhash.has_key('verb'): - # Special case: if a commit reports a node as "deleted", then - # "added", it's a replacment. - if orighash['verb'] == "Deleting": - if newhash['verb'] == "Adding": - orighash['verb'] = "Replacing" - - # Add future stackable attributes here... - - return orighash - - -# helper func -def add_elements_as_path(top_node, element_list): - """Add the elements in ELEMENT_LIST as if they were a single path - below TOP_NODE.""" - - # The idea of this function is to take a list like so: - # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree - # like this: - # - # Z -> A -> B -> C - # - # where 1 -> 2 means 2 is a child of 1. - # - - prev_node = top_node - for i in element_list: - new_node = SVNTreeNode(i, None) - prev_node.add_child(new_node) - prev_node = new_node - - -# Sorting function -- sort 2 nodes by their names. -def node_is_greater(a, b): - "Sort the names of two nodes." - # Interal use only - if a.name == b.name: - return 0 - if a.name > b.name: - return 1 - else: - return -1 - - -# Helper for compare_trees -def compare_file_nodes(a, b): - """Compare two nodes' names, contents, and properties, ignoring - children. Return 0 if the same, 1 otherwise.""" - if a.name != b.name: - return 1 - if a.contents != b.contents: - return 1 - if a.props != b.props: - return 1 - if a.atts != b.atts: - return 1 - - -# Internal utility used by most build_tree_from_foo() routines. -# -# (Take the output and .add_child() it to a root node.) - -def create_from_path(path, contents=None, props={}, atts={}): - """Create and return a linked list of treenodes, given a PATH - representing a single entry into that tree. CONTENTS and PROPS are - optional arguments that will be deposited in the tail node.""" - - # get a list of all the names in the path - # each of these will be a child of the former - elements = path.split("/") - if len(elements) == 0: - raise SVNTreeError - - root_node = SVNTreeNode(elements[0], None) - - add_elements_as_path(root_node, elements[1:]) - - # deposit contents in the very last node. - node = root_node - while 1: - if node.children is None: - node.contents = contents - node.props = props - node.atts = atts - break - node = node.children[0] - - return root_node - - -# helper for handle_dir(), which is a helper for build_tree_from_wc() -def get_props(path): - "Return a hash of props for PATH, using the svn client." - - # It's not kosher to look inside SVN/ and try to read the internal - # property storage format. Instead, we use 'svn proplist'. After - # all, this is the only way the user can retrieve them, so we're - # respecting the black-box paradigm. - - props = {} - output, errput = main.run_svn(1, "proplist", path, "--verbose") - - for line in output: - name, value = line.split(' : ') - name = string.strip(name) - value = string.strip(value) - props[name] = value - - return props - - -# helper for handle_dir(), which helps build_tree_from_wc() -def get_text(path): - "Return a string with the textual contents of a file at PATH." - - # sanity check - if not os.path.isfile(path): - return None - - fp = open(path, 'r') - contents = fp.read() - fp.close() - return contents - - -# main recursive helper for build_tree_from_wc() -def handle_dir(path, current_parent, load_props, ignore_svn): - - # get a list of all the files - all_files = os.listdir(path) - files = [] - dirs = [] - - # put dirs and files in their own lists, and remove SVN dirs - for f in all_files: - f = os.path.join(path, f) - if (os.path.isdir(f) and os.path.basename(f) != 'SVN'): - dirs.append(f) - elif os.path.isfile(f): - files.append(f) - - # add each file as a child of CURRENT_PARENT - for f in files: - fcontents = get_text(f) - if load_props: - fprops = get_props(f) - else: - fprops = {} - c = SVNTreeNode(os.path.basename(f), None, - fcontents, fprops) - c.mtime = os.stat(f).st_mtime - current_parent.add_child(c) - - # for each subdir, create a node, walk its tree, add it as a child - for d in dirs: - if load_props: - dprops = get_props(d) - else: - dprops = {} - new_dir_node = SVNTreeNode(os.path.basename(d), [], None, dprops) - handle_dir(d, new_dir_node, load_props, ignore_svn) - new_dir_node.mtime = os.stat(f).st_mtime - current_parent.add_child(new_dir_node) - -def get_child(node, name): - """If SVNTreeNode NODE contains a child named NAME, return child; - else, return None. If SVNTreeNode is not a directory, raise a - SVNTreeIsNotDirectory exception""" - if node.children == None: - raise SVNTreeIsNotDirectory - for n in node.children: - if (name == n.name): - return n - return None - - -# Helper for compare_trees -def default_singleton_handler(a, baton): - "Printing SVNTreeNode A's name, then raise SVNTreeUnequal." - print "Got singleton", a.name - a.pprint() - raise SVNTreeUnequal - - -########################################################################### -########################################################################### -# EXPORTED ROUTINES ARE BELOW - - -# Main tree comparison routine! - -def compare_trees(a, b, - singleton_handler_a = None, - a_baton = None, - singleton_handler_b = None, - b_baton = None): - """Compare SVNTreeNodes A and B, expressing differences using FUNC_A - and FUNC_B. FUNC_A and FUNC_B are functions of two arguments (a - SVNTreeNode and a context baton), and may raise exception - SVNTreeUnequal. Their return value is ignored. - - If A and B are both files, then return 0 if their contents, - properties, and names are all the same; else raise a SVNTreeUnequal. - If A is a file and B is a directory, raise a SVNTypeMismatch; same - vice-versa. If both are directories, then for each entry that - exists in both, call compare_trees on the two entries; otherwise, if - the entry exists only in A, invoke FUNC_A on it, and likewise for - B with FUNC_B.""" - - def display_nodes(a, b): - 'Display two nodes, expected and actual.' - print "=============================================================" - print "Expected", b.name, "and actual", a.name, "are different!" - print "=============================================================" - print "EXPECTED NODE TO BE:" - print "=============================================================" - b.pprint() - print "=============================================================" - print "ACTUAL NODE FOUND:" - print "=============================================================" - a.pprint() - - # Setup singleton handlers - if (singleton_handler_a is None): - singleton_handler_a = default_singleton_handler - if (singleton_handler_b is None): - singleton_handler_b = default_singleton_handler - - try: - # A and B are both files. - if ((a.children is None) and (b.children is None)): - if compare_file_nodes(a, b): - display_nodes(a, b) - raise main.SVNTreeUnequal - # One is a file, one is a directory. - elif (((a.children is None) and (b.children is not None)) - or ((a.children is not None) and (b.children is None))): - display_nodes(a, b) - raise main.SVNTypeMismatch - # They're both directories. - else: - # First, compare the directories' two hashes. - if (a.props != b.props) or (a.atts != b.atts): - display_nodes(a, b) - raise main.SVNTreeUnequal - - accounted_for = [] - # For each child of A, check and see if it's in B. If so, run - # compare_trees on the two children and add b's child to - # accounted_for. If not, run FUNC_A on the child. Next, for each - # child of B, check and see if it's in accounted_for. If it is, - # do nothing. If not, run FUNC_B on it. - for a_child in a.children: - b_child = get_child(b, a_child.name) - if b_child: - accounted_for.append(b_child) - compare_trees(a_child, b_child, - singleton_handler_a, a_baton, - singleton_handler_b, b_baton) - else: - singleton_handler_a(a_child, a_baton) - for b_child in b.children: - if (b_child not in accounted_for): - singleton_handler_b(b_child, b_baton) - return 0 - except SVNTypeMismatch: - print 'Unequal Types: one Node is a file, the other is a directory' - raise SVNTreeUnequal - except SVNTreeIsNotDirectory: - print "Error: Foolish call to get_child." - sys.exit(1) - except IndexError: - print "Error: unequal number of children" - raise SVNTreeUnequal - except SVNTreeUnequal: - if a.name == root_node_name: - return 1 - else: - print "Unequal at node %s" % a.name - raise SVNTreeUnequal - return 0 - - - - -# Visually show a tree's structure - -def dump_tree(n,indent=""): - "Print out a nice representation of the tree's structure." - - # Code partially stolen from Dave Beazley. - if n.children is None: - tmp_children = [] - else: - tmp_children = n.children - - if n.name == root_node_name: - print "%s%s" % (indent, "ROOT") - else: - print "%s%s" % (indent, n.name) - - indent = indent.replace("-", " ") - indent = indent.replace("+", " ") - for i in range(len(tmp_children)): - c = tmp_children[i] - if i == len(tmp_children) - 1: - dump_tree(c,indent + " +-- ") - else: - dump_tree(c,indent + " |-- ") - - -################################################################### -################################################################### -# PARSERS that return trees made of SVNTreeNodes.... - - -################################################################### -# Build an "expected" static tree from a list of lists - - -# Create a list of lists, of the form: -# -# [ [path, contents, props, atts], ... ] -# -# and run it through this parser. PATH is a string, a path to the -# object. CONTENTS is either a string or None, and PROPS and ATTS are -# populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be -# attached to the basename-node of the associated PATH. - -def build_generic_tree(nodelist): - "Given a list of lists of a specific format, return a tree." - - root = SVNTreeNode(root_node_name) - - for list in nodelist: - new_branch = create_from_path(list[0], list[1], list[2], list[3]) - root.add_child(new_branch) - - return root - - -#################################################################### -# Build trees from different kinds of subcommand output. - - -# Parse co/up output into a tree. -# -# Tree nodes will contain no contents, and only one 'status' att. - -def build_tree_from_checkout(lines): - "Return a tree derived by parsing the output LINES from 'co' or 'up'." - - root = SVNTreeNode(root_node_name) - rm = re.compile ('^([MAGCUD_ ][MAGCUD_ ]) (.+)') - - for line in lines: - match = rm.search(line) - if match and match.groups(): - new_branch = create_from_path(match.group(2), None, {}, - {'status' : match.group(1)}) - root.add_child(new_branch) - - return root - - -# Parse ci/im output into a tree. -# -# Tree nodes will contain no contents, and only one 'verb' att. - -def build_tree_from_commit(lines): - "Return a tree derived by parsing the output LINES from 'ci' or 'im'." - - # Lines typically have a verb followed by whitespace then a path. - root = SVNTreeNode(root_node_name) - rm1 = re.compile ('^(\w+)\s+(.+)') - rm2 = re.compile ('^Transmitting') - - for line in lines: - match = rm2.search(line) - if not match: - match = rm1.search(line) - if match and match.groups(): - new_branch = create_from_path(match.group(2), None, {}, - {'verb' : match.group(1)}) - root.add_child(new_branch) - - return root - - -# Parse status output into a tree. -# -# Tree nodes will contain no contents, and these atts: -# -# 'status', 'wc_rev', 'repos_rev' -# ... and possibly 'locked', 'copied', IFF columns non-empty. -# - -def build_tree_from_status(lines): - "Return a tree derived by parsing the output LINES from 'st'." - - root = SVNTreeNode(root_node_name) - rm = re.compile ('^.+\:.+(\d+)') - lastline = string.strip(lines.pop()) - match = rm.search(lastline) - if match and match.groups(): - repos_rev = match.group(1) - else: - repos_rev = '?' - - # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU - rm = re.compile ('^([MACDRUG_ ][MACDRUG_ ])(.)(.) . [^0-9-]+(\d+|-)(.{23})(.+)') - for line in lines: - match = rm.search(line) - if match and match.groups(): - if match.group(5) != '-': # ignore items that only exist on repos - atthash = {'status' : match.group(1), - 'wc_rev' : match.group(4), - 'repos_rev' : repos_rev} - if match.group(2) != ' ': - atthash['locked'] = match.group(2) - if match.group(3) != ' ': - atthash['copied'] = match.group(3) - new_branch = create_from_path(match.group(6), None, {}, atthash) - - root.add_child(new_branch) - - return root - - -#################################################################### -# Build trees by looking at the working copy - - -# The reason the 'load_props' flag is off by default is because it -# creates a drastic slowdown -- we spawn a new 'svn proplist' -# process for every file and dir in the working copy! - - -def build_tree_from_wc(wc_path, load_props=0, ignore_svn=1): - """Takes WC_PATH as the path to a working copy. Walks the tree below - that path, and creates the tree based on the actual found - files. If IGNORE_SVN is true, then exclude SVN dirs from the tree. - If LOAD_PROPS is true, the props will be added to the tree.""" - - root = SVNTreeNode(root_node_name, None) - - # if necessary, store the root dir's props in the root node. - if load_props: - root.props = get_props(wc_path) - - # Walk the tree recursively - handle_dir(os.path.normpath(wc_path), root, load_props, ignore_svn) - - return root - -### End of file. -# local variables: -# eval: (load-file "../../../../../tools/dev/svn-dev.el") -# end: diff --git a/v2/test/tree.py b/v2/test/tree.py index 900637305..18822265a 100644 --- a/v2/test/tree.py +++ b/v2/test/tree.py @@ -1,15 +1,90 @@ # Copyright 2003 Dave Abrahams # Copyright 2001, 2002 Vladimir Prus +# Copyright 2012 Jurko Gospodnetic # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) -# This file is based in part on the content of svn_tree.py. +############################################################################### +# +# Based in part on an old Subversion tree.py source file (tools for comparing +# directory trees). See http://subversion.tigris.org for more information. +# +# Copyright (c) 2001 Sam Tobin-Hochstadt. All rights reserved. +# +# This software is licensed as described in the file COPYING, which you should +# have received as part of this distribution. The terms are also available at +# http://subversion.tigris.org/license-1.html. If newer versions of this +# license are posted there, you may use a newer version instead, at your +# option. +# +############################################################################### -import svn_tree; +import os +import os.path +import stat +import sys -class Trees_difference: +class TreeNode: + """ + Fundamental data type used to build file system tree structures. + + If CHILDREN is None, then the node represents a file. Otherwise, CHILDREN + is a list of the nodes representing that directory's children. + + NAME is simply the name of the file or directory. CONTENTS is a string + holding the file's contents (if a file). + + """ + + def __init__(self, name, children=None, contents=None): + assert children is None or contents is None + self.name = name + self.mtime = 0 + self.children = children + self.contents = contents + self.path = name + + def add_child(self, newchild): + assert not self.is_file() + for a in self.children: + if a.name == newchild.name: + if newchild.is_file(): + a.contents = newchild.contents + a.path = os.path.join(self.path, newchild.name) + else: + for i in newchild.children: + a.add_child(i) + break + else: + self.children.append(newchild) + newchild.path = os.path.join(self.path, newchild.name) + + def get_child(self, name): + """ + If the given TreeNode directory NODE contains a child named NAME, + return the child; else, return None. + + """ + for n in self.children: + if n.name == name: + return n + + def is_file(self): + return self.children is None + + def pprint(self): + print(" * Node name: %s" % self.name) + print(" Path: %s" % self.path) + print(" Contents: %s" % self.contents) + if self.is_file(): + print(" Children: is a file.") + else: + print(" Children: %d" % len(self.children)) + + +class TreeDifference: def __init__(self): self.added_files = [] self.removed_files = [] @@ -23,94 +98,139 @@ class Trees_difference: self.touched_files.extend(other.touched_files) def ignore_directories(self): - """Removes directories for list of found differences""" + """Removes directories from our lists of found differences.""" not_dir = lambda x : x[-1] != "/" self.added_files = filter(not_dir, self.added_files) self.removed_files = filter(not_dir, self.removed_files) self.modified_files = filter(not_dir, self.modified_files) self.touched_files = filter(not_dir, self.touched_files) - def pprint(self, f=None): - print >> f, "Added files :", self.added_files - print >> f, "Removed files :", self.removed_files - print >> f, "Modified files:", self.modified_files - print >> f, "Touched files :", self.touched_files + def pprint(self, file=sys.stdout): + file.write("Added files : %s\n" % self.added_files) + file.write("Removed files : %s\n" % self.removed_files) + file.write("Modified files: %s\n" % self.modified_files) + file.write("Touched files : %s\n" % self.touched_files) def empty(self): - return (not self.added_files and not self.removed_files and - not self.modified_files and not self.touched_files) + return not (self.added_files or self.removed_files or + self.modified_files or self.touched_files) -def build_tree(dir): - return svn_tree.build_tree_from_wc(dir, load_props=0, ignore_svn=1) -def trees_difference(a, b, current_name=""): - """Compare SVNTreeNodes A and B, and create Trees_difference class.""" +def build_tree(path): + """ + Takes PATH as the folder path, walks the file system below that path, and + creates a tree structure based on any files and folders found there. - assert a.name == b.name + All root nodes have an empty name to avoid being displayed when listing + differences between two + """ + root = TreeNode("__ROOT_TREE_NODE__", children=[]) + _handle_dir(os.path.normpath(path), root) + return root - result = Trees_difference() - try: - # A and B are both files. - if a.children is None and b.children is None: - assert a.name == b.name - if svn_tree.compare_file_nodes(a, b): - result.modified_files.append(current_name) - elif a.mtime != b.mtime: - result.touched_files.append(current_name) - # One is a file, one is a directory. - # This case has been disabled because svn_tree does not distinguish - # empty directories from files, at least on Cygwin. - elif 0 and bool(a.children is None) != bool(b.children is None): - a.pprint() - b.pprint() - raise svn_tree.SVNTypeMismatch - # They are both directories. - else: - # accounted_for holds children present in both trees. - accounted_for = [] - for a_child in (a.children or []): - b_child = svn_tree.get_child(b, a_child.name) - if b_child: - accounted_for.append(b_child) - if current_name: - result.append(trees_difference(a_child, b_child, current_name + "/" + a_child.name)) - else: - result.append(trees_difference(a_child, b_child, a_child.name)) - else: - if current_name: - result.removed_files.append(current_name + "/" + a_child.name) - else: - result.removed_files.append(a_child.name) - for b_child in (b.children or []): - if (b_child not in accounted_for): - result.added_files.extend(traverse_tree(b_child, current_name)) +def tree_difference(a, b): + """Compare TreeNodes A and B, and create a TreeDifference instance.""" + return _do_tree_difference(a, b, "", True) + + +def _do_tree_difference(a, b, parent_path, root=False): + """Internal recursive worker function for tree_difference().""" + + # We do not want to list root node names. + if root: + assert not parent_path + assert not a.is_file() + assert not b.is_file() + full_path = "" + else: + assert a.name == b.name + full_path = parent_path + a.name + result = TreeDifference() + + # A and B are both files. + if a.is_file() and b.is_file(): + if a.contents != b.contents: + result.modified_files.append(full_path) + elif a.mtime != b.mtime: + result.touched_files.append(full_path) + return result + + # Directory converted to file. + if not a.is_file() and b.is_file(): + result.removed_files.extend(_traverse_tree(a, parent_path)) + result.added_files.append(full_path) + + # File converted to directory. + elif a.is_file() and not b.is_file(): + result.removed_files.append(full_path) + result.added_files.extend(_traverse_tree(b, parent_path)) + + # A and B are both directories. + else: + if full_path: + full_path += "/" + accounted_for = [] # Children present in both trees. + for a_child in a.children: + b_child = b.get_child(a_child.name) + if b_child: + accounted_for.append(b_child) + result.append(_do_tree_difference(a_child, b_child, full_path)) + else: + result.removed_files.append(full_path + a_child.name) + for b_child in b.children: + if b_child not in accounted_for: + result.added_files.extend(_traverse_tree(b_child, full_path)) - except svn_tree.SVNTypeMismatch: - print("Unequal Types: one Node is a file, the other is a directory") - raise svn_tree.SVNTreeUnequal - except svn_tree.SVNTreeIsNotDirectory: - print("Error: Foolish call to get_child.") - sys.exit(1) - except IndexError: - print("Error: unequal number of children") - raise svn_tree.SVNTreeUnequal return result -def dump_tree(t): - svn_tree.dump_tree(t) -def traverse_tree(t, parent_name=""): - """Returns the list of all names in tree.""" - if parent_name: - full_node_name = parent_name + "/" + t.name - else: - full_node_name = t.name - - if t.children is None: +def _traverse_tree(t, parent_path): + """Returns a list of all names in a tree.""" + assert not parent_path or parent_path[-1] == "/" + full_node_name = parent_path + t.name + if t.is_file(): result = [full_node_name] else: - result = [full_node_name + "/"] + name_prefix = full_node_name + "/" + result = [name_prefix] for i in t.children: - result.extend(traverse_tree(i, full_node_name)) + result.extend(_traverse_tree(i, name_prefix)) return result + + +def _get_text(path): + """Return a string with the textual contents of a file at PATH.""" + fp = open(path, 'r') + try: + return fp.read() + finally: + fp.close() + + +def _handle_dir(path, current_parent): + """Main recursive worker function for build_tree().""" + files = [] + dirs = [] + + # List files & folders. + for f in os.listdir(path): + f = os.path.join(path, f) + if os.path.isdir(f): + dirs.append(f) + elif os.path.isfile(f): + files.append(f) + + # Add each file as a child of CURRENT_PARENT. + for f in files: + fcontents = _get_text(f) + c = TreeNode(os.path.basename(f), contents=fcontents) + c.mtime = os.stat(f).st_mtime + current_parent.add_child(c) + + # For each subdir, create a node, walk its tree, add it as a child. + for d in dirs: + new_dir_node = TreeNode(os.path.basename(d), children=[]) + _handle_dir(d, new_dir_node) + new_dir_node.mtime = os.stat(f).st_mtime + current_parent.add_child(new_dir_node)