Bring CHANGES up to date.
[cvs2svn.git] / svntest / wc.py
blobaede391394be9aeec4dffc01c03018eac5143608
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
23 # under the License.
24 ######################################################################
26 import os
27 import sys
28 import re
29 import urllib
30 import logging
31 import pprint
33 if sys.version_info[0] >= 3:
34 # Python >=3.0
35 from io import StringIO
36 else:
37 # Python <3.0
38 from cStringIO import StringIO
40 import svntest
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 " ")
63 # [one space]
65 # - out-of-date flag (7) (single letter: "*" or " ")
67 # [three spaces]
69 # - working revision ('wc_rev') (either digits or "-", "?" or " ")
71 # [one space]
73 # - last-changed revision (either digits or "?" or " ")
75 # [one space]
77 # - last author (optional string of non-whitespace
78 # characters)
80 # [spaces]
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_ ])'
88 '([L ])'
89 '([+ ])'
90 '([SX ])'
91 '([KOBT ])'
92 '([C ]) '
93 '([* ]) +'
94 '((?P<wc_rev>\d+|-|\?) +(\d|-|\?)+ +(\S+) +)?'
95 '(?P<path>.+)$')
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_ ])'
102 '([B ])'
103 '([CAUD ])\s+'
104 '(.+)')
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+(.+)')
114 class State:
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)
129 self.wc_dir = wc_dir
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] != '/':
143 parent += '/'
144 for path, item in state.desc.items():
145 path = parent + path
146 self.desc[path] = item
148 def remove(self, *paths):
149 "Remove PATHS from the state (the paths must exist)."
150 for path in paths:
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 + '/':
159 del self.desc[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."""
164 desc = { }
165 for path, item in self.desc.items():
166 desc[path] = item.copy()
167 if new_root is None:
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
176 StateItem.
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.
182 if args:
183 for path in args:
184 try:
185 path_ref = self.desc[to_relpath(path)]
186 except KeyError, e:
187 e.args = ["Path '%s' not present in WC state descriptor" % path]
188 raise
189 path_ref.tweak(**kw)
190 else:
191 for item in self.desc.values():
192 item.tweak(**kw)
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)):
198 item.tweak(**kw)
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."""
204 desc = { }
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:
222 # a directory
223 if not os.path.exists(fullpath):
224 os.makedirs(fullpath)
225 else:
226 # a file
228 # ensure its directory exists
229 dirpath = os.path.dirname(fullpath)
230 if not os.path.exists(dirpath):
231 os.makedirs(dirpath)
233 # write out the file contents now
234 open(fullpath, 'wb').write(item.contents)
236 def normalize(self):
237 """Return a "normalized" version of self.
239 A normalized version has the following characteristics:
241 * wc_dir == ''
242 * paths use forward slashes
243 * paths are relative
245 If self is already normalized, then it is returned. Otherwise, a
246 new State is constructed with (shallow) references to self's
247 StateItem instances.
249 If the caller needs a fully disjoint State, then use .copy() on
250 the result.
252 if self.wc_dir == '':
253 return self
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:
276 fs = frozenset()
277 return fs, fs, fs
279 paths_self = set(norm_self.desc.keys())
280 paths_other = set(norm_other.desc.keys())
281 changed = set()
282 for path in paths_self.intersection(paths_other):
283 if norm_self.desc[path] != norm_other.desc[path]:
284 changed.add(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:
302 return
304 # Use the shortest path as a way to find the "root-most" affected node.
305 def _shortest_path(path_set):
306 shortest = None
307 for path in path_set:
308 if shortest is None or len(path) < len(shortest):
309 shortest = path
310 return shortest
312 if changed:
313 path = _shortest_path(changed)
314 display_nodes(label, path, norm_self.desc[path], norm_other.desc[path])
315 elif unique_self:
316 path = _shortest_path(unique_self)
317 default_singleton_handler('actual ' + label, path, norm_self.desc[path])
318 elif unique_other:
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():
327 if item.status:
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':
331 del self.desc[path]
332 else:
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
350 if item.writelocked:
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
366 def old_tree(self):
367 "Return an old-style tree (for compatibility purposes)."
368 nodelist = [ ]
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)
373 if 0:
374 check = tree.as_state()
375 if self != check:
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())
384 return tree
386 def __str__(self):
387 return str(self.old_tree())
389 def __eq__(self, other):
390 if not isinstance(other, State):
391 return False
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)
399 @classmethod
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 != ' ':
405 return value
406 return None
408 desc = { }
409 for line in lines:
410 if line.startswith('DBG:'):
411 continue
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'):
417 break
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
422 continue
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
434 return cls('', desc)
436 @classmethod
437 def from_skipped(cls, lines):
438 """Create a State object from 'Skipped' lines."""
440 desc = { }
441 for line in lines:
442 if line.startswith('DBG:'):
443 continue
445 match = _re_parse_skipped.search(line)
446 if match:
447 desc[to_relpath(match.group(1))] = StateItem()
449 return cls('', desc)
451 @classmethod
452 def from_summarize(cls, lines):
453 """Create a State object from 'svn diff --summarize' lines."""
455 desc = { }
456 for line in lines:
457 if line.startswith('DBG:'):
458 continue
460 match = _re_parse_summarize.search(line)
461 if match:
462 desc[to_relpath(match.group(2))] = StateItem(status=match.group(1))
464 return cls('', desc)
466 @classmethod
467 def from_checkout(cls, lines, include_skipped=True):
468 """Create a State object from 'svn checkout' lines."""
470 if include_skipped:
471 re_extra = _re_parse_co_skipped
472 else:
473 re_extra = _re_parse_co_restored
475 desc = { }
476 for line in lines:
477 if line.startswith('DBG:'):
478 continue
480 match = _re_parse_checkout.search(line)
481 if match:
482 if match.group(3) != ' ':
483 treeconflict = match.group(3)
484 else:
485 treeconflict = None
486 desc[to_relpath(match.group(4))] = StateItem(status=match.group(1),
487 treeconflict=treeconflict)
488 else:
489 match = re_extra.search(line)
490 if match:
491 desc[to_relpath(match.group(2))] = StateItem(verb=match.group(1))
493 return cls('', desc)
495 @classmethod
496 def from_commit(cls, lines):
497 """Create a State object from 'svn commit' lines."""
499 desc = { }
500 for line in lines:
501 if line.startswith('DBG:') or line.startswith('Transmitting'):
502 continue
504 match = _re_parse_commit_ext.search(line)
505 if match:
506 desc[to_relpath(match.group(4))] = StateItem(verb=match.group(1))
507 continue
509 match = _re_parse_commit.search(line)
510 if match:
511 desc[to_relpath(match.group(3))] = StateItem(verb=match.group(1))
513 return cls('', desc)
515 @classmethod
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.
524 if not base:
525 # we're going to walk the base, and the OS wants "."
526 base = '.'
528 desc = { }
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:
534 dirs.remove(dot_svn)
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()
539 else:
540 contents = None
541 desc[repos_join(parent, name)] = StateItem(contents=contents)
543 if load_props:
544 paths = [os.path.join(base, to_ospath(p)) for p in desc.keys()]
545 paths.append(base)
546 all_props = svntest.tree.get_props(paths)
547 for node, props in all_props.items():
548 if node == base:
549 desc['.'] = StateItem(props=props)
550 else:
551 if base == '.':
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
556 return cls('', desc)
558 @classmethod
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'
564 program.
566 if not base:
567 # we're going to walk the base, and the OS wants "."
568 base = '.'
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)
575 return cls('', {
576 to_relpath(base): StateItem.from_entry(entries[basename]),
579 desc = { }
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)
588 if entries is None:
589 continue
591 if dirpath == '.':
592 parent = ''
593 elif dirpath.startswith('.' + os.sep):
594 parent = to_relpath(dirpath[2:])
595 else:
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:
605 continue
606 # entries that are ABSENT don't show up in status
607 if entry.absent:
608 continue
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
615 continue
616 item = StateItem.from_entry(entry)
617 if name:
618 desc[repos_join(parent, name)] = item
619 implied_url = repos_join(parent_url, svn_uri_quote(name))
620 else:
621 item._url = entry.url # attach URL to directory StateItems
622 desc[parent] = item
624 grandpa, this_name = repos_split(parent)
625 if grandpa in desc:
626 implied_url = repos_join(desc[grandpa]._url,
627 svn_uri_quote(this_name))
628 else:
629 implied_url = None
631 if implied_url and implied_url != entry.url:
632 item.switched = 'S'
634 return cls('', desc)
637 class StateItem:
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
642 for each item.
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,
649 treeconflict=None):
650 # provide an empty prop dict if it wasn't provided
651 if props is None:
652 props = { }
654 ### keep/make these ints one day?
655 if wc_rev is not None:
656 wc_rev = str(wc_rev)
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.
663 self.props = props
664 # A two-character string from the first two columns of 'svn status'.
665 self.status = status
666 # The action word such as 'Adding' printed by commands like 'svn update'.
667 self.verb = verb
668 # The base revision number of the node in the WC, as a string.
669 self.wc_rev = wc_rev
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 ' '.
676 self.locked = locked
677 self.copied = copied
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
683 def copy(self):
684 "Make a deep copy of self."
685 new = StateItem()
686 vars(new).update(vars(self))
687 new.props = self.props.copy()
688 return new
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':
694 value = str(value)
695 setattr(self, name, value)
697 def __eq__(self, other):
698 if not isinstance(other, StateItem):
699 return False
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):
716 atts = { }
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)
736 @classmethod
737 def from_entry(cls, entry):
738 status = ' '
739 if entry.schedule == 1: # svn_wc_schedule_add
740 status = 'A '
741 elif entry.schedule == 2: # svn_wc_schedule_delete
742 status = 'D '
743 elif entry.schedule == 3: # svn_wc_schedule_replace
744 status = 'R '
745 elif entry.conflict_old:
746 ### I'm assuming we only need to check one, rather than all conflict_*
747 status = 'C '
749 ### is this the sufficient? guessing here w/o investigation.
750 if entry.prejfile:
751 status = status[0] + 'C'
753 if entry.locked:
754 locked = 'L'
755 else:
756 locked = None
758 if entry.copied:
759 wc_rev = '-'
760 copied = '+'
761 else:
762 if entry.revision == -1:
763 wc_rev = '?'
764 else:
765 wc_rev = entry.revision
766 copied = None
768 ### figure out switched
769 switched = None
771 if entry.lock_token:
772 writelocked = 'K'
773 else:
774 writelocked = None
776 return cls(status=status,
777 wc_rev=wc_rev,
778 locked=locked,
779 copied=copied,
780 switched=switched,
781 writelocked=writelocked,
785 if os.sep == '/':
786 to_relpath = to_ospath = lambda path: path
787 else:
788 def to_relpath(path):
789 """Return PATH but with all native path separators changed to '/'."""
790 return path.replace(os.sep, '/')
791 def to_ospath(path):
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."""
800 if path == base:
801 return ''
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.
809 pass
810 else:
811 # Account for a separator between the base and the relpath we're creating
812 base += os.sep
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('/')
821 if idx == -1:
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."""
828 if base == '':
829 return path
830 if path == '':
831 return base
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, "!$&'()*+,-./:=@_~")
841 # ------------
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
848 relpath = ''
850 while True:
851 db_path = os.path.join(root_path, dot_svn, 'wc.db')
852 try:
853 db = svntest.sqlite3.connect(db_path)
854 break
855 except: pass
856 head, tail = os.path.split(root_path)
857 if head == root_path:
858 raise svntest.Failure("No DB for " + local_path)
859 root_path = head
860 relpath = os.path.join(tail, relpath).replace(os.path.sep, '/').rstrip('/')
862 return db, root_path, relpath
864 # ------------
866 def text_base_path(file_path):
867 """Return the path to the text-base file for the versioned file
868 FILE_PATH."""
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):
881 return 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)
890 # ------------
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)])
897 while tree.children:
898 assert len(tree.children) == 1
899 tree = tree.children[0]
900 return tree
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)
908 o = StringIO()
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")
915 expected.pprint(o)
916 o.write("=============================================================\n")
917 o.write("ACTUAL NODE FOUND:\n")
918 o.write("=============================================================\n")
919 actual.pprint(o)
921 logger.warn(o.getvalue())
922 o.close()
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))
928 o = StringIO()
929 node.pprint(o)
930 logger.warn(o.getvalue())
931 o.close()
932 raise svntest.tree.SVNTreeUnequal