=== modified file 'bzrlib/branch.py' --- bzrlib/branch.py +++ bzrlib/branch.py @@ -18,9 +18,9 @@ import sys import os import errno +import logging from warnings import warn from cStringIO import StringIO - import bzrlib from bzrlib.inventory import InventoryEntry @@ -28,7 +28,7 @@ from bzrlib.trace import mutter, note from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, rename, splitpath, sha_file, appendpath, - file_kind) + file_kind, has_realpath) import bzrlib.errors as errors from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId, NoSuchRevision, HistoryMissing, NotBranchError, @@ -74,8 +74,11 @@ os.path.commonprefix (python2.4) has a bad bug that it works just on string prefixes, assuming that '/u' is a prefix of '/u2'. This - avoids that problem.""" - rp = os.path.abspath(path) + avoids that problem. + """ + logging.debug("_relpath: base=%r, path=%r", base, path) + abspath = os.path.abspath(path) + rp = _traverse_leading_symlinks(abspath) s = [] head = rp @@ -86,10 +89,30 @@ if tail: s.insert(0, tail) else: + logging.debug("_relpath: => NotBranchError") raise NotBranchError("path %r is not within branch %r" % (rp, base)) + logging.debug("_relpath: => %r", os.sep.join(s)) return os.sep.join(s) - + + +def _traverse_leading_symlinks(path): + """Traverse all the symlinks leading to path. + + This allows computing _relpath properly when the path does not start with + the base, yet point through symlinks to a file within the base. + + Since we need to retain the ability of referring to symlink files, the last + component of the path is not traversed. This assumes that the path has been + previously cleaned up of trailing special names (like '.' and '..'). + """ + if has_realpath(): + dirname, basename = os.path.split(path) + realdir = os.path.realpath(dirname) + return os.path.join(realdir, basename) + else: + return path + ###################################################################### # branch objects === modified file 'bzrlib/osutils.py' --- bzrlib/osutils.py +++ bzrlib/osutils.py @@ -110,18 +110,6 @@ else: raise BzrError("lstat/stat of (%r): %r" % (f, e)) -def normalizepath(f): - if hasattr(os.path, 'realpath'): - F = os.path.realpath - else: - F = os.path.abspath - [p,e] = os.path.split(f) - if e == "" or e == "." or e == "..": - return F(f) - else: - return os.path.join(F(p), e) - - def backup_file(fn): """Copy a file to a backup. @@ -416,11 +404,12 @@ def has_symlinks(): - if hasattr(os, 'symlink'): - return True - else: - return False - + return hasattr(os, 'symlink') + + +def has_realpath(): + return hasattr(os.path, 'realpath') + def contains_whitespace(s): """True if there are any whitespace characters in s.""" === modified file 'bzrlib/selftest/whitebox.py' --- bzrlib/selftest/whitebox.py +++ bzrlib/selftest/whitebox.py @@ -4,6 +4,7 @@ from bzrlib.selftest import TestCaseInTempDir, TestCase from bzrlib.branch import ScratchBranch, Branch from bzrlib.errors import NotBranchError, NotVersionedError +from bzrlib import osutils class TestBranch(TestCaseInTempDir): @@ -119,16 +120,19 @@ dtmp = tempfile.mkdtemp() # On Mac OSX, /tmp actually expands to /private/tmp dtmp = os.path.realpath(dtmp) + # use a subdir to test symlink detection + base = os.path.join(dtmp, 'base') + os.mkdir(base) def rp(p): - return _relpath(dtmp, p) + return _relpath(base, p) try: # check paths inside dtmp while standing outside it - self.assertEqual(rp(os.path.join(dtmp, 'foo')), 'foo') + self.assertEqual(rp(os.path.join(base, 'foo')), 'foo') # root = nothing - self.assertEqual(rp(dtmp), '') + self.assertEqual(rp(base), '') self.assertRaises(NotBranchError, rp, @@ -138,27 +142,50 @@ # os.path.commonprefix gets these wrong! self.assertRaises(NotBranchError, rp, - dtmp.rstrip('\\/') + '2') + base.rstrip('\\/') + '2') self.assertRaises(NotBranchError, rp, - dtmp.rstrip('\\/') + '2/foo') + base.rstrip('\\/') + '2/foo') + + + # check that relpath knows about symlinks + if osutils.has_symlinks(): + + # we can find the tree by traversing a symlink + symlink_base = os.path.join(dtmp, 'symlink-base') + os.symlink('base', symlink_base) + self.assertEqual(rp(os.path.join(symlink_base, 'foo')), 'foo') + + # when the path ends in a symlink it's not traversed so we + # retain the ability of referring to versioned symlinks. + symlink_outside = os.path.join(base, 'symlink-outside') + outside = os.path.join(dtmp, 'outside') + os.symlink(outside, symlink_outside) + self.assertEqual(rp(symlink_outside), 'symlink-outside') + self.assertRaises(NotBranchError, rp, + os.path.join(symlink_outside, 'foo')) + # now operations based on relpath of files in current # directory, or nearby - os.chdir(dtmp) + os.chdir(base) FOO_BAR_QUUX = os.path.join('foo', 'bar', 'quux') self.assertEqual(rp('foo/bar/quux'), FOO_BAR_QUUX) self.assertEqual(rp('foo'), 'foo') + self.assertEqual(rp('.'), '') + self.assertEqual(rp('./foo'), 'foo') self.assertEqual(rp(os.path.abspath('foo')), 'foo') - self.assertRaises(NotBranchError, - rp, '../foo') + self.assertRaises(NotBranchError, rp, '..') + + self.assertRaises(NotBranchError, rp, '../foo') + finally: os.chdir(savedir)