2 # wc.py: functions for interacting with a Subversion working copy
4 # Subversion is a tool for revision control.
5 # See http://subversion.tigris.org for more information.
7 # ====================================================================
8 # Licensed to the Apache Software Foundation (ASF) under one
9 # or more contributor license agreements. See the NOTICE file
10 # distributed with this work for additional information
11 # regarding copyright ownership. The ASF licenses this file
12 # to you under the Apache License, Version 2.0 (the
13 # "License"); you may not use this file except in compliance
14 # with the License. You may obtain a copy of the License at
16 # http://www.apache.org/licenses/LICENSE-2.0
18 # Unless required by applicable law or agreed to in writing,
19 # software distributed under the License is distributed on an
20 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21 # KIND, either express or implied. See the License for the
22 # specific language governing permissions and limitations
24 ######################################################################
33 if sys
.version_info
[0] >= 3:
35 from io
import StringIO
38 from cStringIO
import StringIO
42 logger
= logging
.getLogger()
46 # 'status -v' output looks like this:
48 # "%c%c%c%c%c%c%c %c %6s %6s %-12s %s\n"
50 # (Taken from 'print_status' in subversion/svn/status.c.)
52 # Here are the parameters. The middle number or string in parens is the
53 # match.group(), followed by a brief description of the field:
55 # - text status (1) (single letter)
56 # - prop status (1) (single letter)
57 # - wc-lockedness flag (2) (single letter: "L" or " ")
58 # - copied flag (3) (single letter: "+" or " ")
59 # - switched flag (4) (single letter: "S", "X" or " ")
60 # - repos lock status (5) (single letter: "K", "O", "B", "T", " ")
61 # - tree conflict flag (6) (single letter: "C" or " ")
65 # - out-of-date flag (7) (single letter: "*" or " ")
69 # - working revision ('wc_rev') (either digits or "-", "?" or " ")
73 # - last-changed revision (either digits or "?" or " ")
77 # - last author (optional string of non-whitespace
82 # - path ('path') (string of characters until newline)
84 # Working revision, last-changed revision, and last author are whitespace
85 # only if the item is missing.
87 _re_parse_status
= re
.compile('^([?!MACDRUGXI_~ ][MACDRUG_ ])'
94 '((?P<wc_rev>\d+|-|\?) +(\d|-|\?)+ +(\S+) +)?'
97 _re_parse_skipped
= re
.compile("^Skipped[^']* '(.+)'( --.*)?\n")
99 _re_parse_summarize
= re
.compile("^([MAD ][M ]) (.+)\n")
101 _re_parse_checkout
= re
.compile('^([RMAGCUDE_ B][MAGCUDE_ ])'
105 _re_parse_co_skipped
= re
.compile('^(Restored|Skipped|Removed external)'
106 '\s+\'(.+)\'(( --|: ).*)?')
107 _re_parse_co_restored
= re
.compile('^(Restored)\s+\'(.+)\'')
109 # Lines typically have a verb followed by whitespace then a path.
110 _re_parse_commit_ext
= re
.compile('^(([A-Za-z]+( [a-z]+)*)) \'(.+)\'( --.*)?')
111 _re_parse_commit
= re
.compile('^(\w+( \(bin\))?)\s+(.+)')
115 """Describes an existing or expected state of a working copy.
117 The primary metaphor here is a dictionary of paths mapping to instances
118 of StateItem, which describe each item in a working copy.
120 Note: the paths should be *relative* to the root of the working copy,
121 using '/' for the separator (see to_relpath()), and the root of the
122 working copy is identified by the empty path: ''.
125 def __init__(self
, wc_dir
, desc
):
126 "Create a State using the specified description."
127 assert isinstance(desc
, dict)
130 self
.desc
= desc
# dictionary: path -> StateItem
132 def add(self
, more_desc
):
133 "Add more state items into the State."
134 assert isinstance(more_desc
, dict)
136 self
.desc
.update(more_desc
)
138 def add_state(self
, parent
, state
):
139 "Import state items from a State object, reparent the items to PARENT."
140 assert isinstance(state
, State
)
142 if parent
and parent
[-1] != '/':
144 for path
, item
in state
.desc
.items():
146 self
.desc
[path
] = item
148 def remove(self
, *paths
):
149 "Remove PATHS from the state (the paths must exist)."
151 del self
.desc
[to_relpath(path
)]
153 def remove_subtree(self
, *paths
):
154 "Remove PATHS recursively from the state (the paths must exist)."
155 for subtree_path
in paths
:
156 subtree_path
= to_relpath(subtree_path
)
157 for path
, item
in self
.desc
.items():
158 if path
== subtree_path
or path
[:len(subtree_path
) + 1] == subtree_path
+ '/':
161 def copy(self
, new_root
=None):
162 """Make a deep copy of self. If NEW_ROOT is not None, then set the
163 copy's wc_dir NEW_ROOT instead of to self's wc_dir."""
165 for path
, item
in self
.desc
.items():
166 desc
[path
] = item
.copy()
168 new_root
= self
.wc_dir
169 return State(new_root
, desc
)
171 def tweak(self
, *args
, **kw
):
172 """Tweak the items' values.
174 Each argument in ARGS is the path of a StateItem that already exists in
175 this State. Each keyword argument in KW is a modifiable property of
178 The general form of this method is .tweak([paths...,] key=value...). If
179 one or more paths are provided, then those items' values are
180 modified. If no paths are given, then all items are modified.
185 path_ref
= self
.desc
[to_relpath(path
)]
187 e
.args
= ["Path '%s' not present in WC state descriptor" % path
]
191 for item
in self
.desc
.values():
194 def tweak_some(self
, filter, **kw
):
195 "Tweak the items for which the filter returns true."
196 for path
, item
in self
.desc
.items():
197 if list(filter(path
, item
)):
200 def subtree(self
, subtree_path
):
201 """Return a State object which is a deep copy of the sub-tree
202 beneath SUBTREE_PATH (which is assumed to be rooted at the tree of
203 this State object's WC_DIR). Exclude SUBTREE_PATH itself."""
205 for path
, item
in self
.desc
.items():
206 if path
[:len(subtree_path
) + 1] == subtree_path
+ '/':
207 desc
[path
[len(subtree_path
) + 1:]] = item
.copy()
208 return State(self
.wc_dir
, desc
)
210 def write_to_disk(self
, target_dir
):
211 """Construct a directory structure on disk, matching our state.
213 WARNING: any StateItem that does not have contents (.contents is None)
214 is assumed to be a directory.
216 if not os
.path
.exists(target_dir
):
217 os
.makedirs(target_dir
)
219 for path
, item
in self
.desc
.items():
220 fullpath
= os
.path
.join(target_dir
, path
)
221 if item
.contents
is None:
223 if not os
.path
.exists(fullpath
):
224 os
.makedirs(fullpath
)
228 # ensure its directory exists
229 dirpath
= os
.path
.dirname(fullpath
)
230 if not os
.path
.exists(dirpath
):
233 # write out the file contents now
234 open(fullpath
, 'wb').write(item
.contents
)
237 """Return a "normalized" version of self.
239 A normalized version has the following characteristics:
242 * paths use forward slashes
245 If self is already normalized, then it is returned. Otherwise, a
246 new State is constructed with (shallow) references to self's
249 If the caller needs a fully disjoint State, then use .copy() on
252 if self
.wc_dir
== '':
255 base
= to_relpath(os
.path
.normpath(self
.wc_dir
))
257 desc
= dict([(repos_join(base
, path
), item
)
258 for path
, item
in self
.desc
.items()])
259 return State('', desc
)
261 def compare(self
, other
):
262 """Compare this State against an OTHER State.
264 Three new set objects will be returned: CHANGED, UNIQUE_SELF, and
265 UNIQUE_OTHER. These contain paths of StateItems that are different
266 between SELF and OTHER, paths of items unique to SELF, and paths
267 of item that are unique to OTHER, respectively.
269 assert isinstance(other
, State
)
271 norm_self
= self
.normalize()
272 norm_other
= other
.normalize()
274 # fast-path the easy case
275 if norm_self
== norm_other
:
279 paths_self
= set(norm_self
.desc
.keys())
280 paths_other
= set(norm_other
.desc
.keys())
282 for path
in paths_self
.intersection(paths_other
):
283 if norm_self
.desc
[path
] != norm_other
.desc
[path
]:
286 return changed
, paths_self
- paths_other
, paths_other
- paths_self
288 def compare_and_display(self
, label
, other
):
289 """Compare this State against an OTHER State, and display differences.
291 Information will be written to stdout, displaying any differences
292 between the two states. LABEL will be used in the display. SELF is the
293 "expected" state, and OTHER is the "actual" state.
295 If any changes are detected/displayed, then SVNTreeUnequal is raised.
297 norm_self
= self
.normalize()
298 norm_other
= other
.normalize()
300 changed
, unique_self
, unique_other
= norm_self
.compare(norm_other
)
301 if not changed
and not unique_self
and not unique_other
:
304 # Use the shortest path as a way to find the "root-most" affected node.
305 def _shortest_path(path_set
):
307 for path
in path_set
:
308 if shortest
is None or len(path
) < len(shortest
):
313 path
= _shortest_path(changed
)
314 display_nodes(label
, path
, norm_self
.desc
[path
], norm_other
.desc
[path
])
316 path
= _shortest_path(unique_self
)
317 default_singleton_handler('actual ' + label
, path
, norm_self
.desc
[path
])
319 path
= _shortest_path(unique_other
)
320 default_singleton_handler('expected ' + label
, path
,
321 norm_other
.desc
[path
])
323 raise svntest
.tree
.SVNTreeUnequal
325 def tweak_for_entries_compare(self
):
326 for path
, item
in self
.desc
.copy().items():
328 # If this is an unversioned tree-conflict, remove it.
329 # These are only in their parents' THIS_DIR, they don't have entries.
330 if item
.status
[0] in '!?' and item
.treeconflict
== 'C':
333 # when reading the entry structures, we don't examine for text or
334 # property mods, so clear those flags. we also do not examine the
335 # filesystem, so we cannot detect missing or obstructed files.
336 if item
.status
[0] in 'M!~':
337 item
.status
= ' ' + item
.status
[1]
338 if item
.status
[1] == 'M':
339 item
.status
= item
.status
[0] + ' '
340 # under wc-ng terms, we may report a different revision than the
341 # backwards-compatible code should report. if there is a special
342 # value for compatibility, then use it.
343 if item
.entry_rev
is not None:
344 item
.wc_rev
= item
.entry_rev
345 item
.entry_rev
= None
346 # status might vary as well, e.g. when a directory is missing
347 if item
.entry_status
is not None:
348 item
.status
= item
.entry_status
349 item
.entry_status
= None
351 # we don't contact the repository, so our only information is what
352 # is in the working copy. 'K' means we have one and it matches the
353 # repos. 'O' means we don't have one but the repos says the item
354 # is locked by us, elsewhere. 'T' means we have one, and the repos
355 # has one, but it is now owned by somebody else. 'B' means we have
356 # one, but the repos does not.
358 # for each case of "we have one", set the writelocked state to 'K',
359 # and clear it to None for the others. this will match what is
360 # generated when we examine our working copy state.
361 if item
.writelocked
in 'TB':
362 item
.writelocked
= 'K'
363 elif item
.writelocked
== 'O':
364 item
.writelocked
= None
367 "Return an old-style tree (for compatibility purposes)."
369 for path
, item
in self
.desc
.items():
370 nodelist
.append(item
.as_node_tuple(os
.path
.join(self
.wc_dir
, path
)))
372 tree
= svntest
.tree
.build_generic_tree(nodelist
)
374 check
= tree
.as_state()
376 logger
.warn(pprint
.pformat(self
.desc
))
377 logger
.warn(pprint
.pformat(check
.desc
))
378 # STATE -> TREE -> STATE is lossy.
379 # In many cases, TREE -> STATE -> TREE is not.
380 # Even though our conversion from a TREE has lost some information, we
381 # may be able to verify that our lesser-STATE produces the same TREE.
382 svntest
.tree
.compare_trees('mismatch', tree
, check
.old_tree())
387 return str(self
.old_tree())
389 def __eq__(self
, other
):
390 if not isinstance(other
, State
):
392 norm_self
= self
.normalize()
393 norm_other
= other
.normalize()
394 return norm_self
.desc
== norm_other
.desc
396 def __ne__(self
, other
):
397 return not self
.__eq
__(other
)
400 def from_status(cls
, lines
):
401 """Create a State object from 'svn status' output."""
403 def not_space(value
):
404 if value
and value
!= ' ':
410 if line
.startswith('DBG:'):
413 # Quit when we hit an externals status announcement.
414 ### someday we can fix the externals tests to expect the additional
415 ### flood of externals status data.
416 if line
.startswith('Performing'):
419 match
= _re_parse_status
.search(line
)
420 if not match
or match
.group(10) == '-':
421 # ignore non-matching lines, or items that only exist on repos
424 item
= StateItem(status
=match
.group(1),
425 locked
=not_space(match
.group(2)),
426 copied
=not_space(match
.group(3)),
427 switched
=not_space(match
.group(4)),
428 writelocked
=not_space(match
.group(5)),
429 treeconflict
=not_space(match
.group(6)),
430 wc_rev
=not_space(match
.group('wc_rev')),
432 desc
[to_relpath(match
.group('path'))] = item
437 def from_skipped(cls
, lines
):
438 """Create a State object from 'Skipped' lines."""
442 if line
.startswith('DBG:'):
445 match
= _re_parse_skipped
.search(line
)
447 desc
[to_relpath(match
.group(1))] = StateItem()
452 def from_summarize(cls
, lines
):
453 """Create a State object from 'svn diff --summarize' lines."""
457 if line
.startswith('DBG:'):
460 match
= _re_parse_summarize
.search(line
)
462 desc
[to_relpath(match
.group(2))] = StateItem(status
=match
.group(1))
467 def from_checkout(cls
, lines
, include_skipped
=True):
468 """Create a State object from 'svn checkout' lines."""
471 re_extra
= _re_parse_co_skipped
473 re_extra
= _re_parse_co_restored
477 if line
.startswith('DBG:'):
480 match
= _re_parse_checkout
.search(line
)
482 if match
.group(3) != ' ':
483 treeconflict
= match
.group(3)
486 desc
[to_relpath(match
.group(4))] = StateItem(status
=match
.group(1),
487 treeconflict
=treeconflict
)
489 match
= re_extra
.search(line
)
491 desc
[to_relpath(match
.group(2))] = StateItem(verb
=match
.group(1))
496 def from_commit(cls
, lines
):
497 """Create a State object from 'svn commit' lines."""
501 if line
.startswith('DBG:') or line
.startswith('Transmitting'):
504 match
= _re_parse_commit_ext
.search(line
)
506 desc
[to_relpath(match
.group(4))] = StateItem(verb
=match
.group(1))
509 match
= _re_parse_commit
.search(line
)
511 desc
[to_relpath(match
.group(3))] = StateItem(verb
=match
.group(1))
516 def from_wc(cls
, base
, load_props
=False, ignore_svn
=True):
517 """Create a State object from a working copy.
519 Walks the tree at PATH, building a State based on the actual files
520 and directories found. If LOAD_PROPS is True, then the properties
521 will be loaded for all nodes (Very Expensive!). If IGNORE_SVN is
522 True, then the .svn subdirectories will be excluded from the State.
525 # we're going to walk the base, and the OS wants "."
529 dot_svn
= svntest
.main
.get_admin_name()
531 for dirpath
, dirs
, files
in os
.walk(base
):
532 parent
= path_to_key(dirpath
, base
)
533 if ignore_svn
and dot_svn
in dirs
:
535 for name
in dirs
+ files
:
536 node
= os
.path
.join(dirpath
, name
)
537 if os
.path
.isfile(node
):
538 contents
= open(node
, 'r').read()
541 desc
[repos_join(parent
, name
)] = StateItem(contents
=contents
)
544 paths
= [os
.path
.join(base
, to_ospath(p
)) for p
in desc
.keys()]
546 all_props
= svntest
.tree
.get_props(paths
)
547 for node
, props
in all_props
.items():
549 desc
['.'] = StateItem(props
=props
)
552 # 'svn proplist' strips './' from the paths. put it back on.
553 node
= os
.path
.join('.', node
)
554 desc
[path_to_key(node
, base
)].props
= props
559 def from_entries(cls
, base
):
560 """Create a State object from a working copy, via the old "entries" API.
562 Walks the tree at PATH, building a State based on the information
563 provided by the old entries API, as accessed via the 'entries-dump'
567 # we're going to walk the base, and the OS wants "."
570 if os
.path
.isfile(base
):
571 # a few tests run status on a single file. quick-and-dirty this. we
572 # really should analyze the entry (similar to below) to be general.
573 dirpath
, basename
= os
.path
.split(base
)
574 entries
= svntest
.main
.run_entriesdump(dirpath
)
576 to_relpath(base
): StateItem
.from_entry(entries
[basename
]),
580 dot_svn
= svntest
.main
.get_admin_name()
582 for dirpath
in svntest
.main
.run_entriesdump_subdirs(base
):
584 if base
== '.' and dirpath
!= '.':
585 dirpath
= '.' + os
.path
.sep
+ dirpath
587 entries
= svntest
.main
.run_entriesdump(dirpath
)
593 elif dirpath
.startswith('.' + os
.sep
):
594 parent
= to_relpath(dirpath
[2:])
596 parent
= to_relpath(dirpath
)
598 parent_url
= entries
[''].url
600 for name
, entry
in entries
.items():
601 # if the entry is marked as DELETED *and* it is something other than
602 # schedule-add, then skip it. we can add a new node "over" where a
603 # DELETED node lives.
604 if entry
.deleted
and entry
.schedule
!= 1:
606 # entries that are ABSENT don't show up in status
609 if name
and entry
.kind
== 2:
610 # stub subdirectory. leave a "missing" StateItem in here. note
611 # that we can't put the status as "! " because that gets tweaked
612 # out of our expected tree.
613 item
= StateItem(status
=' ', wc_rev
='?')
614 desc
[repos_join(parent
, name
)] = item
616 item
= StateItem
.from_entry(entry
)
618 desc
[repos_join(parent
, name
)] = item
619 implied_url
= repos_join(parent_url
, svn_uri_quote(name
))
621 item
._url
= entry
.url
# attach URL to directory StateItems
624 grandpa
, this_name
= repos_split(parent
)
626 implied_url
= repos_join(desc
[grandpa
]._url
,
627 svn_uri_quote(this_name
))
631 if implied_url
and implied_url
!= entry
.url
:
638 """Describes an individual item within a working copy.
640 Note that the location of this item is not specified. An external
641 mechanism, such as the State class, will provide location information
645 def __init__(self
, contents
=None, props
=None,
646 status
=None, verb
=None, wc_rev
=None,
647 entry_rev
=None, entry_status
=None,
648 locked
=None, copied
=None, switched
=None, writelocked
=None,
650 # provide an empty prop dict if it wasn't provided
654 ### keep/make these ints one day?
655 if wc_rev
is not None:
658 # Any attribute can be None if not relevant, unless otherwise stated.
660 # A string of content (if the node is a file).
661 self
.contents
= contents
662 # A dictionary mapping prop name to prop value; never None.
664 # A two-character string from the first two columns of 'svn status'.
666 # The action word such as 'Adding' printed by commands like 'svn update'.
668 # The base revision number of the node in the WC, as a string.
670 # These will be set when we expect the wc_rev/status to differ from those
671 # found in the entries code.
672 self
.entry_rev
= entry_rev
673 self
.entry_status
= entry_status
674 # For the following attributes, the value is the status character of that
675 # field from 'svn status', except using value None instead of status ' '.
678 self
.switched
= switched
679 self
.writelocked
= writelocked
680 # Value 'C' or ' ', or None as an expected status meaning 'do not check'.
681 self
.treeconflict
= treeconflict
684 "Make a deep copy of self."
686 vars(new
).update(vars(self
))
687 new
.props
= self
.props
.copy()
690 def tweak(self
, **kw
):
691 for name
, value
in kw
.items():
692 # Refine the revision args (for now) to ensure they are strings.
693 if value
is not None and name
== 'wc_rev':
695 setattr(self
, name
, value
)
697 def __eq__(self
, other
):
698 if not isinstance(other
, StateItem
):
700 v_self
= dict([(k
, v
) for k
, v
in vars(self
).items()
701 if not k
.startswith('_')])
702 v_other
= dict([(k
, v
) for k
, v
in vars(other
).items()
703 if not k
.startswith('_')])
704 if self
.treeconflict
is None:
705 v_other
= v_other
.copy()
706 v_other
['treeconflict'] = None
707 if other
.treeconflict
is None:
708 v_self
= v_self
.copy()
709 v_self
['treeconflict'] = None
710 return v_self
== v_other
712 def __ne__(self
, other
):
713 return not self
.__eq
__(other
)
715 def as_node_tuple(self
, path
):
717 if self
.status
is not None:
718 atts
['status'] = self
.status
719 if self
.verb
is not None:
720 atts
['verb'] = self
.verb
721 if self
.wc_rev
is not None:
722 atts
['wc_rev'] = self
.wc_rev
723 if self
.locked
is not None:
724 atts
['locked'] = self
.locked
725 if self
.copied
is not None:
726 atts
['copied'] = self
.copied
727 if self
.switched
is not None:
728 atts
['switched'] = self
.switched
729 if self
.writelocked
is not None:
730 atts
['writelocked'] = self
.writelocked
731 if self
.treeconflict
is not None:
732 atts
['treeconflict'] = self
.treeconflict
734 return (os
.path
.normpath(path
), self
.contents
, self
.props
, atts
)
737 def from_entry(cls
, entry
):
739 if entry
.schedule
== 1: # svn_wc_schedule_add
741 elif entry
.schedule
== 2: # svn_wc_schedule_delete
743 elif entry
.schedule
== 3: # svn_wc_schedule_replace
745 elif entry
.conflict_old
:
746 ### I'm assuming we only need to check one, rather than all conflict_*
749 ### is this the sufficient? guessing here w/o investigation.
751 status
= status
[0] + 'C'
762 if entry
.revision
== -1:
765 wc_rev
= entry
.revision
768 ### figure out switched
776 return cls(status
=status
,
781 writelocked
=writelocked
,
786 to_relpath
= to_ospath
= lambda path
: path
788 def to_relpath(path
):
789 """Return PATH but with all native path separators changed to '/'."""
790 return path
.replace(os
.sep
, '/')
792 """Return PATH but with each '/' changed to the native path separator."""
793 return path
.replace('/', os
.sep
)
796 def path_to_key(path
, base
):
797 """Return the relative path that represents the absolute path PATH under
798 the absolute path BASE. PATH must be a path under BASE. The returned
799 path has '/' separators."""
803 if base
.endswith(os
.sep
) or base
.endswith('/') or base
.endswith(':'):
804 # Special path format on Windows:
805 # 'C:/' Is a valid root which includes its separator ('C:/file')
806 # 'C:' is a valid root which isn't followed by a separator ('C:file')
808 # In this case, we don't need a separator between the base and the path.
811 # Account for a separator between the base and the relpath we're creating
814 assert path
.startswith(base
), "'%s' is not a prefix of '%s'" % (base
, path
)
815 return to_relpath(path
[len(base
):])
818 def repos_split(repos_relpath
):
819 """Split a repos path into its directory and basename parts."""
820 idx
= repos_relpath
.rfind('/')
822 return '', repos_relpath
823 return repos_relpath
[:idx
], repos_relpath
[idx
+1:]
826 def repos_join(base
, path
):
827 """Join two repos paths. This generally works for URLs too."""
832 return base
+ '/' + path
835 def svn_uri_quote(url
):
836 # svn defines a different set of "safe" characters than Python does, so
837 # we need to avoid escaping them. see subr/path.c:uri_char_validity[]
838 return urllib
.quote(url
, "!$&'()*+,-./:=@_~")
843 def open_wc_db(local_path
):
844 """Open the SQLite DB for the WC path LOCAL_PATH.
845 Return (DB object, WC root path, WC relpath of LOCAL_PATH)."""
846 dot_svn
= svntest
.main
.get_admin_name()
847 root_path
= local_path
851 db_path
= os
.path
.join(root_path
, dot_svn
, 'wc.db')
853 db
= svntest
.sqlite3
.connect(db_path
)
856 head
, tail
= os
.path
.split(root_path
)
857 if head
== root_path
:
858 raise svntest
.Failure("No DB for " + local_path
)
860 relpath
= os
.path
.join(tail
, relpath
).replace(os
.path
.sep
, '/').rstrip('/')
862 return db
, root_path
, relpath
866 def text_base_path(file_path
):
867 """Return the path to the text-base file for the versioned file
870 info
= svntest
.actions
.run_and_parse_info(file_path
)[0]
872 checksum
= info
['Checksum']
873 db
, root_path
, relpath
= open_wc_db(file_path
)
875 # Calculate single DB location
876 dot_svn
= svntest
.main
.get_admin_name()
877 fn
= os
.path
.join(root_path
, dot_svn
, 'pristine', checksum
[0:2], checksum
)
879 # For SVN_WC__VERSION < 29
880 if os
.path
.isfile(fn
):
883 # For SVN_WC__VERSION >= 29
884 if os
.path
.isfile(fn
+ ".svn-base"):
885 return fn
+ ".svn-base"
887 raise svntest
.Failure("No pristine text for " + relpath
)
891 ### probably toss these at some point. or major rework. or something.
892 ### just bootstrapping some changes for now.
895 def item_to_node(path
, item
):
896 tree
= svntest
.tree
.build_generic_tree([item
.as_node_tuple(path
)])
898 assert len(tree
.children
) == 1
899 tree
= tree
.children
[0]
902 ### yanked from tree.compare_trees()
903 def display_nodes(label
, path
, expected
, actual
):
904 'Display two nodes, expected and actual.'
905 expected
= item_to_node(path
, expected
)
906 actual
= item_to_node(path
, actual
)
909 o
.write("=============================================================\n")
910 o
.write("Expected '%s' and actual '%s' in %s tree are different!\n"
911 % (expected
.name
, actual
.name
, label
))
912 o
.write("=============================================================\n")
913 o
.write("EXPECTED NODE TO BE:\n")
914 o
.write("=============================================================\n")
916 o
.write("=============================================================\n")
917 o
.write("ACTUAL NODE FOUND:\n")
918 o
.write("=============================================================\n")
921 logger
.warn(o
.getvalue())
924 ### yanked from tree.py
925 def default_singleton_handler(description
, path
, item
):
926 node
= item_to_node(path
, item
)
927 logger
.warn("Couldn't find node '%s' in %s tree" % (node
.name
, description
))
930 logger
.warn(o
.getvalue())
932 raise svntest
.tree
.SVNTreeUnequal