| 1 | #!/usr/bin/env python |
|---|
| 2 | # |
|---|
| 3 | # tree.py: tools for comparing directory trees |
|---|
| 4 | # |
|---|
| 5 | # Subversion is a tool for revision control. |
|---|
| 6 | # See http://subversion.tigris.org for more information. |
|---|
| 7 | # |
|---|
| 8 | # ==================================================================== |
|---|
| 9 | # Copyright (c) 2001 Sam Tobin-Hochstadt. All rights reserved. |
|---|
| 10 | # |
|---|
| 11 | # This software is licensed as described in the file COPYING, which |
|---|
| 12 | # you should have received as part of this distribution. The terms |
|---|
| 13 | # are also available at http://subversion.tigris.org/license-1.html. |
|---|
| 14 | # If newer versions of this license are posted there, you may use a |
|---|
| 15 | # newer version instead, at your option. |
|---|
| 16 | # |
|---|
| 17 | ###################################################################### |
|---|
| 18 | |
|---|
| 19 | # This file was modified by Vladimir Prus to store modification times in |
|---|
| 20 | # tree nodes. |
|---|
| 21 | |
|---|
| 22 | import re |
|---|
| 23 | import string |
|---|
| 24 | import os.path |
|---|
| 25 | import os |
|---|
| 26 | import stat |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | #======================================================================== |
|---|
| 31 | |
|---|
| 32 | # ===> Overview of our Datastructures <=== |
|---|
| 33 | |
|---|
| 34 | # The general idea here is that many, many things can be represented by |
|---|
| 35 | # a tree structure: |
|---|
| 36 | |
|---|
| 37 | # - a working copy's structure and contents |
|---|
| 38 | # - the output of 'svn status' |
|---|
| 39 | # - the output of 'svn checkout/update' |
|---|
| 40 | # - the output of 'svn commit' |
|---|
| 41 | |
|---|
| 42 | # The idea is that a test function creates a "expected" tree of some |
|---|
| 43 | # kind, and is then able to compare it to an "actual" tree that comes |
|---|
| 44 | # from running the Subversion client. This is what makes a test |
|---|
| 45 | # automated; if an actual and expected tree match exactly, then the test |
|---|
| 46 | # has passed. (See compare_trees() below.) |
|---|
| 47 | |
|---|
| 48 | # The SVNTreeNode class is the fundamental data type used to build tree |
|---|
| 49 | # structures. The class contains a method for "dropping" a new node |
|---|
| 50 | # into an ever-growing tree structure. (See also create_from_path()). |
|---|
| 51 | |
|---|
| 52 | # We have four parsers in this file for the four use cases listed above: |
|---|
| 53 | # each parser examines some kind of input and returns a tree of |
|---|
| 54 | # SVNTreeNode objects. (See build_tree_from_checkout(), |
|---|
| 55 | # build_tree_from_commit(), build_tree_from_status(), and |
|---|
| 56 | # build_tree_from_wc()). These trees are the "actual" trees that result |
|---|
| 57 | # from running the Subversion client. |
|---|
| 58 | |
|---|
| 59 | # Also necessary, of course, is a convenient way for a test to create an |
|---|
| 60 | # "expected" tree. The test *could* manually construct and link a bunch |
|---|
| 61 | # of SVNTreeNodes, certainly. But instead, all the tests are using the |
|---|
| 62 | # build_generic_tree() routine instead. |
|---|
| 63 | |
|---|
| 64 | # build_generic_tree() takes a specially-formatted list of lists as |
|---|
| 65 | # input, and returns a tree of SVNTreeNodes. The list of lists has this |
|---|
| 66 | # structure: |
|---|
| 67 | |
|---|
| 68 | # [ ['/full/path/to/item', 'text contents', {prop-hash}, {att-hash}], |
|---|
| 69 | # [...], |
|---|
| 70 | # [...], |
|---|
| 71 | # ... ] |
|---|
| 72 | |
|---|
| 73 | # You can see that each item in the list essentially defines an |
|---|
| 74 | # SVNTreeNode. build_generic_tree() instantiates a SVNTreeNode for each |
|---|
| 75 | # item, and then drops it into a tree by parsing each item's full path. |
|---|
| 76 | |
|---|
| 77 | # So a typical test routine spends most of its time preparing lists of |
|---|
| 78 | # this format and sending them to build_generic_tree(), rather than |
|---|
| 79 | # building the "expected" trees directly. |
|---|
| 80 | |
|---|
| 81 | # ### Note: in the future, we'd like to remove this extra layer of |
|---|
| 82 | # ### abstraction. We'd like the SVNTreeNode class to be more |
|---|
| 83 | # ### directly programmer-friendly, providing a number of accessor |
|---|
| 84 | # ### routines, so that tests can construct trees directly. |
|---|
| 85 | |
|---|
| 86 | # The first three fields of each list-item are self-explanatory. It's |
|---|
| 87 | # the fourth field, the "attribute" hash, that needs some explanation. |
|---|
| 88 | # The att-hash is used to place extra information about the node itself, |
|---|
| 89 | # depending on the parsing context: |
|---|
| 90 | |
|---|
| 91 | # - in the 'svn co/up' use-case, each line of output starts with two |
|---|
| 92 | # characters from the set of (A, D, G, U, C, _). This status code |
|---|
| 93 | # is stored in a attribute named 'status'. |
|---|
| 94 | |
|---|
| 95 | # - in the 'svn ci/im' use-case, each line of output starts with one |
|---|
| 96 | # of the words (Adding, Deleting, Sending). This verb is stored in |
|---|
| 97 | # an attribute named 'verb'. |
|---|
| 98 | |
|---|
| 99 | # - in the 'svn status' use-case (which is always run with the -v |
|---|
| 100 | # (--verbose) flag), each line of output contains a working revision |
|---|
| 101 | # number and a two-letter status code similar to the 'svn co/up' |
|---|
| 102 | # case. The repository revision is also printed. All of this |
|---|
| 103 | # information is stored in attributes named 'wc_rev', 'status', and |
|---|
| 104 | # 'repos_rev', respectively. |
|---|
| 105 | |
|---|
| 106 | # - in the working-copy use-case, the att-hash is ignored. |
|---|
| 107 | |
|---|
| 108 | |
|---|
| 109 | # Finally, one last explanation: the file 'actions.py' contain a number |
|---|
| 110 | # of helper routines named 'run_and_verify_FOO'. These routines take |
|---|
| 111 | # one or more "expected" trees as input, then run some svn subcommand, |
|---|
| 112 | # then push the output through an appropriate parser to derive an |
|---|
| 113 | # "actual" tree. Then it runs compare_trees() and returns the result. |
|---|
| 114 | # This is why most tests typically end with a call to |
|---|
| 115 | # run_and_verify_FOO(). |
|---|
| 116 | |
|---|
| 117 | |
|---|
| 118 | |
|---|
| 119 | |
|---|
| 120 | # A node in a tree. |
|---|
| 121 | # |
|---|
| 122 | # If CHILDREN is None, then the node is a file. Otherwise, CHILDREN |
|---|
| 123 | # is a list of the nodes making up that directory's children. |
|---|
| 124 | # |
|---|
| 125 | # NAME is simply the name of the file or directory. CONTENTS is a |
|---|
| 126 | # string that contains the file's contents (if a file), PROPS are |
|---|
| 127 | # properties attached to files or dirs, and ATTS is a dictionary of |
|---|
| 128 | # other metadata attached to the node. |
|---|
| 129 | |
|---|
| 130 | class SVNTreeNode: |
|---|
| 131 | |
|---|
| 132 | def __init__(self, name, children=None, contents=None, props={}, atts={}): |
|---|
| 133 | self.name = name |
|---|
| 134 | self.mtime = 0 |
|---|
| 135 | self.children = children |
|---|
| 136 | self.contents = contents |
|---|
| 137 | self.props = props |
|---|
| 138 | self.atts = atts |
|---|
| 139 | self.path = name |
|---|
| 140 | |
|---|
| 141 | # TODO: Check to make sure contents and children are mutually exclusive |
|---|
| 142 | |
|---|
| 143 | def add_child(self, newchild): |
|---|
| 144 | if self.children is None: # if you're a file, |
|---|
| 145 | self.children = [] # become an empty dir. |
|---|
| 146 | child_already_exists = 0 |
|---|
| 147 | for a in self.children: |
|---|
| 148 | if a.name == newchild.name: |
|---|
| 149 | child_already_exists = 1 |
|---|
| 150 | break |
|---|
| 151 | if child_already_exists == 0: |
|---|
| 152 | self.children.append(newchild) |
|---|
| 153 | newchild.path = os.path.join (self.path, newchild.name) |
|---|
| 154 | |
|---|
| 155 | # If you already have the node, |
|---|
| 156 | else: |
|---|
| 157 | if newchild.children is None: |
|---|
| 158 | # this is the 'end' of the chain, so copy any content here. |
|---|
| 159 | a.contents = newchild.contents |
|---|
| 160 | a.props = newchild.props |
|---|
| 161 | a.atts = newchild.atts |
|---|
| 162 | a.path = os.path.join (self.path, newchild.name) |
|---|
| 163 | else: |
|---|
| 164 | # try to add dangling children to your matching node |
|---|
| 165 | for i in newchild.children: |
|---|
| 166 | a.add_child(i) |
|---|
| 167 | |
|---|
| 168 | |
|---|
| 169 | def pprint(self): |
|---|
| 170 | print " * Node name: ", self.name |
|---|
| 171 | print " Path: ", self.path |
|---|
| 172 | print " Contents: ", self.contents |
|---|
| 173 | print " Properties:", self.props |
|---|
| 174 | print " Attributes:", self.atts |
|---|
| 175 | ### FIXME: I'd like to be able to tell the difference between |
|---|
| 176 | ### self.children is None (file) and self.children == [] (empty |
|---|
| 177 | ### diretory), but it seems that most places that construct |
|---|
| 178 | ### SVNTreeNode objects don't even try to do that. --xbc |
|---|
| 179 | if self.children is not None: |
|---|
| 180 | print " Children: ", len(self.children) |
|---|
| 181 | else: |
|---|
| 182 | print " Children: is a file." |
|---|
| 183 | |
|---|
| 184 | # reserved name of the root of the tree |
|---|
| 185 | |
|---|
| 186 | root_node_name = "__SVN_ROOT_NODE" |
|---|
| 187 | |
|---|
| 188 | # Exception raised if you screw up in this module. |
|---|
| 189 | |
|---|
| 190 | class SVNTreeError(Exception): pass |
|---|
| 191 | |
|---|
| 192 | # Exception raised if two trees are unequal |
|---|
| 193 | |
|---|
| 194 | class SVNTreeUnequal(Exception): pass |
|---|
| 195 | |
|---|
| 196 | # Exception raised if one node is file and other is dir |
|---|
| 197 | |
|---|
| 198 | class SVNTypeMismatch(Exception): pass |
|---|
| 199 | |
|---|
| 200 | # Exception raised if get_child is passed a file. |
|---|
| 201 | |
|---|
| 202 | class SVNTreeIsNotDirectory(Exception): pass |
|---|
| 203 | |
|---|
| 204 | |
|---|
| 205 | # Some attributes 'stack' on each other if the same node is added |
|---|
| 206 | # twice to a tree. Place all such special cases in here. |
|---|
| 207 | def attribute_merge(orighash, newhash): |
|---|
| 208 | "Merge the attributes in NEWHASH into ORIGHASH." |
|---|
| 209 | |
|---|
| 210 | if orighash.has_key('verb') and newhash.has_key('verb'): |
|---|
| 211 | # Special case: if a commit reports a node as "deleted", then |
|---|
| 212 | # "added", it's a replacment. |
|---|
| 213 | if orighash['verb'] == "Deleting": |
|---|
| 214 | if newhash['verb'] == "Adding": |
|---|
| 215 | orighash['verb'] = "Replacing" |
|---|
| 216 | |
|---|
| 217 | # Add future stackable attributes here... |
|---|
| 218 | |
|---|
| 219 | return orighash |
|---|
| 220 | |
|---|
| 221 | |
|---|
| 222 | # helper func |
|---|
| 223 | def add_elements_as_path(top_node, element_list): |
|---|
| 224 | """Add the elements in ELEMENT_LIST as if they were a single path |
|---|
| 225 | below TOP_NODE.""" |
|---|
| 226 | |
|---|
| 227 | # The idea of this function is to take a list like so: |
|---|
| 228 | # ['A', 'B', 'C'] and a top node, say 'Z', and generate a tree |
|---|
| 229 | # like this: |
|---|
| 230 | # |
|---|
| 231 | # Z -> A -> B -> C |
|---|
| 232 | # |
|---|
| 233 | # where 1 -> 2 means 2 is a child of 1. |
|---|
| 234 | # |
|---|
| 235 | |
|---|
| 236 | prev_node = top_node |
|---|
| 237 | for i in element_list: |
|---|
| 238 | new_node = SVNTreeNode(i, None) |
|---|
| 239 | prev_node.add_child(new_node) |
|---|
| 240 | prev_node = new_node |
|---|
| 241 | |
|---|
| 242 | |
|---|
| 243 | # Sorting function -- sort 2 nodes by their names. |
|---|
| 244 | def node_is_greater(a, b): |
|---|
| 245 | "Sort the names of two nodes." |
|---|
| 246 | # Interal use only |
|---|
| 247 | if a.name == b.name: |
|---|
| 248 | return 0 |
|---|
| 249 | if a.name > b.name: |
|---|
| 250 | return 1 |
|---|
| 251 | else: |
|---|
| 252 | return -1 |
|---|
| 253 | |
|---|
| 254 | |
|---|
| 255 | # Helper for compare_trees |
|---|
| 256 | def compare_file_nodes(a, b): |
|---|
| 257 | """Compare two nodes' names, contents, and properties, ignoring |
|---|
| 258 | children. Return 0 if the same, 1 otherwise.""" |
|---|
| 259 | if a.name != b.name: |
|---|
| 260 | return 1 |
|---|
| 261 | if a.contents != b.contents: |
|---|
| 262 | return 1 |
|---|
| 263 | if a.props != b.props: |
|---|
| 264 | return 1 |
|---|
| 265 | if a.atts != b.atts: |
|---|
| 266 | return 1 |
|---|
| 267 | |
|---|
| 268 | |
|---|
| 269 | # Internal utility used by most build_tree_from_foo() routines. |
|---|
| 270 | # |
|---|
| 271 | # (Take the output and .add_child() it to a root node.) |
|---|
| 272 | |
|---|
| 273 | def create_from_path(path, contents=None, props={}, atts={}): |
|---|
| 274 | """Create and return a linked list of treenodes, given a PATH |
|---|
| 275 | representing a single entry into that tree. CONTENTS and PROPS are |
|---|
| 276 | optional arguments that will be deposited in the tail node.""" |
|---|
| 277 | |
|---|
| 278 | # get a list of all the names in the path |
|---|
| 279 | # each of these will be a child of the former |
|---|
| 280 | elements = path.split("/") |
|---|
| 281 | if len(elements) == 0: |
|---|
| 282 | raise SVNTreeError |
|---|
| 283 | |
|---|
| 284 | root_node = SVNTreeNode(elements[0], None) |
|---|
| 285 | |
|---|
| 286 | add_elements_as_path(root_node, elements[1:]) |
|---|
| 287 | |
|---|
| 288 | # deposit contents in the very last node. |
|---|
| 289 | node = root_node |
|---|
| 290 | while 1: |
|---|
| 291 | if node.children is None: |
|---|
| 292 | node.contents = contents |
|---|
| 293 | node.props = props |
|---|
| 294 | node.atts = atts |
|---|
| 295 | break |
|---|
| 296 | node = node.children[0] |
|---|
| 297 | |
|---|
| 298 | return root_node |
|---|
| 299 | |
|---|
| 300 | |
|---|
| 301 | # helper for handle_dir(), which is a helper for build_tree_from_wc() |
|---|
| 302 | def get_props(path): |
|---|
| 303 | "Return a hash of props for PATH, using the svn client." |
|---|
| 304 | |
|---|
| 305 | # It's not kosher to look inside SVN/ and try to read the internal |
|---|
| 306 | # property storage format. Instead, we use 'svn proplist'. After |
|---|
| 307 | # all, this is the only way the user can retrieve them, so we're |
|---|
| 308 | # respecting the black-box paradigm. |
|---|
| 309 | |
|---|
| 310 | props = {} |
|---|
| 311 | output, errput = main.run_svn(1, "proplist", path, "--verbose") |
|---|
| 312 | |
|---|
| 313 | for line in output: |
|---|
| 314 | name, value = line.split(' : ') |
|---|
| 315 | name = string.strip(name) |
|---|
| 316 | value = string.strip(value) |
|---|
| 317 | props[name] = value |
|---|
| 318 | |
|---|
| 319 | return props |
|---|
| 320 | |
|---|
| 321 | |
|---|
| 322 | # helper for handle_dir(), which helps build_tree_from_wc() |
|---|
| 323 | def get_text(path): |
|---|
| 324 | "Return a string with the textual contents of a file at PATH." |
|---|
| 325 | |
|---|
| 326 | # sanity check |
|---|
| 327 | if not os.path.isfile(path): |
|---|
| 328 | return None |
|---|
| 329 | |
|---|
| 330 | fp = open(path, 'r') |
|---|
| 331 | contents = fp.read() |
|---|
| 332 | fp.close() |
|---|
| 333 | return contents |
|---|
| 334 | |
|---|
| 335 | |
|---|
| 336 | # main recursive helper for build_tree_from_wc() |
|---|
| 337 | def handle_dir(path, current_parent, load_props, ignore_svn): |
|---|
| 338 | |
|---|
| 339 | # get a list of all the files |
|---|
| 340 | all_files = os.listdir(path) |
|---|
| 341 | files = [] |
|---|
| 342 | dirs = [] |
|---|
| 343 | |
|---|
| 344 | # put dirs and files in their own lists, and remove SVN dirs |
|---|
| 345 | for f in all_files: |
|---|
| 346 | f = os.path.join(path, f) |
|---|
| 347 | if (os.path.isdir(f) and os.path.basename(f) != 'SVN'): |
|---|
| 348 | dirs.append(f) |
|---|
| 349 | elif os.path.isfile(f): |
|---|
| 350 | files.append(f) |
|---|
| 351 | |
|---|
| 352 | # add each file as a child of CURRENT_PARENT |
|---|
| 353 | for f in files: |
|---|
| 354 | fcontents = get_text(f) |
|---|
| 355 | if load_props: |
|---|
| 356 | fprops = get_props(f) |
|---|
| 357 | else: |
|---|
| 358 | fprops = {} |
|---|
| 359 | c = SVNTreeNode(os.path.basename(f), None, |
|---|
| 360 | fcontents, fprops) |
|---|
| 361 | c.mtime = os.stat(f)[stat.ST_MTIME] |
|---|
| 362 | current_parent.add_child(c) |
|---|
| 363 | |
|---|
| 364 | # for each subdir, create a node, walk its tree, add it as a child |
|---|
| 365 | for d in dirs: |
|---|
| 366 | if load_props: |
|---|
| 367 | dprops = get_props(d) |
|---|
| 368 | else: |
|---|
| 369 | dprops = {} |
|---|
| 370 | new_dir_node = SVNTreeNode(os.path.basename(d), [], None, dprops) |
|---|
| 371 | handle_dir(d, new_dir_node, load_props, ignore_svn) |
|---|
| 372 | new_dir_node.mtime = os.stat(f)[stat.ST_MTIME] |
|---|
| 373 | current_parent.add_child(new_dir_node) |
|---|
| 374 | |
|---|
| 375 | def get_child(node, name): |
|---|
| 376 | """If SVNTreeNode NODE contains a child named NAME, return child; |
|---|
| 377 | else, return None. If SVNTreeNode is not a directory, raise a |
|---|
| 378 | SVNTreeIsNotDirectory exception""" |
|---|
| 379 | if node.children == None: |
|---|
| 380 | raise SVNTreeIsNotDirectory |
|---|
| 381 | for n in node.children: |
|---|
| 382 | if (name == n.name): |
|---|
| 383 | return n |
|---|
| 384 | return None |
|---|
| 385 | |
|---|
| 386 | |
|---|
| 387 | # Helper for compare_trees |
|---|
| 388 | def default_singleton_handler(a, baton): |
|---|
| 389 | "Printing SVNTreeNode A's name, then raise SVNTreeUnequal." |
|---|
| 390 | print "Got singleton", a.name |
|---|
| 391 | a.pprint() |
|---|
| 392 | raise SVNTreeUnequal |
|---|
| 393 | |
|---|
| 394 | |
|---|
| 395 | ########################################################################### |
|---|
| 396 | ########################################################################### |
|---|
| 397 | # EXPORTED ROUTINES ARE BELOW |
|---|
| 398 | |
|---|
| 399 | |
|---|
| 400 | # Main tree comparison routine! |
|---|
| 401 | |
|---|
| 402 | def compare_trees(a, b, |
|---|
| 403 | singleton_handler_a = None, |
|---|
| 404 | a_baton = None, |
|---|
| 405 | singleton_handler_b = None, |
|---|
| 406 | b_baton = None): |
|---|
| 407 | """Compare SVNTreeNodes A and B, expressing differences using FUNC_A |
|---|
| 408 | and FUNC_B. FUNC_A and FUNC_B are functions of two arguments (a |
|---|
| 409 | SVNTreeNode and a context baton), and may raise exception |
|---|
| 410 | SVNTreeUnequal. Their return value is ignored. |
|---|
| 411 | |
|---|
| 412 | If A and B are both files, then return 0 if their contents, |
|---|
| 413 | properties, and names are all the same; else raise a SVNTreeUnequal. |
|---|
| 414 | If A is a file and B is a directory, raise a SVNTypeMismatch; same |
|---|
| 415 | vice-versa. If both are directories, then for each entry that |
|---|
| 416 | exists in both, call compare_trees on the two entries; otherwise, if |
|---|
| 417 | the entry exists only in A, invoke FUNC_A on it, and likewise for |
|---|
| 418 | B with FUNC_B.""" |
|---|
| 419 | |
|---|
| 420 | def display_nodes(a, b): |
|---|
| 421 | 'Display two nodes, expected and actual.' |
|---|
| 422 | print "=============================================================" |
|---|
| 423 | print "Expected", b.name, "and actual", a.name, "are different!" |
|---|
| 424 | print "=============================================================" |
|---|
| 425 | print "EXPECTED NODE TO BE:" |
|---|
| 426 | print "=============================================================" |
|---|
| 427 | b.pprint() |
|---|
| 428 | print "=============================================================" |
|---|
| 429 | print "ACTUAL NODE FOUND:" |
|---|
| 430 | print "=============================================================" |
|---|
| 431 | a.pprint() |
|---|
| 432 | |
|---|
| 433 | # Setup singleton handlers |
|---|
| 434 | if (singleton_handler_a is None): |
|---|
| 435 | singleton_handler_a = default_singleton_handler |
|---|
| 436 | if (singleton_handler_b is None): |
|---|
| 437 | singleton_handler_b = default_singleton_handler |
|---|
| 438 | |
|---|
| 439 | try: |
|---|
| 440 | # A and B are both files. |
|---|
| 441 | if ((a.children is None) and (b.children is None)): |
|---|
| 442 | if compare_file_nodes(a, b): |
|---|
| 443 | display_nodes(a, b) |
|---|
| 444 | raise main.SVNTreeUnequal |
|---|
| 445 | # One is a file, one is a directory. |
|---|
| 446 | elif (((a.children is None) and (b.children is not None)) |
|---|
| 447 | or ((a.children is not None) and (b.children is None))): |
|---|
| 448 | display_nodes(a, b) |
|---|
| 449 | raise main.SVNTypeMismatch |
|---|
| 450 | # They're both directories. |
|---|
| 451 | else: |
|---|
| 452 | # First, compare the directories' two hashes. |
|---|
| 453 | if (a.props != b.props) or (a.atts != b.atts): |
|---|
| 454 | display_nodes(a, b) |
|---|
| 455 | raise main.SVNTreeUnequal |
|---|
| 456 | |
|---|
| 457 | accounted_for = [] |
|---|
| 458 | # For each child of A, check and see if it's in B. If so, run |
|---|
| 459 | # compare_trees on the two children and add b's child to |
|---|
| 460 | # accounted_for. If not, run FUNC_A on the child. Next, for each |
|---|
| 461 | # child of B, check and see if it's in accounted_for. If it is, |
|---|
| 462 | # do nothing. If not, run FUNC_B on it. |
|---|
| 463 | for a_child in a.children: |
|---|
| 464 | b_child = get_child(b, a_child.name) |
|---|
| 465 | if b_child: |
|---|
| 466 | accounted_for.append(b_child) |
|---|
| 467 | compare_trees(a_child, b_child, |
|---|
| 468 | singleton_handler_a, a_baton, |
|---|
| 469 | singleton_handler_b, b_baton) |
|---|
| 470 | else: |
|---|
| 471 | singleton_handler_a(a_child, a_baton) |
|---|
| 472 | for b_child in b.children: |
|---|
| 473 | if (b_child not in accounted_for): |
|---|
| 474 | singleton_handler_b(b_child, b_baton) |
|---|
| 475 | return 0 |
|---|
| 476 | except SVNTypeMismatch: |
|---|
| 477 | print 'Unequal Types: one Node is a file, the other is a directory' |
|---|
| 478 | raise SVNTreeUnequal |
|---|
| 479 | except SVNTreeIsNotDirectory: |
|---|
| 480 | print "Error: Foolish call to get_child." |
|---|
| 481 | sys.exit(1) |
|---|
| 482 | except IndexError: |
|---|
| 483 | print "Error: unequal number of children" |
|---|
| 484 | raise SVNTreeUnequal |
|---|
| 485 | except SVNTreeUnequal: |
|---|
| 486 | if a.name == root_node_name: |
|---|
| 487 | return 1 |
|---|
| 488 | else: |
|---|
| 489 | print "Unequal at node %s" % a.name |
|---|
| 490 | raise SVNTreeUnequal |
|---|
| 491 | return 0 |
|---|
| 492 | |
|---|
| 493 | |
|---|
| 494 | |
|---|
| 495 | |
|---|
| 496 | # Visually show a tree's structure |
|---|
| 497 | |
|---|
| 498 | def dump_tree(n,indent=""): |
|---|
| 499 | "Print out a nice representation of the tree's structure." |
|---|
| 500 | |
|---|
| 501 | # Code partially stolen from Dave Beazley |
|---|
| 502 | if n.children is None: |
|---|
| 503 | tmp_children = [] |
|---|
| 504 | else: |
|---|
| 505 | tmp_children = n.children |
|---|
| 506 | |
|---|
| 507 | if n.name == root_node_name: |
|---|
| 508 | print "%s%s" % (indent, "ROOT") |
|---|
| 509 | else: |
|---|
| 510 | print "%s%s" % (indent, n.name) |
|---|
| 511 | |
|---|
| 512 | indent = indent.replace("-"," ") |
|---|
| 513 | indent = indent.replace("+"," ") |
|---|
| 514 | for i in range(len(tmp_children)): |
|---|
| 515 | c = tmp_children[i] |
|---|
| 516 | if i == len(tmp_children |
|---|
| 517 | )-1: |
|---|
| 518 | dump_tree(c,indent + " +-- ") |
|---|
| 519 | else: |
|---|
| 520 | dump_tree(c,indent + " |-- ") |
|---|
| 521 | |
|---|
| 522 | |
|---|
| 523 | ################################################################### |
|---|
| 524 | ################################################################### |
|---|
| 525 | # PARSERS that return trees made of SVNTreeNodes.... |
|---|
| 526 | |
|---|
| 527 | |
|---|
| 528 | ################################################################### |
|---|
| 529 | # Build an "expected" static tree from a list of lists |
|---|
| 530 | |
|---|
| 531 | |
|---|
| 532 | # Create a list of lists, of the form: |
|---|
| 533 | # |
|---|
| 534 | # [ [path, contents, props, atts], ... ] |
|---|
| 535 | # |
|---|
| 536 | # and run it through this parser. PATH is a string, a path to the |
|---|
| 537 | # object. CONTENTS is either a string or None, and PROPS and ATTS are |
|---|
| 538 | # populated dictionaries or {}. Each CONTENTS/PROPS/ATTS will be |
|---|
| 539 | # attached to the basename-node of the associated PATH. |
|---|
| 540 | |
|---|
| 541 | def build_generic_tree(nodelist): |
|---|
| 542 | "Given a list of lists of a specific format, return a tree." |
|---|
| 543 | |
|---|
| 544 | root = SVNTreeNode(root_node_name) |
|---|
| 545 | |
|---|
| 546 | for list in nodelist: |
|---|
| 547 | new_branch = create_from_path(list[0], list[1], list[2], list[3]) |
|---|
| 548 | root.add_child(new_branch) |
|---|
| 549 | |
|---|
| 550 | return root |
|---|
| 551 | |
|---|
| 552 | |
|---|
| 553 | #################################################################### |
|---|
| 554 | # Build trees from different kinds of subcommand output. |
|---|
| 555 | |
|---|
| 556 | |
|---|
| 557 | # Parse co/up output into a tree. |
|---|
| 558 | # |
|---|
| 559 | # Tree nodes will contain no contents, and only one 'status' att. |
|---|
| 560 | |
|---|
| 561 | def build_tree_from_checkout(lines): |
|---|
| 562 | "Return a tree derived by parsing the output LINES from 'co' or 'up'." |
|---|
| 563 | |
|---|
| 564 | root = SVNTreeNode(root_node_name) |
|---|
| 565 | rm = re.compile ('^([MAGCUD_ ][MAGCUD_ ]) (.+)') |
|---|
| 566 | |
|---|
| 567 | for line in lines: |
|---|
| 568 | match = rm.search(line) |
|---|
| 569 | if match and match.groups(): |
|---|
| 570 | new_branch = create_from_path(match.group(2), None, {}, |
|---|
| 571 | {'status' : match.group(1)}) |
|---|
| 572 | root.add_child(new_branch) |
|---|
| 573 | |
|---|
| 574 | return root |
|---|
| 575 | |
|---|
| 576 | |
|---|
| 577 | # Parse ci/im output into a tree. |
|---|
| 578 | # |
|---|
| 579 | # Tree nodes will contain no contents, and only one 'verb' att. |
|---|
| 580 | |
|---|
| 581 | def build_tree_from_commit(lines): |
|---|
| 582 | "Return a tree derived by parsing the output LINES from 'ci' or 'im'." |
|---|
| 583 | |
|---|
| 584 | # Lines typically have a verb followed by whitespace then a path. |
|---|
| 585 | root = SVNTreeNode(root_node_name) |
|---|
| 586 | rm1 = re.compile ('^(\w+)\s+(.+)') |
|---|
| 587 | rm2 = re.compile ('^Transmitting') |
|---|
| 588 | |
|---|
| 589 | for line in lines: |
|---|
| 590 | match = rm2.search(line) |
|---|
| 591 | if not match: |
|---|
| 592 | match = rm1.search(line) |
|---|
| 593 | if match and match.groups(): |
|---|
| 594 | new_branch = create_from_path(match.group(2), None, {}, |
|---|
| 595 | {'verb' : match.group(1)}) |
|---|
| 596 | root.add_child(new_branch) |
|---|
| 597 | |
|---|
| 598 | return root |
|---|
| 599 | |
|---|
| 600 | |
|---|
| 601 | # Parse status output into a tree. |
|---|
| 602 | # |
|---|
| 603 | # Tree nodes will contain no contents, and these atts: |
|---|
| 604 | # |
|---|
| 605 | # 'status', 'wc_rev', 'repos_rev' |
|---|
| 606 | # ... and possibly 'locked', 'copied', IFF columns non-empty. |
|---|
| 607 | # |
|---|
| 608 | |
|---|
| 609 | def build_tree_from_status(lines): |
|---|
| 610 | "Return a tree derived by parsing the output LINES from 'st'." |
|---|
| 611 | |
|---|
| 612 | root = SVNTreeNode(root_node_name) |
|---|
| 613 | rm = re.compile ('^.+\:.+(\d+)') |
|---|
| 614 | lastline = string.strip(lines.pop()) |
|---|
| 615 | match = rm.search(lastline) |
|---|
| 616 | if match and match.groups(): |
|---|
| 617 | repos_rev = match.group(1) |
|---|
| 618 | else: |
|---|
| 619 | repos_rev = '?' |
|---|
| 620 | |
|---|
| 621 | # Try http://www.wordsmith.org/anagram/anagram.cgi?anagram=ACDRMGU |
|---|
| 622 | rm = re.compile ('^([MACDRUG_ ][MACDRUG_ ])(.)(.) . [^0-9-]+(\d+|-)(.{23})(.+)') |
|---|
| 623 | for line in lines: |
|---|
| 624 | match = rm.search(line) |
|---|
| 625 | if match and match.groups(): |
|---|
| 626 | if match.group(5) != '-': # ignore items that only exist on repos |
|---|
| 627 | atthash = {'status' : match.group(1), |
|---|
| 628 | 'wc_rev' : match.group(4), |
|---|
| 629 | 'repos_rev' : repos_rev} |
|---|
| 630 | if match.group(2) != ' ': |
|---|
| 631 | atthash['locked'] = match.group(2) |
|---|
| 632 | if match.group(3) != ' ': |
|---|
| 633 | atthash['copied'] = match.group(3) |
|---|
| 634 | new_branch = create_from_path(match.group(6), None, {}, atthash) |
|---|
| 635 | |
|---|
| 636 | root.add_child(new_branch) |
|---|
| 637 | |
|---|
| 638 | return root |
|---|
| 639 | |
|---|
| 640 | |
|---|
| 641 | #################################################################### |
|---|
| 642 | # Build trees by looking at the working copy |
|---|
| 643 | |
|---|
| 644 | |
|---|
| 645 | # The reason the 'load_props' flag is off by default is because it |
|---|
| 646 | # creates a drastic slowdown -- we spawn a new 'svn proplist' |
|---|
| 647 | # process for every file and dir in the working copy! |
|---|
| 648 | |
|---|
| 649 | |
|---|
| 650 | def build_tree_from_wc(wc_path, load_props=0, ignore_svn=1): |
|---|
| 651 | """Takes WC_PATH as the path to a working copy. Walks the tree below |
|---|
| 652 | that path, and creates the tree based on the actual found |
|---|
| 653 | files. If IGNORE_SVN is true, then exclude SVN dirs from the tree. |
|---|
| 654 | If LOAD_PROPS is true, the props will be added to the tree.""" |
|---|
| 655 | |
|---|
| 656 | root = SVNTreeNode(root_node_name, None) |
|---|
| 657 | |
|---|
| 658 | # if necessary, store the root dir's props in the root node. |
|---|
| 659 | if load_props: |
|---|
| 660 | root.props = get_props(wc_path) |
|---|
| 661 | |
|---|
| 662 | # Walk the tree recursively |
|---|
| 663 | handle_dir(os.path.normpath(wc_path), root, load_props, ignore_svn) |
|---|
| 664 | |
|---|
| 665 | return root |
|---|
| 666 | |
|---|
| 667 | ### End of file. |
|---|
| 668 | # local variables: |
|---|
| 669 | # eval: (load-file "../../../../../tools/dev/svn-dev.el") |
|---|
| 670 | # end: |
|---|