/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 15:14:36 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: john@arbash-meinel.com-20060427151436-eb8c2328f7ea15f3
Repository had a bug with what exception was raised when a file was missing

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