| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- # 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.txt or copy at
- # https://www.bfgroup.xyz/b2/LICENSE.txt)
- ###############################################################################
- #
- # 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.
- #
- ###############################################################################
- from __future__ import print_function
- import os
- import os.path
- import stat
- import sys
- 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 = []
- self.modified_files = []
- self.touched_files = []
- def append(self, other):
- self.added_files.extend(other.added_files)
- self.removed_files.extend(other.removed_files)
- self.modified_files.extend(other.modified_files)
- self.touched_files.extend(other.touched_files)
- def ignore_directories(self):
- """Removes directories from our lists of found differences."""
- not_dir = lambda x : x[-1] != "/"
- self.added_files = list(filter(not_dir, self.added_files))
- self.removed_files = list(filter(not_dir, self.removed_files))
- self.modified_files = list(filter(not_dir, self.modified_files))
- self.touched_files = list(filter(not_dir, 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 or self.removed_files or
- self.modified_files or self.touched_files)
- 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.
- Returns the prepared tree structure plus the maximum file modification
- timestamp under the given folder.
- """
- return _handle_dir(os.path.normpath(path))
- 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))
- return result
- 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:
- name_prefix = full_node_name + "/"
- result = [name_prefix]
- for i in t.children:
- 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, 'rb')
- try:
- return fp.read()
- finally:
- fp.close()
- def _handle_dir(path):
- """
- Main recursive worker function for build_tree(). Returns a newly created
- tree node representing the given normalized folder path as well as the
- maximum file/folder modification time detected under the same path.
- """
- files = []
- dirs = []
- node = TreeNode(os.path.basename(path), children=[])
- max_mtime = node.mtime = os.stat(path).st_mtime
- # 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 a child node for each file.
- for f in files:
- fcontents = _get_text(f)
- new_file_node = TreeNode(os.path.basename(f), contents=fcontents)
- new_file_node.mtime = os.stat(f).st_mtime
- max_mtime = max(max_mtime, new_file_node.mtime)
- node.add_child(new_file_node)
- # For each subdir, create a node, walk its tree, add it as a child.
- for d in dirs:
- new_dir_node, new_max_mtime = _handle_dir(d)
- max_mtime = max(max_mtime, new_max_mtime)
- node.add_child(new_dir_node)
- return node, max_mtime
|