/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 14:32:33 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: john@arbash-meinel.com-20060427143233-f06f529f5421a6eb
cmd_push should use URLs throughout.

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
    # After quoting and encoding, the path should be perfectly
 
183
    # safe as a plain ASCII string, str() just enforces this
 
184
    return str(urllib.quote(relpath))
 
185
 
 
186
 
 
187
def urlunescape(url):
 
188
    """Unescape relpath from url format.
 
189
 
 
190
    This returns a Unicode path from a URL
 
191
    """
 
192
    # jam 20060427 URLs are supposed to be ASCII only strings
 
193
    #       If they are passed in as unicode, urllib.unquote
 
194
    #       will return a UNICODE string, which actually contains
 
195
    #       utf-8 bytes. So we have to ensure that they are
 
196
    #       plain ASCII strings, or the final .decode will
 
197
    #       try to encode the UNICODE => ASCII, and then decode
 
198
    #       it into utf-8.
 
199
    unquoted = urllib.unquote(str(url))
 
200
    try:
 
201
        unicode_path = unquoted.decode('utf-8')
 
202
    except UnicodeError, e:
 
203
        raise InvalidURL(url, e)
 
204
    return unicode_path
 
205
 
 
206
 
 
207
def _posix_local_path_to_url(path):
 
208
    """Convert a local path like ./foo into a URL like file:///path/to/foo
 
209
 
 
210
    This also handles transforming escaping unicode characters, etc.
 
211
    """
 
212
    # importing directly from posixpath allows us to test this 
 
213
    # on non-posix platforms
 
214
    from posixpath import normpath
 
215
    return 'file://' + urlescape(normpath(_posix_abspath(path)))
 
216
 
 
217
 
 
218
def _posix_local_path_from_url(url):
 
219
    """Convert a url like file:///path/to/foo into /path/to/foo"""
 
220
    if not url.startswith('file:///'):
 
221
        raise InvalidURL(url, 'local urls must start with file:///')
 
222
    # We only strip off 2 slashes
 
223
    return urlunescape(url[len('file://'):])
 
224
 
 
225
 
 
226
def _win32_local_path_to_url(path):
 
227
    """Convert a local path like ./foo into a URL like file:///C|/path/to/foo
 
228
 
 
229
    This also handles transforming escaping unicode characters, etc.
 
230
    """
 
231
    # importing directly from ntpath allows us to test this 
 
232
    # on non-win32 platforms
 
233
    # TODO: jam 20060426 consider moving this import outside of the function
 
234
    from ntpath import normpath
 
235
    win32_path = normpath(abspath(path)).replace('\\', '/')
 
236
    return 'file:///' + win32_path[0] + '|' + urlescape(win32_path[2:])
 
237
 
 
238
 
 
239
def _win32_local_path_from_url(url):
 
240
    """Convert a url like file:///C|/path/to/foo into C:/path/to/foo"""
 
241
    if not url.startswith('file:///'):
 
242
        raise InvalidURL(url, 'local urls must start with file:///')
 
243
    # We strip off all 3 slashes
 
244
    win32_url = url[len('file:///'):]
 
245
    if (win32_url[0] not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
 
246
        or win32_url[1] != '|'
 
247
        or win32_url[2] != '/'):
 
248
        raise InvalidURL(url, 'Win32 file urls start with file:///X|/, where X is a valid drive letter')
 
249
    # TODO: jam 20060426, we could .upper() or .lower() the drive letter
 
250
    #       for better consistency.
 
251
    return win32_url[0] + u':' + urlunescape(win32_url[2:])
 
252
 
 
253
 
 
254
# In Python 2.4.2 and older, os.path.abspath and os.path.realpath
 
255
# choke on a Unicode string containing a relative path if
 
256
# os.getcwd() returns a non-sys.getdefaultencoding()-encoded
 
257
# string.
 
258
_fs_enc = sys.getfilesystemencoding()
 
259
def _posix_abspath(path):
 
260
    return os.path.abspath(path.encode(_fs_enc)).decode(_fs_enc)
 
261
 
 
262
 
 
263
def _posix_realpath(path):
 
264
    return os.path.realpath(path.encode(_fs_enc)).decode(_fs_enc)
 
265
 
 
266
 
 
267
def _win32_abspath(path):
 
268
    return _posix_abspath(path).replace('\\', '/')
 
269
 
 
270
 
 
271
def _win32_realpath(path):
 
272
    return _posix_realpath(path).replace('\\', '/')
 
273
 
 
274
 
 
275
def _win32_pathjoin(*args):
 
276
    return os.path.join(*args).replace('\\', '/')
 
277
 
 
278
 
 
279
def _win32_normpath(path):
 
280
    return os.path.normpath(path).replace('\\', '/')
 
281
 
 
282
 
 
283
def _win32_getcwd():
 
284
    return os.getcwdu().replace('\\', '/')
 
285
 
 
286
 
 
287
def _win32_mkdtemp(*args, **kwargs):
 
288
    return tempfile.mkdtemp(*args, **kwargs).replace('\\', '/')
 
289
 
 
290
 
 
291
def _win32_rename(old, new):
 
292
    fancy_rename(old, new, rename_func=os.rename, unlink_func=os.unlink)
 
293
 
 
294
 
 
295
# Default is to just use the python builtins
 
296
abspath = _posix_abspath
 
297
realpath = _posix_realpath
 
298
pathjoin = os.path.join
 
299
normpath = os.path.normpath
 
300
getcwd = os.getcwdu
 
301
mkdtemp = tempfile.mkdtemp
 
302
rename = os.rename
 
303
dirname = os.path.dirname
 
304
basename = os.path.basename
 
305
local_path_to_url = _posix_local_path_to_url
 
306
local_path_from_url = _posix_local_path_from_url
 
307
 
 
308
MIN_ABS_PATHLENGTH = 1
 
309
MIN_ABS_URLPATHLENGTH = len('file:///')
 
310
 
 
311
 
 
312
 
 
313
if sys.platform == 'win32':
 
314
    abspath = _win32_abspath
 
315
    realpath = _win32_realpath
 
316
    pathjoin = _win32_pathjoin
 
317
    normpath = _win32_normpath
 
318
    getcwd = _win32_getcwd
 
319
    mkdtemp = _win32_mkdtemp
 
320
    rename = _win32_rename
 
321
 
 
322
    local_path_to_url = _win32_local_path_to_url
 
323
    local_path_from_url = _win32_local_path_from_url
 
324
 
 
325
    MIN_ABS_PATHLENGTH = 3
 
326
    MIN_ABS_URLPATHLENGTH = len('file:///C|/')
 
327
 
 
328
def normalizepath(f):
 
329
    if hasattr(os.path, 'realpath'):
 
330
        F = realpath
 
331
    else:
 
332
        F = abspath
 
333
    [p,e] = os.path.split(f)
 
334
    if e == "" or e == "." or e == "..":
 
335
        return F(f)
 
336
    else:
 
337
        return pathjoin(F(p), e)
 
338
 
 
339
 
 
340
def backup_file(fn):
 
341
    """Copy a file to a backup.
 
342
 
 
343
    Backups are named in GNU-style, with a ~ suffix.
 
344
 
 
345
    If the file is already a backup, it's not copied.
 
346
    """
 
347
    if fn[-1] == '~':
 
348
        return
 
349
    bfn = fn + '~'
 
350
 
 
351
    if has_symlinks() and os.path.islink(fn):
 
352
        target = os.readlink(fn)
 
353
        os.symlink(target, bfn)
 
354
        return
 
355
    inf = file(fn, 'rb')
 
356
    try:
 
357
        content = inf.read()
 
358
    finally:
 
359
        inf.close()
 
360
    
 
361
    outf = file(bfn, 'wb')
 
362
    try:
 
363
        outf.write(content)
 
364
    finally:
 
365
        outf.close()
 
366
 
 
367
 
 
368
def isdir(f):
 
369
    """True if f is an accessible directory."""
 
370
    try:
 
371
        return S_ISDIR(os.lstat(f)[ST_MODE])
 
372
    except OSError:
 
373
        return False
 
374
 
 
375
 
 
376
def isfile(f):
 
377
    """True if f is a regular file."""
 
378
    try:
 
379
        return S_ISREG(os.lstat(f)[ST_MODE])
 
380
    except OSError:
 
381
        return False
 
382
 
 
383
def islink(f):
 
384
    """True if f is a symlink."""
 
385
    try:
 
386
        return S_ISLNK(os.lstat(f)[ST_MODE])
 
387
    except OSError:
 
388
        return False
 
389
 
 
390
def is_inside(dir, fname):
 
391
    """True if fname is inside dir.
 
392
    
 
393
    The parameters should typically be passed to osutils.normpath first, so
 
394
    that . and .. and repeated slashes are eliminated, and the separators
 
395
    are canonical for the platform.
 
396
    
 
397
    The empty string as a dir name is taken as top-of-tree and matches 
 
398
    everything.
 
399
    
 
400
    >>> is_inside('src', pathjoin('src', 'foo.c'))
 
401
    True
 
402
    >>> is_inside('src', 'srccontrol')
 
403
    False
 
404
    >>> is_inside('src', pathjoin('src', 'a', 'a', 'a', 'foo.c'))
 
405
    True
 
406
    >>> is_inside('foo.c', 'foo.c')
 
407
    True
 
408
    >>> is_inside('foo.c', '')
 
409
    False
 
410
    >>> is_inside('', 'foo.c')
 
411
    True
 
412
    """
 
413
    # XXX: Most callers of this can actually do something smarter by 
 
414
    # looking at the inventory
 
415
    if dir == fname:
 
416
        return True
 
417
    
 
418
    if dir == '':
 
419
        return True
 
420
 
 
421
    if dir[-1] != '/':
 
422
        dir += '/'
 
423
 
 
424
    return fname.startswith(dir)
 
425
 
 
426
 
 
427
def is_inside_any(dir_list, fname):
 
428
    """True if fname is inside any of given dirs."""
 
429
    for dirname in dir_list:
 
430
        if is_inside(dirname, fname):
 
431
            return True
 
432
    else:
 
433
        return False
 
434
 
 
435
 
 
436
def pumpfile(fromfile, tofile):
 
437
    """Copy contents of one file to another."""
 
438
    BUFSIZE = 32768
 
439
    while True:
 
440
        b = fromfile.read(BUFSIZE)
 
441
        if not b:
 
442
            break
 
443
        tofile.write(b)
 
444
 
 
445
 
 
446
def file_iterator(input_file, readsize=32768):
 
447
    while True:
 
448
        b = input_file.read(readsize)
 
449
        if len(b) == 0:
 
450
            break
 
451
        yield b
 
452
 
 
453
 
 
454
def sha_file(f):
 
455
    if hasattr(f, 'tell'):
 
456
        assert f.tell() == 0
 
457
    s = sha.new()
 
458
    BUFSIZE = 128<<10
 
459
    while True:
 
460
        b = f.read(BUFSIZE)
 
461
        if not b:
 
462
            break
 
463
        s.update(b)
 
464
    return s.hexdigest()
 
465
 
 
466
 
 
467
 
 
468
def sha_strings(strings):
 
469
    """Return the sha-1 of concatenation of strings"""
 
470
    s = sha.new()
 
471
    map(s.update, strings)
 
472
    return s.hexdigest()
 
473
 
 
474
 
 
475
def sha_string(f):
 
476
    s = sha.new()
 
477
    s.update(f)
 
478
    return s.hexdigest()
 
479
 
 
480
 
 
481
def fingerprint_file(f):
 
482
    s = sha.new()
 
483
    b = f.read()
 
484
    s.update(b)
 
485
    size = len(b)
 
486
    return {'size': size,
 
487
            'sha1': s.hexdigest()}
 
488
 
 
489
 
 
490
def compare_files(a, b):
 
491
    """Returns true if equal in contents"""
 
492
    BUFSIZE = 4096
 
493
    while True:
 
494
        ai = a.read(BUFSIZE)
 
495
        bi = b.read(BUFSIZE)
 
496
        if ai != bi:
 
497
            return False
 
498
        if ai == '':
 
499
            return True
 
500
 
 
501
 
 
502
def local_time_offset(t=None):
 
503
    """Return offset of local zone from GMT, either at present or at time t."""
 
504
    # python2.3 localtime() can't take None
 
505
    if t == None:
 
506
        t = time.time()
 
507
        
 
508
    if time.localtime(t).tm_isdst and time.daylight:
 
509
        return -time.altzone
 
510
    else:
 
511
        return -time.timezone
 
512
 
 
513
    
 
514
def format_date(t, offset=0, timezone='original', date_fmt=None, 
 
515
                show_offset=True):
 
516
    ## TODO: Perhaps a global option to use either universal or local time?
 
517
    ## Or perhaps just let people set $TZ?
 
518
    assert isinstance(t, float)
 
519
    
 
520
    if timezone == 'utc':
 
521
        tt = time.gmtime(t)
 
522
        offset = 0
 
523
    elif timezone == 'original':
 
524
        if offset == None:
 
525
            offset = 0
 
526
        tt = time.gmtime(t + offset)
 
527
    elif timezone == 'local':
 
528
        tt = time.localtime(t)
 
529
        offset = local_time_offset(t)
 
530
    else:
 
531
        raise BzrError("unsupported timezone format %r" % timezone,
 
532
                       ['options are "utc", "original", "local"'])
 
533
    if date_fmt is None:
 
534
        date_fmt = "%a %Y-%m-%d %H:%M:%S"
 
535
    if show_offset:
 
536
        offset_str = ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)
 
537
    else:
 
538
        offset_str = ''
 
539
    return (time.strftime(date_fmt, tt) +  offset_str)
 
540
 
 
541
 
 
542
def compact_date(when):
 
543
    return time.strftime('%Y%m%d%H%M%S', time.gmtime(when))
 
544
    
 
545
 
 
546
 
 
547
def filesize(f):
 
548
    """Return size of given open file."""
 
549
    return os.fstat(f.fileno())[ST_SIZE]
 
550
 
 
551
 
 
552
# Define rand_bytes based on platform.
 
553
try:
 
554
    # Python 2.4 and later have os.urandom,
 
555
    # but it doesn't work on some arches
 
556
    os.urandom(1)
 
557
    rand_bytes = os.urandom
 
558
except (NotImplementedError, AttributeError):
 
559
    # If python doesn't have os.urandom, or it doesn't work,
 
560
    # then try to first pull random data from /dev/urandom
 
561
    if os.path.exists("/dev/urandom"):
 
562
        rand_bytes = file('/dev/urandom', 'rb').read
 
563
    # Otherwise, use this hack as a last resort
 
564
    else:
 
565
        # not well seeded, but better than nothing
 
566
        def rand_bytes(n):
 
567
            import random
 
568
            s = ''
 
569
            while n:
 
570
                s += chr(random.randint(0, 255))
 
571
                n -= 1
 
572
            return s
 
573
 
 
574
 
 
575
ALNUM = '0123456789abcdefghijklmnopqrstuvwxyz'
 
576
def rand_chars(num):
 
577
    """Return a random string of num alphanumeric characters
 
578
    
 
579
    The result only contains lowercase chars because it may be used on 
 
580
    case-insensitive filesystems.
 
581
    """
 
582
    s = ''
 
583
    for raw_byte in rand_bytes(num):
 
584
        s += ALNUM[ord(raw_byte) % 36]
 
585
    return s
 
586
 
 
587
 
 
588
## TODO: We could later have path objects that remember their list
 
589
## decomposition (might be too tricksy though.)
 
590
 
 
591
def splitpath(p):
 
592
    """Turn string into list of parts.
 
593
 
 
594
    >>> splitpath('a')
 
595
    ['a']
 
596
    >>> splitpath('a/b')
 
597
    ['a', 'b']
 
598
    >>> splitpath('a/./b')
 
599
    ['a', 'b']
 
600
    >>> splitpath('a/.b')
 
601
    ['a', '.b']
 
602
    >>> splitpath('a/../b')
 
603
    Traceback (most recent call last):
 
604
    ...
 
605
    BzrError: sorry, '..' not allowed in path
 
606
    """
 
607
    assert isinstance(p, types.StringTypes)
 
608
 
 
609
    # split on either delimiter because people might use either on
 
610
    # Windows
 
611
    ps = re.split(r'[\\/]', p)
 
612
 
 
613
    rps = []
 
614
    for f in ps:
 
615
        if f == '..':
 
616
            raise BzrError("sorry, %r not allowed in path" % f)
 
617
        elif (f == '.') or (f == ''):
 
618
            pass
 
619
        else:
 
620
            rps.append(f)
 
621
    return rps
 
622
 
 
623
def joinpath(p):
 
624
    assert isinstance(p, list)
 
625
    for f in p:
 
626
        if (f == '..') or (f == None) or (f == ''):
 
627
            raise BzrError("sorry, %r not allowed in path" % f)
 
628
    return pathjoin(*p)
 
629
 
 
630
 
 
631
def appendpath(p1, p2):
 
632
    if p1 == '':
 
633
        return p2
 
634
    else:
 
635
        return pathjoin(p1, p2)
 
636
    
 
637
 
 
638
def split_lines(s):
 
639
    """Split s into lines, but without removing the newline characters."""
 
640
    lines = s.split('\n')
 
641
    result = [line + '\n' for line in lines[:-1]]
 
642
    if lines[-1]:
 
643
        result.append(lines[-1])
 
644
    return result
 
645
 
 
646
 
 
647
def hardlinks_good():
 
648
    return sys.platform not in ('win32', 'cygwin', 'darwin')
 
649
 
 
650
 
 
651
def link_or_copy(src, dest):
 
652
    """Hardlink a file, or copy it if it can't be hardlinked."""
 
653
    if not hardlinks_good():
 
654
        copyfile(src, dest)
 
655
        return
 
656
    try:
 
657
        os.link(src, dest)
 
658
    except (OSError, IOError), e:
 
659
        if e.errno != errno.EXDEV:
 
660
            raise
 
661
        copyfile(src, dest)
 
662
 
 
663
def delete_any(full_path):
 
664
    """Delete a file or directory."""
 
665
    try:
 
666
        os.unlink(full_path)
 
667
    except OSError, e:
 
668
    # We may be renaming a dangling inventory id
 
669
        if e.errno not in (errno.EISDIR, errno.EACCES, errno.EPERM):
 
670
            raise
 
671
        os.rmdir(full_path)
 
672
 
 
673
 
 
674
def has_symlinks():
 
675
    if hasattr(os, 'symlink'):
 
676
        return True
 
677
    else:
 
678
        return False
 
679
        
 
680
 
 
681
def contains_whitespace(s):
 
682
    """True if there are any whitespace characters in s."""
 
683
    for ch in string.whitespace:
 
684
        if ch in s:
 
685
            return True
 
686
    else:
 
687
        return False
 
688
 
 
689
 
 
690
def contains_linebreaks(s):
 
691
    """True if there is any vertical whitespace in s."""
 
692
    for ch in '\f\n\r':
 
693
        if ch in s:
 
694
            return True
 
695
    else:
 
696
        return False
 
697
 
 
698
 
 
699
def relpath(base, path):
 
700
    """Return path relative to base, or raise exception.
 
701
 
 
702
    The path may be either an absolute path or a path relative to the
 
703
    current working directory.
 
704
 
 
705
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
706
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
707
    avoids that problem.
 
708
    """
 
709
 
 
710
    assert len(base) >= MIN_ABS_PATHLENGTH, ('Length of base must be equal or'
 
711
        ' exceed the platform minimum length (which is %d)' % 
 
712
        MIN_ABS_PATHLENGTH)
 
713
 
 
714
    rp = abspath(path)
 
715
 
 
716
    s = []
 
717
    head = rp
 
718
    while len(head) >= len(base):
 
719
        if head == base:
 
720
            break
 
721
        head, tail = os.path.split(head)
 
722
        if tail:
 
723
            s.insert(0, tail)
 
724
    else:
 
725
        raise PathNotChild(rp, base)
 
726
 
 
727
    if s:
 
728
        return pathjoin(*s)
 
729
    else:
 
730
        return ''
 
731
 
 
732
 
 
733
def urlrelpath(base, path):
 
734
    """Compute just the relative sub-portion of a url
 
735
    
 
736
    This assumes that both paths are already fully specified file:// URLs.
 
737
    """
 
738
    assert len(base) >= MIN_ABS_URLPATHLENGTH, ('Length of base must be equal or'
 
739
        ' exceed the platform minimum url length (which is %d)' % 
 
740
        MIN_ABS_URLPATHLENGTH)
 
741
 
 
742
    base = local_path_from_url(base)
 
743
    path = local_path_from_url(path)
 
744
    return urlescape(relpath(base, path))
 
745
 
 
746
 
 
747
def safe_unicode(unicode_or_utf8_string):
 
748
    """Coerce unicode_or_utf8_string into unicode.
 
749
 
 
750
    If it is unicode, it is returned.
 
751
    Otherwise it is decoded from utf-8. If a decoding error
 
752
    occurs, it is wrapped as a If the decoding fails, the exception is wrapped 
 
753
    as a BzrBadParameter exception.
 
754
    """
 
755
    if isinstance(unicode_or_utf8_string, unicode):
 
756
        return unicode_or_utf8_string
 
757
    try:
 
758
        return unicode_or_utf8_string.decode('utf8')
 
759
    except UnicodeDecodeError:
 
760
        raise BzrBadParameterNotUnicode(unicode_or_utf8_string)
 
761
 
 
762
 
 
763
_platform_normalizes_filenames = False
 
764
if sys.platform == 'darwin':
 
765
    _platform_normalizes_filenames = True
 
766
 
 
767
 
 
768
def normalizes_filenames():
 
769
    """Return True if this platform normalizes unicode filenames.
 
770
 
 
771
    Mac OSX does, Windows/Linux do not.
 
772
    """
 
773
    return _platform_normalizes_filenames
 
774
 
 
775
 
 
776
if _platform_normalizes_filenames:
 
777
    def unicode_filename(path):
 
778
        """Make sure 'path' is a properly normalized filename.
 
779
 
 
780
        On platforms where the system normalizes filenames (Mac OSX),
 
781
        you can access a file by any path which will normalize
 
782
        correctly.
 
783
        Internally, bzr only supports NFC/NFKC normalization, since
 
784
        that is the standard for XML documents.
 
785
        So we return an normalized path, and indicate this has been
 
786
        properly normalized.
 
787
 
 
788
        :return: (path, is_normalized) Return a path which can
 
789
                access the file, and whether or not this path is
 
790
                normalized.
 
791
        """
 
792
        return unicodedata.normalize('NFKC', path), True
 
793
else:
 
794
    def unicode_filename(path):
 
795
        """Make sure 'path' is a properly normalized filename.
 
796
 
 
797
        On platforms where the system does not normalize filenames 
 
798
        (Windows, Linux), you have to access a file by its exact path.
 
799
        Internally, bzr only supports NFC/NFKC normalization, since
 
800
        that is the standard for XML documents.
 
801
        So we return the original path, and indicate if this is
 
802
        properly normalized.
 
803
 
 
804
        :return: (path, is_normalized) Return a path which can
 
805
                access the file, and whether or not this path is
 
806
                normalized.
 
807
        """
 
808
        return path, unicodedata.normalize('NFKC', path) == path
 
809
 
 
810
 
 
811
def terminal_width():
 
812
    """Return estimated terminal width."""
 
813
 
 
814
    # TODO: Do something smart on Windows?
 
815
 
 
816
    # TODO: Is there anything that gets a better update when the window
 
817
    # is resized while the program is running? We could use the Python termcap
 
818
    # library.
 
819
    try:
 
820
        return int(os.environ['COLUMNS'])
 
821
    except (IndexError, KeyError, ValueError):
 
822
        return 80
 
823
 
 
824
def supports_executable():
 
825
    return sys.platform != "win32"
 
826
 
 
827
 
 
828
def strip_url_trailing_slash(path):
 
829
    """Strip trailing slash, except for root paths.
 
830
    The definition of 'root path' is platform-dependent.
 
831
    """
 
832
    assert path.startswith('file:///'), \
 
833
        'strip_url_trailing_slash expects file:// urls (%s)' % path
 
834
    if len(path) != MIN_ABS_URLPATHLENGTH and path[-1] == '/':
 
835
        return path[:-1]
 
836
    else:
 
837
        return path
 
838
 
 
839
 
 
840
_validWin32PathRE = re.compile(r'^([A-Za-z]:[/\\])?[^:<>*"?\|]*$')
 
841
 
 
842
 
 
843
def check_legal_path(path):
 
844
    """Check whether the supplied path is legal.  
 
845
    This is only required on Windows, so we don't test on other platforms
 
846
    right now.
 
847
    """
 
848
    if sys.platform != "win32":
 
849
        return
 
850
    if _validWin32PathRE.match(path) is None:
 
851
        raise IllegalPath(path)