/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/local.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2012, 2016 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Transport for the local filesystem.
 
18
 
 
19
This is a fairly thin wrapper on regular file IO.
 
20
"""
 
21
 
 
22
import os
 
23
from stat import ST_MODE, S_ISDIR, S_IMODE
 
24
import sys
 
25
 
 
26
from ..lazy_import import lazy_import
 
27
lazy_import(globals(), """
 
28
import errno
 
29
import shutil
 
30
 
 
31
from breezy import (
 
32
    atomicfile,
 
33
    osutils,
 
34
    urlutils,
 
35
    )
 
36
from breezy.transport import LateReadError
 
37
""")
 
38
 
 
39
from .. import transport
 
40
 
 
41
 
 
42
_append_flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
 
43
_put_non_atomic_flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
 
44
 
 
45
 
 
46
class LocalTransport(transport.Transport):
 
47
    """This is the transport agent for local filesystem access."""
 
48
 
 
49
    def __init__(self, base):
 
50
        """Set the base path where files will be stored."""
 
51
        if not base.startswith('file://'):
 
52
            raise AssertionError("not a file:// url: %r" % base)
 
53
        if base[-1] != '/':
 
54
            base = base + '/'
 
55
 
 
56
        # Special case : windows has no "root", but does have
 
57
        # multiple lettered drives inside it. #240910
 
58
        if sys.platform == 'win32' and base == 'file:///':
 
59
            base = ''
 
60
            self._local_base = ''
 
61
            super(LocalTransport, self).__init__(base)
 
62
            return
 
63
 
 
64
        super(LocalTransport, self).__init__(base)
 
65
        self._local_base = urlutils.local_path_from_url(base)
 
66
        if self._local_base[-1] != '/':
 
67
            self._local_base = self._local_base + '/'
 
68
 
 
69
    def clone(self, offset=None):
 
70
        """Return a new LocalTransport with root at self.base + offset
 
71
        Because the local filesystem does not require a connection,
 
72
        we can just return a new object.
 
73
        """
 
74
        if offset is None:
 
75
            return LocalTransport(self.base)
 
76
        else:
 
77
            abspath = self.abspath(offset)
 
78
            if abspath == 'file://':
 
79
                # fix upwalk for UNC path
 
80
                # when clone from //HOST/path updir recursively
 
81
                # we should stop at least at //HOST part
 
82
                abspath = self.base
 
83
            return LocalTransport(abspath)
 
84
 
 
85
    def _abspath(self, relative_reference):
 
86
        """Return a path for use in os calls.
 
87
 
 
88
        Several assumptions are made:
 
89
         - relative_reference does not contain '..'
 
90
         - relative_reference is url escaped.
 
91
        """
 
92
        if relative_reference in ('.', ''):
 
93
            # _local_base normally has a trailing slash; strip it so that stat
 
94
            # on a transport pointing to a symlink reads the link not the
 
95
            # referent but be careful of / and c:\
 
96
            return osutils.split(self._local_base)[0]
 
97
        return self._local_base + urlutils.unescape(relative_reference)
 
98
 
 
99
    def abspath(self, relpath):
 
100
        """Return the full url to the given relative URL."""
 
101
        # TODO: url escape the result. RBC 20060523.
 
102
        # jam 20060426 Using normpath on the real path, because that ensures
 
103
        #       proper handling of stuff like
 
104
        path = osutils.normpath(osutils.pathjoin(
 
105
            self._local_base, urlutils.unescape(relpath)))
 
106
        # on windows, our _local_base may or may not have a drive specified
 
107
        # (ie, it may be "/" or "c:/foo").
 
108
        # If 'relpath' is '/' we *always* get back an abspath without
 
109
        # the drive letter - but if our transport already has a drive letter,
 
110
        # we want our abspaths to have a drive letter too - so handle that
 
111
        # here.
 
112
        if (sys.platform == "win32" and self._local_base[1:2] == ":"
 
113
                and path == '/'):
 
114
            path = self._local_base[:3]
 
115
 
 
116
        return urlutils.local_path_to_url(path)
 
117
 
 
118
    def local_abspath(self, relpath):
 
119
        """Transform the given relative path URL into the actual path on disk
 
120
 
 
121
        This function only exists for the LocalTransport, since it is
 
122
        the only one that has direct local access.
 
123
        This is mostly for stuff like WorkingTree which needs to know
 
124
        the local working directory.  The returned path will always contain
 
125
        forward slashes as the path separator, regardless of the platform.
 
126
 
 
127
        This function is quite expensive: it calls realpath which resolves
 
128
        symlinks.
 
129
        """
 
130
        absurl = self.abspath(relpath)
 
131
        # mutter(u'relpath %s => base: %s, absurl %s', relpath, self.base, absurl)
 
132
        return urlutils.local_path_from_url(absurl)
 
133
 
 
134
    def relpath(self, abspath):
 
135
        """Return the local path portion from a given absolute path.
 
136
        """
 
137
        if abspath is None:
 
138
            abspath = u'.'
 
139
 
 
140
        return urlutils.file_relpath(self.base, abspath)
 
141
 
 
142
    def has(self, relpath):
 
143
        return os.access(self._abspath(relpath), os.F_OK)
 
144
 
 
145
    def get(self, relpath):
 
146
        """Get the file at the given relative path.
 
147
 
 
148
        :param relpath: The relative path to the file
 
149
        """
 
150
        canonical_url = self.abspath(relpath)
 
151
        if canonical_url in transport._file_streams:
 
152
            transport._file_streams[canonical_url].flush()
 
153
        try:
 
154
            path = self._abspath(relpath)
 
155
            return osutils.open_file(path, 'rb')
 
156
        except (IOError, OSError) as e:
 
157
            if e.errno == errno.EISDIR:
 
158
                return LateReadError(relpath)
 
159
            self._translate_error(e, path)
 
160
 
 
161
    def put_file(self, relpath, f, mode=None):
 
162
        """Copy the file-like object into the location.
 
163
 
 
164
        :param relpath: Location to put the contents, relative to base.
 
165
        :param f:       File-like object.
 
166
        :param mode: The mode for the newly created file,
 
167
                     None means just use the default
 
168
        """
 
169
 
 
170
        path = relpath
 
171
        try:
 
172
            path = self._abspath(relpath)
 
173
            osutils.check_legal_path(path)
 
174
            fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
 
175
        except (IOError, OSError) as e:
 
176
            self._translate_error(e, path)
 
177
        try:
 
178
            length = self._pump(f, fp)
 
179
            fp.commit()
 
180
        finally:
 
181
            fp.close()
 
182
        return length
 
183
 
 
184
    def put_bytes(self, relpath, raw_bytes, mode=None):
 
185
        """Copy the string into the location.
 
186
 
 
187
        :param relpath: Location to put the contents, relative to base.
 
188
        :param raw_bytes:   String
 
189
        """
 
190
        if not isinstance(raw_bytes, bytes):
 
191
            raise TypeError(
 
192
                'raw_bytes must be bytes, not %s' % type(raw_bytes))
 
193
        path = relpath
 
194
        try:
 
195
            path = self._abspath(relpath)
 
196
            osutils.check_legal_path(path)
 
197
            fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
 
198
        except (IOError, OSError) as e:
 
199
            self._translate_error(e, path)
 
200
        try:
 
201
            if bytes:
 
202
                fp.write(raw_bytes)
 
203
            fp.commit()
 
204
        finally:
 
205
            fp.close()
 
206
 
 
207
    def _put_non_atomic_helper(self, relpath, writer,
 
208
                               mode=None,
 
209
                               create_parent_dir=False,
 
210
                               dir_mode=None):
 
211
        """Common functionality information for the put_*_non_atomic.
 
212
 
 
213
        This tracks all the create_parent_dir stuff.
 
214
 
 
215
        :param relpath: the path we are putting to.
 
216
        :param writer: A function that takes an os level file descriptor
 
217
            and writes whatever data it needs to write there.
 
218
        :param mode: The final file mode.
 
219
        :param create_parent_dir: Should we be creating the parent directory
 
220
            if it doesn't exist?
 
221
        """
 
222
        abspath = self._abspath(relpath)
 
223
        if mode is None:
 
224
            # os.open() will automatically use the umask
 
225
            local_mode = 0o666
 
226
        else:
 
227
            local_mode = mode
 
228
        try:
 
229
            fd = os.open(abspath, _put_non_atomic_flags, local_mode)
 
230
        except (IOError, OSError) as e:
 
231
            # We couldn't create the file, maybe we need to create
 
232
            # the parent directory, and try again
 
233
            if (not create_parent_dir
 
234
                    or e.errno not in (errno.ENOENT, errno.ENOTDIR)):
 
235
                self._translate_error(e, relpath)
 
236
            parent_dir = os.path.dirname(abspath)
 
237
            if not parent_dir:
 
238
                self._translate_error(e, relpath)
 
239
            self._mkdir(parent_dir, mode=dir_mode)
 
240
            # We created the parent directory, lets try to open the
 
241
            # file again
 
242
            try:
 
243
                fd = os.open(abspath, _put_non_atomic_flags, local_mode)
 
244
            except (IOError, OSError) as e:
 
245
                self._translate_error(e, relpath)
 
246
        try:
 
247
            st = os.fstat(fd)
 
248
            if mode is not None and mode != S_IMODE(st.st_mode):
 
249
                # Because of umask, we may still need to chmod the file.
 
250
                # But in the general case, we won't have to
 
251
                osutils.chmod_if_possible(abspath, mode)
 
252
            writer(fd)
 
253
        finally:
 
254
            os.close(fd)
 
255
 
 
256
    def put_file_non_atomic(self, relpath, f, mode=None,
 
257
                            create_parent_dir=False,
 
258
                            dir_mode=None):
 
259
        """Copy the file-like object into the target location.
 
260
 
 
261
        This function is not strictly safe to use. It is only meant to
 
262
        be used when you already know that the target does not exist.
 
263
        It is not safe, because it will open and truncate the remote
 
264
        file. So there may be a time when the file has invalid contents.
 
265
 
 
266
        :param relpath: The remote location to put the contents.
 
267
        :param f:       File-like object.
 
268
        :param mode:    Possible access permissions for new file.
 
269
                        None means do not set remote permissions.
 
270
        :param create_parent_dir: If we cannot create the target file because
 
271
                        the parent directory does not exist, go ahead and
 
272
                        create it, and then try again.
 
273
        """
 
274
        def writer(fd):
 
275
            self._pump_to_fd(f, fd)
 
276
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
277
                                    create_parent_dir=create_parent_dir,
 
278
                                    dir_mode=dir_mode)
 
279
 
 
280
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
281
                             create_parent_dir=False, dir_mode=None):
 
282
        def writer(fd):
 
283
            if bytes:
 
284
                os.write(fd, bytes)
 
285
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
286
                                    create_parent_dir=create_parent_dir,
 
287
                                    dir_mode=dir_mode)
 
288
 
 
289
    def iter_files_recursive(self):
 
290
        """Iter the relative paths of files in the transports sub-tree."""
 
291
        queue = list(self.list_dir(u'.'))
 
292
        while queue:
 
293
            relpath = queue.pop(0)
 
294
            st = self.stat(relpath)
 
295
            if S_ISDIR(st[ST_MODE]):
 
296
                for i, basename in enumerate(self.list_dir(relpath)):
 
297
                    queue.insert(i, relpath + '/' + basename)
 
298
            else:
 
299
                yield relpath
 
300
 
 
301
    def _mkdir(self, abspath, mode=None):
 
302
        """Create a real directory, filtering through mode"""
 
303
        if mode is None:
 
304
            # os.mkdir() will filter through umask
 
305
            local_mode = 0o777
 
306
        else:
 
307
            local_mode = mode
 
308
        try:
 
309
            os.mkdir(abspath, local_mode)
 
310
        except (IOError, OSError) as e:
 
311
            self._translate_error(e, abspath)
 
312
        if mode is not None:
 
313
            try:
 
314
                osutils.chmod_if_possible(abspath, mode)
 
315
            except (IOError, OSError) as e:
 
316
                self._translate_error(e, abspath)
 
317
 
 
318
    def mkdir(self, relpath, mode=None):
 
319
        """Create a directory at the given path."""
 
320
        self._mkdir(self._abspath(relpath), mode=mode)
 
321
 
 
322
    def open_write_stream(self, relpath, mode=None):
 
323
        """See Transport.open_write_stream."""
 
324
        abspath = self._abspath(relpath)
 
325
        try:
 
326
            handle = osutils.open_file(abspath, 'wb')
 
327
        except (IOError, OSError) as e:
 
328
            self._translate_error(e, abspath)
 
329
        handle.truncate()
 
330
        if mode is not None:
 
331
            self._check_mode_and_size(abspath, handle.fileno(), mode)
 
332
        transport._file_streams[self.abspath(relpath)] = handle
 
333
        return transport.FileFileStream(self, relpath, handle)
 
334
 
 
335
    def _get_append_file(self, relpath, mode=None):
 
336
        """Call os.open() for the given relpath"""
 
337
        file_abspath = self._abspath(relpath)
 
338
        if mode is None:
 
339
            # os.open() will automatically use the umask
 
340
            local_mode = 0o666
 
341
        else:
 
342
            local_mode = mode
 
343
        try:
 
344
            return file_abspath, os.open(file_abspath, _append_flags, local_mode)
 
345
        except (IOError, OSError) as e:
 
346
            self._translate_error(e, relpath)
 
347
 
 
348
    def _check_mode_and_size(self, file_abspath, fd, mode=None):
 
349
        """Check the mode of the file, and return the current size"""
 
350
        st = os.fstat(fd)
 
351
        if mode is not None and mode != S_IMODE(st.st_mode):
 
352
            # Because of umask, we may still need to chmod the file.
 
353
            # But in the general case, we won't have to
 
354
            osutils.chmod_if_possible(file_abspath, mode)
 
355
        return st.st_size
 
356
 
 
357
    def append_file(self, relpath, f, mode=None):
 
358
        """Append the text in the file-like object into the final location."""
 
359
        file_abspath, fd = self._get_append_file(relpath, mode=mode)
 
360
        try:
 
361
            result = self._check_mode_and_size(file_abspath, fd, mode=mode)
 
362
            self._pump_to_fd(f, fd)
 
363
        finally:
 
364
            os.close(fd)
 
365
        return result
 
366
 
 
367
    def append_bytes(self, relpath, bytes, mode=None):
 
368
        """Append the text in the string into the final location."""
 
369
        file_abspath, fd = self._get_append_file(relpath, mode=mode)
 
370
        try:
 
371
            result = self._check_mode_and_size(file_abspath, fd, mode=mode)
 
372
            if bytes:
 
373
                os.write(fd, bytes)
 
374
        finally:
 
375
            os.close(fd)
 
376
        return result
 
377
 
 
378
    def _pump_to_fd(self, fromfile, to_fd):
 
379
        """Copy contents of one file to another."""
 
380
        BUFSIZE = 32768
 
381
        while True:
 
382
            b = fromfile.read(BUFSIZE)
 
383
            if not b:
 
384
                break
 
385
            os.write(to_fd, b)
 
386
 
 
387
    def copy(self, rel_from, rel_to):
 
388
        """Copy the item at rel_from to the location at rel_to"""
 
389
        path_from = self._abspath(rel_from)
 
390
        path_to = self._abspath(rel_to)
 
391
        try:
 
392
            shutil.copy(path_from, path_to)
 
393
        except (IOError, OSError) as e:
 
394
            # TODO: What about path_to?
 
395
            self._translate_error(e, path_from)
 
396
 
 
397
    def rename(self, rel_from, rel_to):
 
398
        path_from = self._abspath(rel_from)
 
399
        path_to = self._abspath(rel_to)
 
400
        try:
 
401
            # *don't* call breezy.osutils.rename, because we want to
 
402
            # detect conflicting names on rename, and osutils.rename tries to
 
403
            # mask cross-platform differences there
 
404
            os.rename(path_from, path_to)
 
405
        except (IOError, OSError) as e:
 
406
            # TODO: What about path_to?
 
407
            self._translate_error(e, path_from)
 
408
 
 
409
    def move(self, rel_from, rel_to):
 
410
        """Move the item at rel_from to the location at rel_to"""
 
411
        path_from = self._abspath(rel_from)
 
412
        path_to = self._abspath(rel_to)
 
413
 
 
414
        try:
 
415
            # this version will delete the destination if necessary
 
416
            osutils.rename(path_from, path_to)
 
417
        except (IOError, OSError) as e:
 
418
            # TODO: What about path_to?
 
419
            self._translate_error(e, path_from)
 
420
 
 
421
    def delete(self, relpath):
 
422
        """Delete the item at relpath"""
 
423
        path = relpath
 
424
        try:
 
425
            path = self._abspath(relpath)
 
426
            os.remove(path)
 
427
        except (IOError, OSError) as e:
 
428
            self._translate_error(e, path)
 
429
 
 
430
    def external_url(self):
 
431
        """See breezy.transport.Transport.external_url."""
 
432
        # File URL's are externally usable.
 
433
        return self.base
 
434
 
 
435
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
436
        """Copy a set of entries from self into another Transport.
 
437
 
 
438
        :param relpaths: A list/generator of entries to be copied.
 
439
        """
 
440
        if isinstance(other, LocalTransport):
 
441
            # Both from & to are on the local filesystem
 
442
            # Unfortunately, I can't think of anything faster than just
 
443
            # copying them across, one by one :(
 
444
            total = self._get_total(relpaths)
 
445
            count = 0
 
446
            for path in relpaths:
 
447
                self._update_pb(pb, 'copy-to', count, total)
 
448
                try:
 
449
                    mypath = self._abspath(path)
 
450
                    otherpath = other._abspath(path)
 
451
                    shutil.copy(mypath, otherpath)
 
452
                    if mode is not None:
 
453
                        osutils.chmod_if_possible(otherpath, mode)
 
454
                except (IOError, OSError) as e:
 
455
                    self._translate_error(e, path)
 
456
                count += 1
 
457
            return count
 
458
        else:
 
459
            return super(LocalTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
460
 
 
461
    def listable(self):
 
462
        """See Transport.listable."""
 
463
        return True
 
464
 
 
465
    def list_dir(self, relpath):
 
466
        """Return a list of all files at the given location.
 
467
        WARNING: many transports do not support this, so trying avoid using
 
468
        it if at all possible.
 
469
        """
 
470
        path = self._abspath(relpath)
 
471
        try:
 
472
            entries = os.listdir(path)
 
473
        except (IOError, OSError) as e:
 
474
            self._translate_error(e, path)
 
475
        return [urlutils.escape(entry) for entry in entries]
 
476
 
 
477
    def stat(self, relpath):
 
478
        """Return the stat information for a file.
 
479
        """
 
480
        path = relpath
 
481
        try:
 
482
            path = self._abspath(relpath)
 
483
            return os.lstat(path)
 
484
        except (IOError, OSError) as e:
 
485
            self._translate_error(e, path)
 
486
 
 
487
    def lock_read(self, relpath):
 
488
        """Lock the given file for shared (read) access.
 
489
        :return: A lock object, which should be passed to Transport.unlock()
 
490
        """
 
491
        from breezy.lock import ReadLock
 
492
        path = relpath
 
493
        try:
 
494
            path = self._abspath(relpath)
 
495
            return ReadLock(path)
 
496
        except (IOError, OSError) as e:
 
497
            self._translate_error(e, path)
 
498
 
 
499
    def lock_write(self, relpath):
 
500
        """Lock the given file for exclusive (write) access.
 
501
        WARNING: many transports do not support this, so trying avoid using it
 
502
 
 
503
        :return: A lock object, which should be passed to Transport.unlock()
 
504
        """
 
505
        from breezy.lock import WriteLock
 
506
        return WriteLock(self._abspath(relpath))
 
507
 
 
508
    def rmdir(self, relpath):
 
509
        """See Transport.rmdir."""
 
510
        path = relpath
 
511
        try:
 
512
            path = self._abspath(relpath)
 
513
            os.rmdir(path)
 
514
        except (IOError, OSError) as e:
 
515
            self._translate_error(e, path)
 
516
 
 
517
    if osutils.host_os_dereferences_symlinks():
 
518
        def readlink(self, relpath):
 
519
            """See Transport.readlink."""
 
520
            try:
 
521
                return osutils.readlink(self._abspath(relpath))
 
522
            except (IOError, OSError) as e:
 
523
                self._translate_error(e, relpath)
 
524
 
 
525
    if osutils.hardlinks_good():
 
526
        def hardlink(self, source, link_name):
 
527
            """See Transport.link."""
 
528
            try:
 
529
                os.link(self._abspath(source), self._abspath(link_name))
 
530
            except (IOError, OSError) as e:
 
531
                self._translate_error(e, source)
 
532
 
 
533
    if getattr(os, 'symlink', None) is not None:
 
534
        def symlink(self, source, link_name):
 
535
            """See Transport.symlink."""
 
536
            abs_link_dirpath = urlutils.dirname(self.abspath(link_name))
 
537
            source_rel = urlutils.file_relpath(
 
538
                abs_link_dirpath, self.abspath(source))
 
539
 
 
540
            try:
 
541
                os.symlink(source_rel, self._abspath(link_name))
 
542
            except (IOError, OSError) as e:
 
543
                self._translate_error(e, source_rel)
 
544
 
 
545
    def _can_roundtrip_unix_modebits(self):
 
546
        if sys.platform == 'win32':
 
547
            # anyone else?
 
548
            return False
 
549
        else:
 
550
            return True
 
551
 
 
552
 
 
553
class EmulatedWin32LocalTransport(LocalTransport):
 
554
    """Special transport for testing Win32 [UNC] paths on non-windows"""
 
555
 
 
556
    def __init__(self, base):
 
557
        if base[-1] != '/':
 
558
            base = base + '/'
 
559
        super(LocalTransport, self).__init__(base)
 
560
        self._local_base = urlutils._win32_local_path_from_url(base)
 
561
 
 
562
    def abspath(self, relpath):
 
563
        path = osutils._win32_normpath(osutils.pathjoin(
 
564
            self._local_base, urlutils.unescape(relpath)))
 
565
        return urlutils._win32_local_path_to_url(path)
 
566
 
 
567
    def clone(self, offset=None):
 
568
        """Return a new LocalTransport with root at self.base + offset
 
569
        Because the local filesystem does not require a connection,
 
570
        we can just return a new object.
 
571
        """
 
572
        if offset is None:
 
573
            return EmulatedWin32LocalTransport(self.base)
 
574
        else:
 
575
            abspath = self.abspath(offset)
 
576
            if abspath == 'file://':
 
577
                # fix upwalk for UNC path
 
578
                # when clone from //HOST/path updir recursively
 
579
                # we should stop at least at //HOST part
 
580
                abspath = self.base
 
581
            return EmulatedWin32LocalTransport(abspath)
 
582
 
 
583
 
 
584
def get_test_permutations():
 
585
    """Return the permutations to be used in testing."""
 
586
    from ..tests import test_server
 
587
    return [(LocalTransport, test_server.LocalURLServer), ]