Make the target arguments to sbox.simple_rm() etc. be relpaths relative to
[svnrdump.git] / svntest / wc.py
blob54de55dd3e1f42f0ef4d1c9a69e2e524315f94e9
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
31 import svntest
35 # 'status -v' output looks like this:
37 # "%c%c%c%c%c%c%c %c %6s %6s %-12s %s\n"
39 # (Taken from 'print_status' in subversion/svn/status.c.)
41 # Here are the parameters. The middle number or string in parens is the
42 # match.group(), followed by a brief description of the field:
44 # - text status (1) (single letter)
45 # - prop status (1) (single letter)
46 # - wc-lockedness flag (2) (single letter: "L" or " ")
47 # - copied flag (3) (single letter: "+" or " ")
48 # - switched flag (4) (single letter: "S", "X" or " ")
49 # - repos lock status (5) (single letter: "K", "O", "B", "T", " ")
50 # - tree conflict flag (6) (single letter: "C" or " ")
52 # [one space]
54 # - out-of-date flag (7) (single letter: "*" or " ")
56 # [three spaces]
58 # - working revision ('wc_rev') (either digits or "-", "?" or " ")
60 # [one space]
62 # - last-changed revision (either digits or "?" or " ")
64 # [one space]
66 # - last author (optional string of non-whitespace
67 # characters)
69 # [spaces]
71 # - path ('path') (string of characters until newline)
73 # Working revision, last-changed revision, and last author are whitespace
74 # only if the item is missing.
76 _re_parse_status = re.compile('^([?!MACDRUGI_~ ][MACDRUG_ ])'
77 '([L ])'
78 '([+ ])'
79 '([SX ])'
80 '([KOBT ])'
81 '([C ]) '
82 '([* ]) +'
83 '((?P<wc_rev>\d+|-|\?) +(\d|-|\?)+ +(\S+) +)?'
84 '(?P<path>.+)$')
86 _re_parse_skipped = re.compile("^Skipped.* '(.+)'\n")
88 _re_parse_summarize = re.compile("^([MAD ][M ]) (.+)\n")
90 _re_parse_checkout = re.compile('^([RMAGCUDE_ ][MAGCUDE_ ])'
91 '([B ])'
92 '([C ])\s+'
93 '(.+)')
94 _re_parse_co_skipped = re.compile('^(Restored|Skipped)\s+\'(.+)\'')
95 _re_parse_co_restored = re.compile('^(Restored)\s+\'(.+)\'')
97 # Lines typically have a verb followed by whitespace then a path.
98 _re_parse_commit = re.compile('^(\w+( \(bin\))?)\s+(.+)')
101 class State:
102 """Describes an existing or expected state of a working copy.
104 The primary metaphor here is a dictionary of paths mapping to instances
105 of StateItem, which describe each item in a working copy.
107 Note: the paths should be *relative* to the root of the working copy,
108 using '/' for the separator (see to_relpath()), and the root of the
109 working copy is identified by the empty path: ''.
112 def __init__(self, wc_dir, desc):
113 "Create a State using the specified description."
114 assert isinstance(desc, dict)
116 self.wc_dir = wc_dir
117 self.desc = desc # dictionary: path -> StateItem
119 def add(self, more_desc):
120 "Add more state items into the State."
121 assert isinstance(more_desc, dict)
123 self.desc.update(more_desc)
125 def add_state(self, parent, state):
126 "Import state items from a State object, reparent the items to PARENT."
127 assert isinstance(state, State)
129 if parent and parent[-1] != '/':
130 parent += '/'
131 for path, item in state.desc.items():
132 path = parent + path
133 self.desc[path] = item
135 def remove(self, *paths):
136 "Remove a path from the state (the path must exist)."
137 for path in paths:
138 del self.desc[to_relpath(path)]
140 def copy(self, new_root=None):
141 """Make a deep copy of self. If NEW_ROOT is not None, then set the
142 copy's wc_dir NEW_ROOT instead of to self's wc_dir."""
143 desc = { }
144 for path, item in self.desc.items():
145 desc[path] = item.copy()
146 if new_root is None:
147 new_root = self.wc_dir
148 return State(new_root, desc)
150 def tweak(self, *args, **kw):
151 """Tweak the items' values.
153 Each argument in ARGS is the path of a StateItem that already exists in
154 this State. Each keyword argument in KW is a modifiable property of
155 StateItem.
157 The general form of this method is .tweak([paths...,] key=value...). If
158 one or more paths are provided, then those items' values are
159 modified. If no paths are given, then all items are modified.
161 if args:
162 for path in args:
163 try:
164 path_ref = self.desc[to_relpath(path)]
165 except KeyError, e:
166 e.args = ["Path '%s' not present in WC state descriptor" % path]
167 raise
168 path_ref.tweak(**kw)
169 else:
170 for item in self.desc.values():
171 item.tweak(**kw)
173 def tweak_some(self, filter, **kw):
174 "Tweak the items for which the filter returns true."
175 for path, item in self.desc.items():
176 if list(filter(path, item)):
177 item.tweak(**kw)
179 def subtree(self, subtree_path):
180 """Return a State object which is a deep copy of the sub-tree
181 identified by SUBTREE_PATH (which is assumed to contain only one
182 element rooted at the tree of this State object's WC_DIR)."""
183 desc = { }
184 for path, item in self.desc.items():
185 path_elements = path.split("/")
186 if len(path_elements) > 1 and path_elements[0] == subtree_path:
187 desc["/".join(path_elements[1:])] = item.copy()
188 return State(self.wc_dir, desc)
190 def write_to_disk(self, target_dir):
191 """Construct a directory structure on disk, matching our state.
193 WARNING: any StateItem that does not have contents (.contents is None)
194 is assumed to be a directory.
196 if not os.path.exists(target_dir):
197 os.makedirs(target_dir)
199 for path, item in self.desc.items():
200 fullpath = os.path.join(target_dir, path)
201 if item.contents is None:
202 # a directory
203 if not os.path.exists(fullpath):
204 os.makedirs(fullpath)
205 else:
206 # a file
208 # ensure its directory exists
209 dirpath = os.path.dirname(fullpath)
210 if not os.path.exists(dirpath):
211 os.makedirs(dirpath)
213 # write out the file contents now
214 open(fullpath, 'wb').write(item.contents)
216 def normalize(self):
217 """Return a "normalized" version of self.
219 A normalized version has the following characteristics:
221 * wc_dir == ''
222 * paths use forward slashes
223 * paths are relative
225 If self is already normalized, then it is returned. Otherwise, a
226 new State is constructed with (shallow) references to self's
227 StateItem instances.
229 If the caller needs a fully disjoint State, then use .copy() on
230 the result.
232 if self.wc_dir == '':
233 return self
235 base = to_relpath(os.path.normpath(self.wc_dir))
237 desc = dict([(repos_join(base, path), item)
238 for path, item in self.desc.items()])
239 return State('', desc)
241 def compare(self, other):
242 """Compare this State against an OTHER State.
244 Three new set objects will be returned: CHANGED, UNIQUE_SELF, and
245 UNIQUE_OTHER. These contain paths of StateItems that are different
246 between SELF and OTHER, paths of items unique to SELF, and paths
247 of item that are unique to OTHER, respectively.
249 assert isinstance(other, State)
251 norm_self = self.normalize()
252 norm_other = other.normalize()
254 # fast-path the easy case
255 if norm_self == norm_other:
256 fs = frozenset()
257 return fs, fs, fs
259 paths_self = set(norm_self.desc.keys())
260 paths_other = set(norm_other.desc.keys())
261 changed = set()
262 for path in paths_self.intersection(paths_other):
263 if norm_self.desc[path] != norm_other.desc[path]:
264 changed.add(path)
266 return changed, paths_self - paths_other, paths_other - paths_self
268 def compare_and_display(self, label, other):
269 """Compare this State against an OTHER State, and display differences.
271 Information will be written to stdout, displaying any differences
272 between the two states. LABEL will be used in the display. SELF is the
273 "expected" state, and OTHER is the "actual" state.
275 If any changes are detected/displayed, then SVNTreeUnequal is raised.
277 norm_self = self.normalize()
278 norm_other = other.normalize()
280 changed, unique_self, unique_other = norm_self.compare(norm_other)
281 if not changed and not unique_self and not unique_other:
282 return
284 # Use the shortest path as a way to find the "root-most" affected node.
285 def _shortest_path(path_set):
286 shortest = None
287 for path in path_set:
288 if shortest is None or len(path) < len(shortest):
289 shortest = path
290 return shortest
292 if changed:
293 path = _shortest_path(changed)
294 display_nodes(label, path, norm_self.desc[path], norm_other.desc[path])
295 elif unique_self:
296 path = _shortest_path(unique_self)
297 default_singleton_handler('actual ' + label, path, norm_self.desc[path])
298 elif unique_other:
299 path = _shortest_path(unique_other)
300 default_singleton_handler('expected ' + label, path,
301 norm_other.desc[path])
303 raise svntest.tree.SVNTreeUnequal
305 def tweak_for_entries_compare(self):
306 for path, item in self.desc.copy().items():
307 if item.status:
308 # If this is an unversioned tree-conflict, remove it.
309 # These are only in their parents' THIS_DIR, they don't have entries.
310 if item.status[0] in '!?' and item.treeconflict == 'C':
311 del self.desc[path]
312 else:
313 # when reading the entry structures, we don't examine for text or
314 # property mods, so clear those flags. we also do not examine the
315 # filesystem, so we cannot detect missing or obstructed files.
316 if item.status[0] in 'M!~':
317 item.status = ' ' + item.status[1]
318 if item.status[1] == 'M':
319 item.status = item.status[0] + ' '
320 # under wc-ng terms, we may report a different revision than the
321 # backwards-compatible code should report. if there is a special
322 # value for compatibility, then use it.
323 if item.entry_rev is not None:
324 item.wc_rev = item.entry_rev
325 item.entry_rev = None
326 # status might vary as well, e.g. when a directory is missing
327 if item.entry_status is not None:
328 item.status = item.entry_status
329 item.entry_status = None
330 if item.writelocked:
331 # we don't contact the repository, so our only information is what
332 # is in the working copy. 'K' means we have one and it matches the
333 # repos. 'O' means we don't have one but the repos says the item
334 # is locked by us, elsewhere. 'T' means we have one, and the repos
335 # has one, but it is now owned by somebody else. 'B' means we have
336 # one, but the repos does not.
338 # for each case of "we have one", set the writelocked state to 'K',
339 # and clear it to None for the others. this will match what is
340 # generated when we examine our working copy state.
341 if item.writelocked in 'TB':
342 item.writelocked = 'K'
343 elif item.writelocked == 'O':
344 item.writelocked = None
346 def old_tree(self):
347 "Return an old-style tree (for compatibility purposes)."
348 nodelist = [ ]
349 for path, item in self.desc.items():
350 nodelist.append(item.as_node_tuple(os.path.join(self.wc_dir, path)))
352 tree = svntest.tree.build_generic_tree(nodelist)
353 if 0:
354 check = tree.as_state()
355 if self != check:
356 import pprint
357 pprint.pprint(self.desc)
358 pprint.pprint(check.desc)
359 # STATE -> TREE -> STATE is lossy.
360 # In many cases, TREE -> STATE -> TREE is not.
361 # Even though our conversion from a TREE has lost some information, we
362 # may be able to verify that our lesser-STATE produces the same TREE.
363 svntest.tree.compare_trees('mismatch', tree, check.old_tree())
365 return tree
367 def __str__(self):
368 return str(self.old_tree())
370 def __eq__(self, other):
371 if not isinstance(other, State):
372 return False
373 norm_self = self.normalize()
374 norm_other = other.normalize()
375 return norm_self.desc == norm_other.desc
377 def __ne__(self, other):
378 return not self.__eq__(other)
380 @classmethod
381 def from_status(cls, lines):
382 """Create a State object from 'svn status' output."""
384 def not_space(value):
385 if value and value != ' ':
386 return value
387 return None
389 desc = { }
390 for line in lines:
391 if line.startswith('DBG:'):
392 continue
394 # Quit when we hit an externals status announcement.
395 ### someday we can fix the externals tests to expect the additional
396 ### flood of externals status data.
397 if line.startswith('Performing'):
398 break
400 match = _re_parse_status.search(line)
401 if not match or match.group(10) == '-':
402 # ignore non-matching lines, or items that only exist on repos
403 continue
405 item = StateItem(status=match.group(1),
406 locked=not_space(match.group(2)),
407 copied=not_space(match.group(3)),
408 switched=not_space(match.group(4)),
409 writelocked=not_space(match.group(5)),
410 treeconflict=not_space(match.group(6)),
411 wc_rev=not_space(match.group('wc_rev')),
413 desc[to_relpath(match.group('path'))] = item
415 return cls('', desc)
417 @classmethod
418 def from_skipped(cls, lines):
419 """Create a State object from 'Skipped' lines."""
421 desc = { }
422 for line in lines:
423 if line.startswith('DBG:'):
424 continue
426 match = _re_parse_skipped.search(line)
427 if match:
428 desc[to_relpath(match.group(1))] = StateItem()
430 return cls('', desc)
432 @classmethod
433 def from_summarize(cls, lines):
434 """Create a State object from 'svn diff --summarize' lines."""
436 desc = { }
437 for line in lines:
438 if line.startswith('DBG:'):
439 continue
441 match = _re_parse_summarize.search(line)
442 if match:
443 desc[to_relpath(match.group(2))] = StateItem(status=match.group(1))
445 return cls('', desc)
447 @classmethod
448 def from_checkout(cls, lines, include_skipped=True):
449 """Create a State object from 'svn checkout' lines."""
451 if include_skipped:
452 re_extra = _re_parse_co_skipped
453 else:
454 re_extra = _re_parse_co_restored
456 desc = { }
457 for line in lines:
458 if line.startswith('DBG:'):
459 continue
461 match = _re_parse_checkout.search(line)
462 if match:
463 if match.group(3) == 'C':
464 treeconflict = 'C'
465 else:
466 treeconflict = None
467 desc[to_relpath(match.group(4))] = StateItem(status=match.group(1),
468 treeconflict=treeconflict)
469 else:
470 match = re_extra.search(line)
471 if match:
472 desc[to_relpath(match.group(2))] = StateItem(verb=match.group(1))
474 return cls('', desc)
476 @classmethod
477 def from_commit(cls, lines):
478 """Create a State object from 'svn commit' lines."""
480 desc = { }
481 for line in lines:
482 if line.startswith('DBG:') or line.startswith('Transmitting'):
483 continue
485 match = _re_parse_commit.search(line)
486 if match:
487 desc[to_relpath(match.group(3))] = StateItem(verb=match.group(1))
489 return cls('', desc)
491 @classmethod
492 def from_wc(cls, base, load_props=False, ignore_svn=True):
493 """Create a State object from a working copy.
495 Walks the tree at PATH, building a State based on the actual files
496 and directories found. If LOAD_PROPS is True, then the properties
497 will be loaded for all nodes (Very Expensive!). If IGNORE_SVN is
498 True, then the .svn subdirectories will be excluded from the State.
500 if not base:
501 # we're going to walk the base, and the OS wants "."
502 base = '.'
504 desc = { }
505 dot_svn = svntest.main.get_admin_name()
507 for dirpath, dirs, files in os.walk(base):
508 parent = path_to_key(dirpath, base)
509 if ignore_svn and dot_svn in dirs:
510 dirs.remove(dot_svn)
511 for name in dirs + files:
512 node = os.path.join(dirpath, name)
513 if os.path.isfile(node):
514 contents = open(node, 'r').read()
515 else:
516 contents = None
517 desc[repos_join(parent, name)] = StateItem(contents=contents)
519 if load_props:
520 paths = [os.path.join(base, to_ospath(p)) for p in desc.keys()]
521 paths.append(base)
522 all_props = svntest.tree.get_props(paths)
523 for node, props in all_props.items():
524 if node == base:
525 desc['.'] = StateItem(props=props)
526 else:
527 if base == '.':
528 # 'svn proplist' strips './' from the paths. put it back on.
529 node = os.path.join('.', node)
530 desc[path_to_key(node, base)].props = props
532 return cls('', desc)
534 @classmethod
535 def from_entries(cls, base):
536 """Create a State object from a working copy, via the old "entries" API.
538 Walks the tree at PATH, building a State based on the information
539 provided by the old entries API, as accessed via the 'entries-dump'
540 program.
542 if not base:
543 # we're going to walk the base, and the OS wants "."
544 base = '.'
546 if os.path.isfile(base):
547 # a few tests run status on a single file. quick-and-dirty this. we
548 # really should analyze the entry (similar to below) to be general.
549 dirpath, basename = os.path.split(base)
550 entries = svntest.main.run_entriesdump(dirpath)
551 return cls('', {
552 to_relpath(base): StateItem.from_entry(entries[basename]),
555 desc = { }
556 dot_svn = svntest.main.get_admin_name()
558 for dirpath in svntest.main.run_entriesdump_subdirs(base):
560 if base == '.' and dirpath != '.':
561 dirpath = '.' + os.path.sep + dirpath
563 entries = svntest.main.run_entriesdump(dirpath)
564 if entries is None:
565 continue
567 if dirpath == '.':
568 parent = ''
569 elif dirpath.startswith('.' + os.sep):
570 parent = to_relpath(dirpath[2:])
571 else:
572 parent = to_relpath(dirpath)
574 parent_url = entries[''].url
576 for name, entry in entries.items():
577 # if the entry is marked as DELETED *and* it is something other than
578 # schedule-add, then skip it. we can add a new node "over" where a
579 # DELETED node lives.
580 if entry.deleted and entry.schedule != 1:
581 continue
582 # entries that are ABSENT don't show up in status
583 if entry.absent:
584 continue
585 if name and entry.kind == 2:
586 # stub subdirectory. leave a "missing" StateItem in here. note
587 # that we can't put the status as "! " because that gets tweaked
588 # out of our expected tree.
589 item = StateItem(status=' ', wc_rev='?')
590 desc[repos_join(parent, name)] = item
591 continue
592 item = StateItem.from_entry(entry)
593 if name:
594 desc[repos_join(parent, name)] = item
595 implied_url = repos_join(parent_url, svn_url_quote(name))
596 else:
597 item._url = entry.url # attach URL to directory StateItems
598 desc[parent] = item
600 grandpa, this_name = repos_split(parent)
601 if grandpa in desc:
602 implied_url = repos_join(desc[grandpa]._url,
603 svn_url_quote(this_name))
604 else:
605 implied_url = None
607 if implied_url and implied_url != entry.url:
608 item.switched = 'S'
610 return cls('', desc)
613 class StateItem:
614 """Describes an individual item within a working copy.
616 Note that the location of this item is not specified. An external
617 mechanism, such as the State class, will provide location information
618 for each item.
621 def __init__(self, contents=None, props=None,
622 status=None, verb=None, wc_rev=None,
623 entry_rev=None, entry_status=None,
624 locked=None, copied=None, switched=None, writelocked=None,
625 treeconflict=None):
626 # provide an empty prop dict if it wasn't provided
627 if props is None:
628 props = { }
630 ### keep/make these ints one day?
631 if wc_rev is not None:
632 wc_rev = str(wc_rev)
634 # Any attribute can be None if not relevant, unless otherwise stated.
636 # A string of content (if the node is a file).
637 self.contents = contents
638 # A dictionary mapping prop name to prop value; never None.
639 self.props = props
640 # A two-character string from the first two columns of 'svn status'.
641 self.status = status
642 # The action word such as 'Adding' printed by commands like 'svn update'.
643 self.verb = verb
644 # The base revision number of the node in the WC, as a string.
645 self.wc_rev = wc_rev
646 # These will be set when we expect the wc_rev/status to differ from those
647 # found in the entries code.
648 self.entry_rev = entry_rev
649 self.entry_status = entry_status
650 # For the following attributes, the value is the status character of that
651 # field from 'svn status', except using value None instead of status ' '.
652 self.locked = locked
653 self.copied = copied
654 self.switched = switched
655 self.writelocked = writelocked
656 # Value 'C' or ' ', or None as an expected status meaning 'do not check'.
657 self.treeconflict = treeconflict
659 def copy(self):
660 "Make a deep copy of self."
661 new = StateItem()
662 vars(new).update(vars(self))
663 new.props = self.props.copy()
664 return new
666 def tweak(self, **kw):
667 for name, value in kw.items():
668 # Refine the revision args (for now) to ensure they are strings.
669 if value is not None and name == 'wc_rev':
670 value = str(value)
671 setattr(self, name, value)
673 def __eq__(self, other):
674 if not isinstance(other, StateItem):
675 return False
676 v_self = dict([(k, v) for k, v in vars(self).items()
677 if not k.startswith('_')])
678 v_other = dict([(k, v) for k, v in vars(other).items()
679 if not k.startswith('_')])
680 if self.treeconflict is None:
681 v_other = v_other.copy()
682 v_other['treeconflict'] = None
683 if other.treeconflict is None:
684 v_self = v_self.copy()
685 v_self['treeconflict'] = None
686 return v_self == v_other
688 def __ne__(self, other):
689 return not self.__eq__(other)
691 def as_node_tuple(self, path):
692 atts = { }
693 if self.status is not None:
694 atts['status'] = self.status
695 if self.verb is not None:
696 atts['verb'] = self.verb
697 if self.wc_rev is not None:
698 atts['wc_rev'] = self.wc_rev
699 if self.locked is not None:
700 atts['locked'] = self.locked
701 if self.copied is not None:
702 atts['copied'] = self.copied
703 if self.switched is not None:
704 atts['switched'] = self.switched
705 if self.writelocked is not None:
706 atts['writelocked'] = self.writelocked
707 if self.treeconflict is not None:
708 atts['treeconflict'] = self.treeconflict
710 return (os.path.normpath(path), self.contents, self.props, atts)
712 @classmethod
713 def from_entry(cls, entry):
714 status = ' '
715 if entry.schedule == 1: # svn_wc_schedule_add
716 status = 'A '
717 elif entry.schedule == 2: # svn_wc_schedule_delete
718 status = 'D '
719 elif entry.schedule == 3: # svn_wc_schedule_replace
720 status = 'R '
721 elif entry.conflict_old:
722 ### I'm assuming we only need to check one, rather than all conflict_*
723 status = 'C '
725 ### is this the sufficient? guessing here w/o investigation.
726 if entry.prejfile:
727 status = status[0] + 'C'
729 if entry.locked:
730 locked = 'L'
731 else:
732 locked = None
734 if entry.copied:
735 wc_rev = '-'
736 copied = '+'
737 else:
738 if entry.revision == -1:
739 wc_rev = '?'
740 else:
741 wc_rev = entry.revision
742 copied = None
744 ### figure out switched
745 switched = None
747 if entry.lock_token:
748 writelocked = 'K'
749 else:
750 writelocked = None
752 return cls(status=status,
753 wc_rev=wc_rev,
754 locked=locked,
755 copied=copied,
756 switched=switched,
757 writelocked=writelocked,
761 if os.sep == '/':
762 to_relpath = to_ospath = lambda path: path
763 else:
764 def to_relpath(path):
765 """Return PATH but with all native path separators changed to '/'."""
766 return path.replace(os.sep, '/')
767 def to_ospath(path):
768 """Return PATH but with each '/' changed to the native path separator."""
769 return path.replace('/', os.sep)
772 def path_to_key(path, base):
773 """Return the relative path that represents the absolute path PATH under
774 the absolute path BASE. PATH must be a path under BASE. The returned
775 path has '/' separators."""
776 if path == base:
777 return ''
779 if base.endswith(os.sep) or base.endswith('/') or base.endswith(':'):
780 # Special path format on Windows:
781 # 'C:/' Is a valid root which includes its separator ('C:/file')
782 # 'C:' is a valid root which isn't followed by a separator ('C:file')
784 # In this case, we don't need a separator between the base and the path.
785 pass
786 else:
787 # Account for a separator between the base and the relpath we're creating
788 base += os.sep
790 assert path.startswith(base), "'%s' is not a prefix of '%s'" % (base, path)
791 return to_relpath(path[len(base):])
794 def repos_split(repos_relpath):
795 """Split a repos path into its directory and basename parts."""
796 idx = repos_relpath.rfind('/')
797 if idx == -1:
798 return '', repos_relpath
799 return repos_relpath[:idx], repos_relpath[idx+1:]
802 def repos_join(base, path):
803 """Join two repos paths. This generally works for URLs too."""
804 if base == '':
805 return path
806 if path == '':
807 return base
808 return base + '/' + path
811 def svn_url_quote(url):
812 # svn defines a different set of "safe" characters than Python does, so
813 # we need to avoid escaping them. see subr/path.c:uri_char_validity[]
814 return urllib.quote(url, "!$&'()*+,-./:=@_~")
817 # ------------
819 def open_wc_db(local_path):
820 """Open the SQLite DB for the WC path LOCAL_PATH.
821 Return (DB object, WC root path, WC relpath of LOCAL_PATH)."""
822 dot_svn = svntest.main.get_admin_name()
823 root_path = local_path
824 relpath = ''
826 while True:
827 db_path = os.path.join(root_path, dot_svn, 'wc.db')
828 try:
829 db = svntest.sqlite3.connect(db_path)
830 break
831 except: pass
832 head, tail = os.path.split(root_path)
833 if head == root_path:
834 raise svntest.Failure("No DB for " + local_path)
835 root_path = head
836 relpath = os.path.join(tail, relpath).replace(os.path.sep, '/').rstrip('/')
838 return db, root_path, relpath
840 # ------------
842 def text_base_path(file_path):
843 """Return the path to the text-base file for the versioned file
844 FILE_PATH."""
845 db, root_path, relpath = open_wc_db(file_path)
847 c = db.cursor()
848 # NODES conversion is complete enough that we can use it if it exists
849 c.execute("""pragma table_info(nodes)""")
850 if c.fetchone():
851 c.execute("""select checksum from nodes
852 where local_relpath = '""" + relpath + """'
853 and op_depth = 0""")
854 else:
855 c.execute("""select checksum from base_node
856 where local_relpath = '""" + relpath + """'""")
857 row = c.fetchone()
858 if row is not None:
859 checksum = row[0]
860 if checksum is not None and checksum[0:6] == "$md5 $":
861 c.execute("""select checksum from pristine
862 where md5_checksum = '""" + checksum + """'""")
863 checksum = c.fetchone()[0]
864 if row is None or checksum is None:
865 raise svntest.Failure("No SHA1 checksum for " + relpath)
866 db.close()
868 checksum = checksum[6:]
869 # Calculate single DB location
870 dot_svn = svntest.main.get_admin_name()
871 fn = os.path.join(root_path, dot_svn, 'pristine', checksum[0:2], checksum)
873 if os.path.isfile(fn):
874 return fn
876 raise svntest.Failure("No pristine text for " + relpath)
879 # ------------
880 ### probably toss these at some point. or major rework. or something.
881 ### just bootstrapping some changes for now.
884 def item_to_node(path, item):
885 tree = svntest.tree.build_generic_tree([item.as_node_tuple(path)])
886 while tree.children:
887 assert len(tree.children) == 1
888 tree = tree.children[0]
889 return tree
891 ### yanked from tree.compare_trees()
892 def display_nodes(label, path, expected, actual):
893 'Display two nodes, expected and actual.'
894 expected = item_to_node(path, expected)
895 actual = item_to_node(path, actual)
896 print("=============================================================")
897 print("Expected '%s' and actual '%s' in %s tree are different!"
898 % (expected.name, actual.name, label))
899 print("=============================================================")
900 print("EXPECTED NODE TO BE:")
901 print("=============================================================")
902 expected.pprint()
903 print("=============================================================")
904 print("ACTUAL NODE FOUND:")
905 print("=============================================================")
906 actual.pprint()
908 ### yanked from tree.py
909 def default_singleton_handler(description, path, item):
910 node = item_to_node(path, item)
911 print("Couldn't find node '%s' in %s tree" % (node.name, description))
912 node.pprint()
913 raise svntest.tree.SVNTreeUnequal