/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 bzrlib/osutils.py

  • Committer: John Arbash Meinel
  • Date: 2006-04-27 01:47:39 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: john@arbash-meinel.com-20060427014739-07fac5ec153f15a2
A few places that were comparing a working trees base to a branch's base, where Branch.base is now a URL

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Bazaar-NG -- distributed version control
 
2
#
 
3
# Copyright (C) 2005 by Canonical Ltd
 
4
#
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
 
 
19
from shutil import copyfile
 
20
from stat import (S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE,
 
21
                  S_ISCHR, S_ISBLK, S_ISFIFO, S_ISSOCK)
 
22
from cStringIO import StringIO
 
23
import errno
 
24
import os
 
25
import re
 
26
import sha
 
27
import string
 
28
import sys
 
29
import time
 
30
import types
 
31
import tempfile
 
32
import unicodedata
 
33
import urllib
 
34
 
 
35
import bzrlib
 
36
from bzrlib.errors import (BzrError,
 
37
                           BzrBadParameterNotUnicode,
 
38
                           NoSuchFile,
 
39
                           PathNotChild,
 
40
                           IllegalPath,
 
41
                           InvalidURL,
 
42
                           )
 
43
from bzrlib.trace import mutter
 
44
 
 
45
 
 
46
def make_readonly(filename):
 
47
    """Make a filename read-only."""
 
48
    mod = os.stat(filename).st_mode
 
49
    mod = mod & 0777555
 
50
    os.chmod(filename, mod)
 
51
 
 
52
 
 
53
def make_writable(filename):
 
54
    mod = os.stat(filename).st_mode
 
55
    mod = mod | 0200
 
56
    os.chmod(filename, mod)
 
57
 
 
58
 
 
59
_QUOTE_RE = None
 
60
 
 
61
 
 
62
def quotefn(f):
 
63
    """Return a quoted filename filename
 
64
 
 
65
    This previously used backslash quoting, but that works poorly on
 
66
    Windows."""
 
67
    # TODO: I'm not really sure this is the best format either.x
 
68
    global _QUOTE_RE
 
69
    if _QUOTE_RE == None:
 
70
        _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/\\_~-])')
 
71
        
 
72
    if _QUOTE_RE.search(f):
 
73
        return '"' + f + '"'
 
74
    else:
 
75
        return f
 
76
 
 
77
 
 
78
def file_kind(f):
 
79
    mode = os.lstat(f)[ST_MODE]
 
80
    if S_ISREG(mode):
 
81
        return 'file'
 
82
    elif S_ISDIR(mode):
 
83
        return 'directory'
 
84
    elif S_ISLNK(mode):
 
85
        return 'symlink'
 
86
    elif S_ISCHR(mode):
 
87
        return 'chardev'
 
88
    elif S_ISBLK(mode):
 
89
        return 'block'
 
90
    elif S_ISFIFO(mode):
 
91
        return 'fifo'
 
92
    elif S_ISSOCK(mode):
 
93
        return 'socket'
 
94
    else:
 
95
        return 'unknown'
 
96
 
 
97
 
 
98
def kind_marker(kind):
 
99
    if kind == 'file':
 
100
        return ''
 
101
    elif kind == 'directory':
 
102
        return '/'
 
103
    elif kind == 'symlink':
 
104
        return '@'
 
105
    else:
 
106
        raise BzrError('invalid file kind %r' % kind)
 
107
 
 
108
def lexists(f):
 
109
    if hasattr(os.path, 'lexists'):
 
110
        return os.path.lexists(f)
 
111
    try:
 
112
        if hasattr(os, 'lstat'):
 
113
            os.lstat(f)
 
114
        else:
 
115
            os.stat(f)
 
116
        return True
 
117
    except OSError,e:
 
118
        if e.errno == errno.ENOENT:
 
119
            return False;
 
120
        else:
 
121
            raise BzrError("lstat/stat of (%r): %r" % (f, e))
 
122
 
 
123
def fancy_rename(old, new, rename_func, unlink_func):
 
124
    """A fancy rename, when you don't have atomic rename.
 
125
    
 
126
    :param old: The old path, to rename from
 
127
    :param new: The new path, to rename to
 
128
    :param rename_func: The potentially non-atomic rename function
 
129
    :param unlink_func: A way to delete the target file if the full rename succeeds
 
130
    """
 
131
 
 
132
    # sftp rename doesn't allow overwriting, so play tricks:
 
133
    import random
 
134
    base = os.path.basename(new)
 
135
    dirname = os.path.dirname(new)
 
136
    tmp_name = u'tmp.%s.%.9f.%d.%s' % (base, time.time(), os.getpid(), rand_chars(10))
 
137
    tmp_name = pathjoin(dirname, tmp_name)
 
138
 
 
139
    # Rename the file out of the way, but keep track if it didn't exist
 
140
    # We don't want to grab just any exception
 
141
    # something like EACCES should prevent us from continuing
 
142
    # The downside is that the rename_func has to throw an exception
 
143
    # with an errno = ENOENT, or NoSuchFile
 
144
    file_existed = False
 
145
    try:
 
146
        rename_func(new, tmp_name)
 
147
    except (NoSuchFile,), e:
 
148
        pass
 
149
    except IOError, e:
 
150
        # RBC 20060103 abstraction leakage: the paramiko SFTP clients rename
 
151
        # function raises an IOError with errno == None when a rename fails.
 
152
        # This then gets caught here.
 
153
        if e.errno not in (None, errno.ENOENT, errno.ENOTDIR):
 
154
            raise
 
155
    except Exception, e:
 
156
        if (not hasattr(e, 'errno') 
 
157
            or e.errno not in (errno.ENOENT, errno.ENOTDIR)):
 
158
            raise
 
159
    else:
 
160
        file_existed = True
 
161
 
 
162
    success = False
 
163
    try:
 
164
        # This may throw an exception, in which case success will
 
165
        # not be set.
 
166
        rename_func(old, new)
 
167
        success = True
 
168
    finally:
 
169
        if file_existed:
 
170
            # If the file used to exist, rename it back into place
 
171
            # otherwise just delete it from the tmp location
 
172
            if success:
 
173
                unlink_func(tmp_name)
 
174
            else:
 
175
                rename_func(tmp_name, new)
 
176
 
 
177
 
 
178
def urlescape(relpath):
 
179
    """Escape relpath to be a valid url."""
 
180
    if isinstance(relpath, unicode):
 
181
        relpath = relpath.encode('utf-8')
 
182
    return urllib.quote(relpath)
 
183
 
 
184
 
 
185
def urlunescape(url):
 
186
    """Unescape relpath from url format.
 
187
 
 
188
    This returns a Unicode path from a URL
 
189
    """
 
190
    unquoted = urllib.unquote(url)
 
191
    try:
 
192
        unicode_path = unquoted.decode('utf-8')
 
193
    except UnicodeError, e:
 
194
        raise InvalidURL(url, e)
 
195
    return unicode_path
 
196
 
 
197
 
 
198
def posix_local_path_to_url(path):
 
199
    """Convert a local path like ./foo into a URL like file:///path/to/foo
 
200
 
 
201
    This also handles transforming escaping unicode characters, etc.
 
202
    """
 
203
    # importing directly from posixpath allows us to test this 
 
204
    # on non-posix platforms
 
205
    from posixpath import abspath, normpath
 
206
    return 'file://' + urlescape(normpath(abspath(path)))
 
207
 
 
208
 
 
209
def posix_local_path_from_url(url):
 
210
    """Convert a url like file:///path/to/foo into /path/to/foo"""
 
211
    if not url.startswith('file:///'):
 
212
        raise InvalidURL(url, 'local urls must start with file:///')
 
213
    # We only strip off 2 slashes
 
214
    return urlunescape(url[len('file://'):])
 
215
 
 
216
 
 
217
def win32_local_path_to_url(path):
 
218
    """Convert a local path like ./foo into a URL like file:///C|/path/to/foo
 
219
 
 
220
    This also handles transforming escaping unicode characters, etc.
 
221
    """
 
222
    # importing directly from ntpath allows us to test this 
 
223
    # on non-win32 platforms
 
224
    # TODO: jam 20060426 consider moving this import outside of the function
 
225
    from ntpath import normpath, abspath
 
226
    win32_path = normpath(abspath(path)).replace('\\', '/')
 
227
    return 'file:///' + win32_path[0] + '|' + urlescape(win32_path[2:])
 
228
 
 
229
 
 
230
def win32_local_path_from_url(url):
 
231
    """Convert a url like file:///C|/path/to/foo into C:/path/to/foo"""
 
232
    if not url.startswith('file:///'):
 
233
        raise InvalidURL(url, 'local urls must start with file:///')
 
234
    # We strip off all 3 slashes
 
235
    win32_url = url[len('file:///'):]
 
236
    if (win32_url[0] not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
 
237
        or win32_url[1] != '|'
 
238
        or win32_url[2] != '/'):
 
239
        raise InvalidURL(url, 'Win32 file urls start with file:///X|/, where X is a valid drive letter')
 
240
    # TODO: jam 20060426, we could .upper() or .lower() the drive letter
 
241
    #       for better consistency.
 
242
    return win32_url[0] + u':' + urlunescape(win32_url[2:])
 
243
 
 
244
 
 
245
# Default is to just use the python builtins
 
246
abspath = os.path.abspath
 
247
realpath = os.path.realpath
 
248
pathjoin = os.path.join
 
249
normpath = os.path.normpath
 
250
getcwd = os.getcwdu
 
251
mkdtemp = tempfile.mkdtemp
 
252
rename = os.rename
 
253
dirname = os.path.dirname
 
254
basename = os.path.basename
 
255
local_path_to_url = posix_local_path_to_url
 
256
local_path_from_url = posix_local_path_from_url
 
257
 
 
258
MIN_ABS_PATHLENGTH = 1
 
259
MIN_ABS_URLPATHLENGTH = len('file:///')
 
260
 
 
261
 
 
262
if os.name == "posix":
 
263
    # In Python 2.4.2 and older, os.path.abspath and os.path.realpath
 
264
    # choke on a Unicode string containing a relative path if
 
265
    # os.getcwd() returns a non-sys.getdefaultencoding()-encoded
 
266
    # string.
 
267
    _fs_enc = sys.getfilesystemencoding()
 
268
    def abspath(path):
 
269
        return os.path.abspath(path.encode(_fs_enc)).decode(_fs_enc)
 
270
 
 
271
    def realpath(path):
 
272
        return os.path.realpath(path.encode(_fs_enc)).decode(_fs_enc)
 
273
 
 
274
if sys.platform == 'win32':
 
275
    # We need to use the Unicode-aware os.path.abspath and
 
276
    # os.path.realpath on Windows systems.
 
277
    def abspath(path):
 
278
        return os.path.abspath(path).replace('\\', '/')
 
279
 
 
280
    def realpath(path):
 
281
        return os.path.realpath(path).replace('\\', '/')
 
282
 
 
283
    def pathjoin(*args):
 
284
        return os.path.join(*args).replace('\\', '/')
 
285
 
 
286
    def normpath(path):
 
287
        return os.path.normpath(path).replace('\\', '/')
 
288
 
 
289
    def getcwd():
 
290
        return os.getcwdu().replace('\\', '/')
 
291
 
 
292
    def mkdtemp(*args, **kwargs):
 
293
        return tempfile.mkdtemp(*args, **kwargs).replace('\\', '/')
 
294
 
 
295
    def rename(old, new):
 
296
        fancy_rename(old, new, rename_func=os.rename, unlink_func=os.unlink)
 
297
 
 
298
    local_path_to_url = win32_local_path_to_url
 
299
    local_path_from_url = win32_local_path_from_url
 
300
 
 
301
    MIN_ABS_PATHLENGTH = 3
 
302
    MIN_ABS_URLPATHLENGTH = len('file:///C|/')
 
303
 
 
304
def normalizepath(f):
 
305
    if hasattr(os.path, 'realpath'):
 
306
        F = realpath
 
307
    else:
 
308
        F = abspath
 
309
    [p,e] = os.path.split(f)
 
310
    if e == "" or e == "." or e == "..":
 
311
        return F(f)
 
312
    else:
 
313
        return pathjoin(F(p), e)
 
314
 
 
315
 
 
316
def backup_file(fn):
 
317
    """Copy a file to a backup.
 
318
 
 
319
    Backups are named in GNU-style, with a ~ suffix.
 
320
 
 
321
    If the file is already a backup, it's not copied.
 
322
    """
 
323
    if fn[-1] == '~':
 
324
        return
 
325
    bfn = fn + '~'
 
326
 
 
327
    if has_symlinks() and os.path.islink(fn):
 
328
        target = os.readlink(fn)
 
329
        os.symlink(target, bfn)
 
330
        return
 
331
    inf = file(fn, 'rb')
 
332
    try:
 
333
        content = inf.read()
 
334
    finally:
 
335
        inf.close()
 
336
    
 
337
    outf = file(bfn, 'wb')
 
338
    try:
 
339
        outf.write(content)
 
340
    finally:
 
341
        outf.close()
 
342
 
 
343
 
 
344
def isdir(f):
 
345
    """True if f is an accessible directory."""
 
346
    try:
 
347
        return S_ISDIR(os.lstat(f)[ST_MODE])
 
348
    except OSError:
 
349
        return False
 
350
 
 
351
 
 
352
def isfile(f):
 
353
    """True if f is a regular file."""
 
354
    try:
 
355
        return S_ISREG(os.lstat(f)[ST_MODE])
 
356
    except OSError:
 
357
        return False
 
358
 
 
359
def islink(f):
 
360
    """True if f is a symlink."""
 
361
    try:
 
362
        return S_ISLNK(os.lstat(f)[ST_MODE])
 
363
    except OSError:
 
364
        return False
 
365
 
 
366
def is_inside(dir, fname):
 
367
    """True if fname is inside dir.
 
368
    
 
369
    The parameters should typically be passed to osutils.normpath first, so
 
370
    that . and .. and repeated slashes are eliminated, and the separators
 
371
    are canonical for the platform.
 
372
    
 
373
    The empty string as a dir name is taken as top-of-tree and matches 
 
374
    everything.
 
375
    
 
376
    >>> is_inside('src', pathjoin('src', 'foo.c'))
 
377
    True
 
378
    >>> is_inside('src', 'srccontrol')
 
379
    False
 
380
    >>> is_inside('src', pathjoin('src', 'a', 'a', 'a', 'foo.c'))
 
381
    True
 
382
    >>> is_inside('foo.c', 'foo.c')
 
383
    True
 
384
    >>> is_inside('foo.c', '')
 
385
    False
 
386
    >>> is_inside('', 'foo.c')
 
387
    True
 
388
    """
 
389
    # XXX: Most callers of this can actually do something smarter by 
 
390
    # looking at the inventory
 
391
    if dir == fname:
 
392
        return True
 
393
    
 
394
    if dir == '':
 
395
        return True
 
396
 
 
397
    if dir[-1] != '/':
 
398
        dir += '/'
 
399
 
 
400
    return fname.startswith(dir)
 
401
 
 
402
 
 
403
def is_inside_any(dir_list, fname):
 
404
    """True if fname is inside any of given dirs."""
 
405
    for dirname in dir_list:
 
406
        if is_inside(dirname, fname):
 
407
            return True
 
408
    else:
 
409
        return False
 
410
 
 
411
 
 
412
def pumpfile(fromfile, tofile):
 
413
    """Copy contents of one file to another."""
 
414
    BUFSIZE = 32768
 
415
    while True:
 
416
        b = fromfile.read(BUFSIZE)
 
417
        if not b:
 
418
            break
 
419
        tofile.write(b)
 
420
 
 
421
 
 
422
def file_iterator(input_file, readsize=32768):
 
423
    while True:
 
424
        b = input_file.read(readsize)
 
425
        if len(b) == 0:
 
426
            break
 
427
        yield b
 
428
 
 
429
 
 
430
def sha_file(f):
 
431
    if hasattr(f, 'tell'):
 
432
        assert f.tell() == 0
 
433
    s = sha.new()
 
434
    BUFSIZE = 128<<10
 
435
    while True:
 
436
        b = f.read(BUFSIZE)
 
437
        if not b:
 
438
            break
 
439
        s.update(b)
 
440
    return s.hexdigest()
 
441
 
 
442
 
 
443
 
 
444
def sha_strings(strings):
 
445
    """Return the sha-1 of concatenation of strings"""
 
446
    s = sha.new()
 
447
    map(s.update, strings)
 
448
    return s.hexdigest()
 
449
 
 
450
 
 
451
def sha_string(f):
 
452
    s = sha.new()
 
453
    s.update(f)
 
454
    return s.hexdigest()
 
455
 
 
456
 
 
457
def fingerprint_file(f):
 
458
    s = sha.new()
 
459
    b = f.read()
 
460
    s.update(b)
 
461
    size = len(b)
 
462
    return {'size': size,
 
463
            'sha1': s.hexdigest()}
 
464
 
 
465
 
 
466
def compare_files(a, b):
 
467
    """Returns true if equal in contents"""
 
468
    BUFSIZE = 4096
 
469
    while True:
 
470
        ai = a.read(BUFSIZE)
 
471
        bi = b.read(BUFSIZE)
 
472
        if ai != bi:
 
473
            return False
 
474
        if ai == '':
 
475
            return True
 
476
 
 
477
 
 
478
def local_time_offset(t=None):
 
479
    """Return offset of local zone from GMT, either at present or at time t."""
 
480
    # python2.3 localtime() can't take None
 
481
    if t == None:
 
482
        t = time.time()
 
483
        
 
484
    if time.localtime(t).tm_isdst and time.daylight:
 
485
        return -time.altzone
 
486
    else:
 
487
        return -time.timezone
 
488
 
 
489
    
 
490
def format_date(t, offset=0, timezone='original', date_fmt=None, 
 
491
                show_offset=True):
 
492
    ## TODO: Perhaps a global option to use either universal or local time?
 
493
    ## Or perhaps just let people set $TZ?
 
494
    assert isinstance(t, float)
 
495
    
 
496
    if timezone == 'utc':
 
497
        tt = time.gmtime(t)
 
498
        offset = 0
 
499
    elif timezone == 'original':
 
500
        if offset == None:
 
501
            offset = 0
 
502
        tt = time.gmtime(t + offset)
 
503
    elif timezone == 'local':
 
504
        tt = time.localtime(t)
 
505
        offset = local_time_offset(t)
 
506
    else:
 
507
        raise BzrError("unsupported timezone format %r" % timezone,
 
508
                       ['options are "utc", "original", "local"'])
 
509
    if date_fmt is None:
 
510
        date_fmt = "%a %Y-%m-%d %H:%M:%S"
 
511
    if show_offset:
 
512
        offset_str = ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)
 
513
    else:
 
514
        offset_str = ''
 
515
    return (time.strftime(date_fmt, tt) +  offset_str)
 
516
 
 
517
 
 
518
def compact_date(when):
 
519
    return time.strftime('%Y%m%d%H%M%S', time.gmtime(when))
 
520
    
 
521
 
 
522
 
 
523
def filesize(f):
 
524
    """Return size of given open file."""
 
525
    return os.fstat(f.fileno())[ST_SIZE]
 
526
 
 
527
 
 
528
# Define rand_bytes based on platform.
 
529
try:
 
530
    # Python 2.4 and later have os.urandom,
 
531
    # but it doesn't work on some arches
 
532
    os.urandom(1)
 
533
    rand_bytes = os.urandom
 
534
except (NotImplementedError, AttributeError):
 
535
    # If python doesn't have os.urandom, or it doesn't work,
 
536
    # then try to first pull random data from /dev/urandom
 
537
    if os.path.exists("/dev/urandom"):
 
538
        rand_bytes = file('/dev/urandom', 'rb').read
 
539
    # Otherwise, use this hack as a last resort
 
540
    else:
 
541
        # not well seeded, but better than nothing
 
542
        def rand_bytes(n):
 
543
            import random
 
544
            s = ''
 
545
            while n:
 
546
                s += chr(random.randint(0, 255))
 
547
                n -= 1
 
548
            return s
 
549
 
 
550
 
 
551
ALNUM = '0123456789abcdefghijklmnopqrstuvwxyz'
 
552
def rand_chars(num):
 
553
    """Return a random string of num alphanumeric characters
 
554
    
 
555
    The result only contains lowercase chars because it may be used on 
 
556
    case-insensitive filesystems.
 
557
    """
 
558
    s = ''
 
559
    for raw_byte in rand_bytes(num):
 
560
        s += ALNUM[ord(raw_byte) % 36]
 
561
    return s
 
562
 
 
563
 
 
564
## TODO: We could later have path objects that remember their list
 
565
## decomposition (might be too tricksy though.)
 
566
 
 
567
def splitpath(p):
 
568
    """Turn string into list of parts.
 
569
 
 
570
    >>> splitpath('a')
 
571
    ['a']
 
572
    >>> splitpath('a/b')
 
573
    ['a', 'b']
 
574
    >>> splitpath('a/./b')
 
575
    ['a', 'b']
 
576
    >>> splitpath('a/.b')
 
577
    ['a', '.b']
 
578
    >>> splitpath('a/../b')
 
579
    Traceback (most recent call last):
 
580
    ...
 
581
    BzrError: sorry, '..' not allowed in path
 
582
    """
 
583
    assert isinstance(p, types.StringTypes)
 
584
 
 
585
    # split on either delimiter because people might use either on
 
586
    # Windows
 
587
    ps = re.split(r'[\\/]', p)
 
588
 
 
589
    rps = []
 
590
    for f in ps:
 
591
        if f == '..':
 
592
            raise BzrError("sorry, %r not allowed in path" % f)
 
593
        elif (f == '.') or (f == ''):
 
594
            pass
 
595
        else:
 
596
            rps.append(f)
 
597
    return rps
 
598
 
 
599
def joinpath(p):
 
600
    assert isinstance(p, list)
 
601
    for f in p:
 
602
        if (f == '..') or (f == None) or (f == ''):
 
603
            raise BzrError("sorry, %r not allowed in path" % f)
 
604
    return pathjoin(*p)
 
605
 
 
606
 
 
607
def appendpath(p1, p2):
 
608
    if p1 == '':
 
609
        return p2
 
610
    else:
 
611
        return pathjoin(p1, p2)
 
612
    
 
613
 
 
614
def split_lines(s):
 
615
    """Split s into lines, but without removing the newline characters."""
 
616
    lines = s.split('\n')
 
617
    result = [line + '\n' for line in lines[:-1]]
 
618
    if lines[-1]:
 
619
        result.append(lines[-1])
 
620
    return result
 
621
 
 
622
 
 
623
def hardlinks_good():
 
624
    return sys.platform not in ('win32', 'cygwin', 'darwin')
 
625
 
 
626
 
 
627
def link_or_copy(src, dest):
 
628
    """Hardlink a file, or copy it if it can't be hardlinked."""
 
629
    if not hardlinks_good():
 
630
        copyfile(src, dest)
 
631
        return
 
632
    try:
 
633
        os.link(src, dest)
 
634
    except (OSError, IOError), e:
 
635
        if e.errno != errno.EXDEV:
 
636
            raise
 
637
        copyfile(src, dest)
 
638
 
 
639
def delete_any(full_path):
 
640
    """Delete a file or directory."""
 
641
    try:
 
642
        os.unlink(full_path)
 
643
    except OSError, e:
 
644
    # We may be renaming a dangling inventory id
 
645
        if e.errno not in (errno.EISDIR, errno.EACCES, errno.EPERM):
 
646
            raise
 
647
        os.rmdir(full_path)
 
648
 
 
649
 
 
650
def has_symlinks():
 
651
    if hasattr(os, 'symlink'):
 
652
        return True
 
653
    else:
 
654
        return False
 
655
        
 
656
 
 
657
def contains_whitespace(s):
 
658
    """True if there are any whitespace characters in s."""
 
659
    for ch in string.whitespace:
 
660
        if ch in s:
 
661
            return True
 
662
    else:
 
663
        return False
 
664
 
 
665
 
 
666
def contains_linebreaks(s):
 
667
    """True if there is any vertical whitespace in s."""
 
668
    for ch in '\f\n\r':
 
669
        if ch in s:
 
670
            return True
 
671
    else:
 
672
        return False
 
673
 
 
674
 
 
675
def relpath(base, path):
 
676
    """Return path relative to base, or raise exception.
 
677
 
 
678
    The path may be either an absolute path or a path relative to the
 
679
    current working directory.
 
680
 
 
681
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
682
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
683
    avoids that problem.
 
684
    """
 
685
 
 
686
    assert len(base) >= MIN_ABS_PATHLENGTH, ('Length of base must be equal or'
 
687
        ' exceed the platform minimum length (which is %d)' % 
 
688
        MIN_ABS_PATHLENGTH)
 
689
 
 
690
    rp = abspath(path)
 
691
 
 
692
    s = []
 
693
    head = rp
 
694
    while len(head) >= len(base):
 
695
        if head == base:
 
696
            break
 
697
        head, tail = os.path.split(head)
 
698
        if tail:
 
699
            s.insert(0, tail)
 
700
    else:
 
701
        raise PathNotChild(rp, base)
 
702
 
 
703
    if s:
 
704
        return pathjoin(*s)
 
705
    else:
 
706
        return ''
 
707
 
 
708
 
 
709
def urlrelpath(base, path):
 
710
    """Compute just the relative sub-portion of a url
 
711
    
 
712
    This assumes that both paths are already fully specified URLs.
 
713
    """
 
714
    assert len(base) >= MIN_ABS_URLPATHLENGTH, ('Length of base must be equal or'
 
715
        ' exceed the platform minimum url length (which is %d)' % 
 
716
        MIN_ABS_URLPATHLENGTH)
 
717
 
 
718
    base = local_path_from_url(base)
 
719
    path = local_path_from_url(path)
 
720
    return relpath(base, path)
 
721
 
 
722
 
 
723
def safe_unicode(unicode_or_utf8_string):
 
724
    """Coerce unicode_or_utf8_string into unicode.
 
725
 
 
726
    If it is unicode, it is returned.
 
727
    Otherwise it is decoded from utf-8. If a decoding error
 
728
    occurs, it is wrapped as a If the decoding fails, the exception is wrapped 
 
729
    as a BzrBadParameter exception.
 
730
    """
 
731
    if isinstance(unicode_or_utf8_string, unicode):
 
732
        return unicode_or_utf8_string
 
733
    try:
 
734
        return unicode_or_utf8_string.decode('utf8')
 
735
    except UnicodeDecodeError:
 
736
        raise BzrBadParameterNotUnicode(unicode_or_utf8_string)
 
737
 
 
738
 
 
739
_platform_normalizes_filenames = False
 
740
if sys.platform == 'darwin':
 
741
    _platform_normalizes_filenames = True
 
742
 
 
743
 
 
744
def normalizes_filenames():
 
745
    """Return True if this platform normalizes unicode filenames.
 
746
 
 
747
    Mac OSX does, Windows/Linux do not.
 
748
    """
 
749
    return _platform_normalizes_filenames
 
750
 
 
751
 
 
752
if _platform_normalizes_filenames:
 
753
    def unicode_filename(path):
 
754
        """Make sure 'path' is a properly normalized filename.
 
755
 
 
756
        On platforms where the system normalizes filenames (Mac OSX),
 
757
        you can access a file by any path which will normalize
 
758
        correctly.
 
759
        Internally, bzr only supports NFC/NFKC normalization, since
 
760
        that is the standard for XML documents.
 
761
        So we return an normalized path, and indicate this has been
 
762
        properly normalized.
 
763
 
 
764
        :return: (path, is_normalized) Return a path which can
 
765
                access the file, and whether or not this path is
 
766
                normalized.
 
767
        """
 
768
        return unicodedata.normalize('NFKC', path), True
 
769
else:
 
770
    def unicode_filename(path):
 
771
        """Make sure 'path' is a properly normalized filename.
 
772
 
 
773
        On platforms where the system does not normalize filenames 
 
774
        (Windows, Linux), you have to access a file by its exact path.
 
775
        Internally, bzr only supports NFC/NFKC normalization, since
 
776
        that is the standard for XML documents.
 
777
        So we return the original path, and indicate if this is
 
778
        properly normalized.
 
779
 
 
780
        :return: (path, is_normalized) Return a path which can
 
781
                access the file, and whether or not this path is
 
782
                normalized.
 
783
        """
 
784
        return path, unicodedata.normalize('NFKC', path) == path
 
785
 
 
786
 
 
787
def terminal_width():
 
788
    """Return estimated terminal width."""
 
789
 
 
790
    # TODO: Do something smart on Windows?
 
791
 
 
792
    # TODO: Is there anything that gets a better update when the window
 
793
    # is resized while the program is running? We could use the Python termcap
 
794
    # library.
 
795
    try:
 
796
        return int(os.environ['COLUMNS'])
 
797
    except (IndexError, KeyError, ValueError):
 
798
        return 80
 
799
 
 
800
def supports_executable():
 
801
    return sys.platform != "win32"
 
802
 
 
803
 
 
804
def strip_url_trailing_slash(path):
 
805
    """Strip trailing slash, except for root paths.
 
806
    The definition of 'root path' is platform-dependent.
 
807
    """
 
808
    assert path.startswith('file:///'), \
 
809
        'strip_url_trailing_slash expects file:// urls (%s)' % path
 
810
    if len(path) != MIN_ABS_URLPATHLENGTH and path[-1] == '/':
 
811
        return path[:-1]
 
812
    else:
 
813
        return path
 
814
 
 
815
 
 
816
_validWin32PathRE = re.compile(r'^([A-Za-z]:[/\\])?[^:<>*"?\|]*$')
 
817
 
 
818
 
 
819
def check_legal_path(path):
 
820
    """Check whether the supplied path is legal.  
 
821
    This is only required on Windows, so we don't test on other platforms
 
822
    right now.
 
823
    """
 
824
    if sys.platform != "win32":
 
825
        return
 
826
    if _validWin32PathRE.match(path) is None:
 
827
        raise IllegalPath(path)