~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/osutils.py

- constraints on revprops
- tests for this

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Bazaar-NG -- distributed version control
2
 
 
 
2
#
3
3
# Copyright (C) 2005 by Canonical Ltd
4
 
 
 
4
#
5
5
# This program is free software; you can redistribute it and/or modify
6
6
# it under the terms of the GNU General Public License as published by
7
7
# the Free Software Foundation; either version 2 of the License, or
8
8
# (at your option) any later version.
9
 
 
 
9
#
10
10
# This program is distributed in the hope that it will be useful,
11
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
13
# GNU General Public License for more details.
14
 
 
 
14
#
15
15
# You should have received a copy of the GNU General Public License
16
16
# along with this program; if not, write to the Free Software
17
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
18
 
19
 
import os, types, re, time, types
20
 
from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE
21
 
 
22
 
from errors import bailout
 
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
 
 
32
import bzrlib
 
33
from bzrlib.config import config_dir, _get_user_id
 
34
from bzrlib.errors import BzrError
 
35
from bzrlib.trace import mutter
 
36
 
23
37
 
24
38
def make_readonly(filename):
25
39
    """Make a filename read-only."""
26
 
    # TODO: probably needs to be fixed for windows
27
40
    mod = os.stat(filename).st_mode
28
41
    mod = mod & 0777555
29
42
    os.chmod(filename, mod)
35
48
    os.chmod(filename, mod)
36
49
 
37
50
 
38
 
_QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])')
 
51
_QUOTE_RE = None
 
52
 
 
53
 
39
54
def quotefn(f):
40
 
    """Return shell-quoted filename"""
41
 
    ## We could be a bit more terse by using double-quotes etc
42
 
    f = _QUOTE_RE.sub(r'\\\1', f)
43
 
    if f[0] == '~':
44
 
        f[0:1] = r'\~' 
45
 
    return f
 
55
    """Return a quoted filename filename
 
56
 
 
57
    This previously used backslash quoting, but that works poorly on
 
58
    Windows."""
 
59
    # TODO: I'm not really sure this is the best format either.x
 
60
    global _QUOTE_RE
 
61
    if _QUOTE_RE == None:
 
62
        _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/\\_~-])')
 
63
        
 
64
    if _QUOTE_RE.search(f):
 
65
        return '"' + f + '"'
 
66
    else:
 
67
        return f
46
68
 
47
69
 
48
70
def file_kind(f):
51
73
        return 'file'
52
74
    elif S_ISDIR(mode):
53
75
        return 'directory'
54
 
    else:
55
 
        bailout("can't handle file kind of %r" % fp)
56
 
 
 
76
    elif S_ISLNK(mode):
 
77
        return 'symlink'
 
78
    elif S_ISCHR(mode):
 
79
        return 'chardev'
 
80
    elif S_ISBLK(mode):
 
81
        return 'block'
 
82
    elif S_ISFIFO(mode):
 
83
        return 'fifo'
 
84
    elif S_ISSOCK(mode):
 
85
        return 'socket'
 
86
    else:
 
87
        return 'unknown'
 
88
 
 
89
 
 
90
def kind_marker(kind):
 
91
    if kind == 'file':
 
92
        return ''
 
93
    elif kind == 'directory':
 
94
        return '/'
 
95
    elif kind == 'symlink':
 
96
        return '@'
 
97
    else:
 
98
        raise BzrError('invalid file kind %r' % kind)
 
99
 
 
100
def lexists(f):
 
101
    try:
 
102
        if hasattr(os, 'lstat'):
 
103
            os.lstat(f)
 
104
        else:
 
105
            os.stat(f)
 
106
        return True
 
107
    except OSError,e:
 
108
        if e.errno == errno.ENOENT:
 
109
            return False;
 
110
        else:
 
111
            raise BzrError("lstat/stat of (%r): %r" % (f, e))
 
112
 
 
113
def normalizepath(f):
 
114
    if hasattr(os.path, 'realpath'):
 
115
        F = os.path.realpath
 
116
    else:
 
117
        F = os.path.abspath
 
118
    [p,e] = os.path.split(f)
 
119
    if e == "" or e == "." or e == "..":
 
120
        return F(f)
 
121
    else:
 
122
        return os.path.join(F(p), e)
 
123
    
 
124
 
 
125
def backup_file(fn):
 
126
    """Copy a file to a backup.
 
127
 
 
128
    Backups are named in GNU-style, with a ~ suffix.
 
129
 
 
130
    If the file is already a backup, it's not copied.
 
131
    """
 
132
    if fn[-1] == '~':
 
133
        return
 
134
    bfn = fn + '~'
 
135
 
 
136
    if has_symlinks() and os.path.islink(fn):
 
137
        target = os.readlink(fn)
 
138
        os.symlink(target, bfn)
 
139
        return
 
140
    inf = file(fn, 'rb')
 
141
    try:
 
142
        content = inf.read()
 
143
    finally:
 
144
        inf.close()
 
145
    
 
146
    outf = file(bfn, 'wb')
 
147
    try:
 
148
        outf.write(content)
 
149
    finally:
 
150
        outf.close()
 
151
 
 
152
if os.name == 'nt':
 
153
    import shutil
 
154
    rename = shutil.move
 
155
else:
 
156
    rename = os.rename
57
157
 
58
158
 
59
159
def isdir(f):
64
164
        return False
65
165
 
66
166
 
67
 
 
68
167
def isfile(f):
69
168
    """True if f is a regular file."""
70
169
    try:
72
171
    except OSError:
73
172
        return False
74
173
 
 
174
def islink(f):
 
175
    """True if f is a symlink."""
 
176
    try:
 
177
        return S_ISLNK(os.lstat(f)[ST_MODE])
 
178
    except OSError:
 
179
        return False
 
180
 
 
181
def is_inside(dir, fname):
 
182
    """True if fname is inside dir.
 
183
    
 
184
    The parameters should typically be passed to os.path.normpath first, so
 
185
    that . and .. and repeated slashes are eliminated, and the separators
 
186
    are canonical for the platform.
 
187
    
 
188
    The empty string as a dir name is taken as top-of-tree and matches 
 
189
    everything.
 
190
    
 
191
    >>> is_inside('src', os.path.join('src', 'foo.c'))
 
192
    True
 
193
    >>> is_inside('src', 'srccontrol')
 
194
    False
 
195
    >>> is_inside('src', os.path.join('src', 'a', 'a', 'a', 'foo.c'))
 
196
    True
 
197
    >>> is_inside('foo.c', 'foo.c')
 
198
    True
 
199
    >>> is_inside('foo.c', '')
 
200
    False
 
201
    >>> is_inside('', 'foo.c')
 
202
    True
 
203
    """
 
204
    # XXX: Most callers of this can actually do something smarter by 
 
205
    # looking at the inventory
 
206
    if dir == fname:
 
207
        return True
 
208
    
 
209
    if dir == '':
 
210
        return True
 
211
 
 
212
    if dir[-1] != os.sep:
 
213
        dir += os.sep
 
214
 
 
215
    return fname.startswith(dir)
 
216
 
 
217
 
 
218
def is_inside_any(dir_list, fname):
 
219
    """True if fname is inside any of given dirs."""
 
220
    for dirname in dir_list:
 
221
        if is_inside(dirname, fname):
 
222
            return True
 
223
    else:
 
224
        return False
 
225
 
75
226
 
76
227
def pumpfile(fromfile, tofile):
77
228
    """Copy contents of one file to another."""
78
229
    tofile.write(fromfile.read())
79
230
 
80
231
 
81
 
def uuid():
82
 
    """Return a new UUID"""
83
 
    
84
 
    ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on
85
 
    ## Linux, but we need something portable for other systems;
86
 
    ## preferably an implementation in Python.
87
 
    bailout('uuids not allowed!')
88
 
    return chomp(os.popen('uuidgen').readline())
89
 
 
90
 
def chomp(s):
91
 
    if s and (s[-1] == '\n'):
92
 
        return s[:-1]
93
 
    else:
94
 
        return s
95
 
 
96
 
 
97
232
def sha_file(f):
98
 
    import sha
99
 
    ## TODO: Maybe read in chunks to handle big files
100
233
    if hasattr(f, 'tell'):
101
234
        assert f.tell() == 0
102
235
    s = sha.new()
103
 
    s.update(f.read())
 
236
    BUFSIZE = 128<<10
 
237
    while True:
 
238
        b = f.read(BUFSIZE)
 
239
        if not b:
 
240
            break
 
241
        s.update(b)
 
242
    return s.hexdigest()
 
243
 
 
244
 
 
245
 
 
246
def sha_strings(strings):
 
247
    """Return the sha-1 of concatenation of strings"""
 
248
    s = sha.new()
 
249
    map(s.update, strings)
104
250
    return s.hexdigest()
105
251
 
106
252
 
107
253
def sha_string(f):
108
 
    import sha
109
254
    s = sha.new()
110
255
    s.update(f)
111
256
    return s.hexdigest()
112
257
 
113
258
 
114
 
 
115
 
def username():
116
 
    """Return email-style username.
117
 
 
118
 
    Something similar to 'Martin Pool <mbp@sourcefrog.net>'
119
 
 
120
 
    :todo: Check it's reasonably well-formed.
121
 
 
122
 
    :todo: Allow taking it from a dotfile to help people on windows
123
 
           who can't easily set variables.
124
 
 
125
 
    :todo: Cope without pwd module, which is only on unix. 
126
 
    """
127
 
    e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL')
128
 
    if e: return e
129
 
 
130
 
    import socket
131
 
    
132
 
    try:
133
 
        import pwd
134
 
        uid = os.getuid()
135
 
        w = pwd.getpwuid(uid)
136
 
        realname, junk = w.pw_gecos.split(',', 1)
137
 
        return '%s <%s@%s>' % (realname, w.pw_name, socket.getfqdn())
138
 
    except ImportError:
139
 
        pass
140
 
 
141
 
    import getpass, socket
142
 
    return '<%s@%s>' % (getpass.getuser(), socket.getfqdn())
143
 
 
144
 
 
145
 
def user_email():
146
 
    """Return just the email component of a username."""
147
 
    e = os.environ.get('BZREMAIL') or os.environ.get('EMAIL')
148
 
    if e:
149
 
        import re
150
 
        m = re.search(r'[\w+.-]+@[\w+.-]+', e)
151
 
        if not m:
152
 
            bailout('%r is not a reasonable email address' % e)
153
 
        return m.group(0)
154
 
 
155
 
 
156
 
    import getpass, socket
157
 
    return '%s@%s' % (getpass.getuser(), socket.getfqdn())
158
 
 
159
 
    
 
259
def fingerprint_file(f):
 
260
    s = sha.new()
 
261
    b = f.read()
 
262
    s.update(b)
 
263
    size = len(b)
 
264
    return {'size': size,
 
265
            'sha1': s.hexdigest()}
160
266
 
161
267
 
162
268
def compare_files(a, b):
163
269
    """Returns true if equal in contents"""
164
 
    # TODO: don't read the whole thing in one go.
165
 
    result = a.read() == b.read()
166
 
    return result
167
 
 
168
 
 
169
 
 
170
 
def format_date(t, inutc=False):
 
270
    BUFSIZE = 4096
 
271
    while True:
 
272
        ai = a.read(BUFSIZE)
 
273
        bi = b.read(BUFSIZE)
 
274
        if ai != bi:
 
275
            return False
 
276
        if ai == '':
 
277
            return True
 
278
 
 
279
 
 
280
def local_time_offset(t=None):
 
281
    """Return offset of local zone from GMT, either at present or at time t."""
 
282
    # python2.3 localtime() can't take None
 
283
    if t == None:
 
284
        t = time.time()
 
285
        
 
286
    if time.localtime(t).tm_isdst and time.daylight:
 
287
        return -time.altzone
 
288
    else:
 
289
        return -time.timezone
 
290
 
 
291
    
 
292
def format_date(t, offset=0, timezone='original'):
171
293
    ## TODO: Perhaps a global option to use either universal or local time?
172
294
    ## Or perhaps just let people set $TZ?
173
 
    import time
174
 
    
175
295
    assert isinstance(t, float)
176
296
    
177
 
    if inutc:
 
297
    if timezone == 'utc':
178
298
        tt = time.gmtime(t)
179
 
        zonename = 'UTC'
180
299
        offset = 0
181
 
    else:
 
300
    elif timezone == 'original':
 
301
        if offset == None:
 
302
            offset = 0
 
303
        tt = time.gmtime(t + offset)
 
304
    elif timezone == 'local':
182
305
        tt = time.localtime(t)
183
 
        if time.daylight:
184
 
            zonename = time.tzname[1]
185
 
            offset = - time.altzone
186
 
        else:
187
 
            zonename = time.tzname[0]
188
 
            offset = - time.timezone
189
 
            
 
306
        offset = local_time_offset(t)
 
307
    else:
 
308
        raise BzrError("unsupported timezone format %r" % timezone,
 
309
                       ['options are "utc", "original", "local"'])
 
310
 
190
311
    return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt)
191
 
            + ' ' + zonename + ' '
192
 
            + '%+03d%02d' % (offset / 3600, (offset / 60) % 60))
 
312
            + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60))
193
313
 
194
314
 
195
315
def compact_date(when):
201
321
    """Return size of given open file."""
202
322
    return os.fstat(f.fileno())[ST_SIZE]
203
323
 
204
 
 
205
 
if hasattr(os, 'urandom'): # python 2.4 and later
 
324
# Define rand_bytes based on platform.
 
325
try:
 
326
    # Python 2.4 and later have os.urandom,
 
327
    # but it doesn't work on some arches
 
328
    os.urandom(1)
206
329
    rand_bytes = os.urandom
207
 
else:
208
 
    # FIXME: No good on non-Linux
209
 
    _rand_file = file('/dev/urandom', 'rb')
210
 
    rand_bytes = _rand_file.read
211
 
 
 
330
except (NotImplementedError, AttributeError):
 
331
    # If python doesn't have os.urandom, or it doesn't work,
 
332
    # then try to first pull random data from /dev/urandom
 
333
    if os.path.exists("/dev/urandom"):
 
334
        rand_bytes = file('/dev/urandom', 'rb').read
 
335
    # Otherwise, use this hack as a last resort
 
336
    else:
 
337
        # not well seeded, but better than nothing
 
338
        def rand_bytes(n):
 
339
            import random
 
340
            s = ''
 
341
            while n:
 
342
                s += chr(random.randint(0, 255))
 
343
                n -= 1
 
344
            return s
212
345
 
213
346
## TODO: We could later have path objects that remember their list
214
347
## decomposition (might be too tricksy though.)
227
360
    >>> splitpath('a/../b')
228
361
    Traceback (most recent call last):
229
362
    ...
230
 
    BzrError: ("sorry, '..' not allowed in path", [])
 
363
    BzrError: sorry, '..' not allowed in path
231
364
    """
232
365
    assert isinstance(p, types.StringTypes)
233
 
    ps = [f for f in p.split('/') if f != '.']
 
366
 
 
367
    # split on either delimiter because people might use either on
 
368
    # Windows
 
369
    ps = re.split(r'[\\/]', p)
 
370
 
 
371
    rps = []
234
372
    for f in ps:
235
373
        if f == '..':
236
 
            bailout("sorry, %r not allowed in path" % f)
237
 
    return ps
 
374
            raise BzrError("sorry, %r not allowed in path" % f)
 
375
        elif (f == '.') or (f == ''):
 
376
            pass
 
377
        else:
 
378
            rps.append(f)
 
379
    return rps
238
380
 
239
381
def joinpath(p):
240
382
    assert isinstance(p, list)
241
383
    for f in p:
242
 
        if (f == '..') or (f is None) or (f == ''):
243
 
            bailout("sorry, %r not allowed in path" % f)
244
 
    return '/'.join(p)
 
384
        if (f == '..') or (f == None) or (f == ''):
 
385
            raise BzrError("sorry, %r not allowed in path" % f)
 
386
    return os.path.join(*p)
245
387
 
246
388
 
247
389
def appendpath(p1, p2):
248
390
    if p1 == '':
249
391
        return p2
250
392
    else:
251
 
        return p1 + '/' + p2
 
393
        return os.path.join(p1, p2)
252
394
    
253
395
 
254
 
def extern_command(cmd, ignore_errors = False):
255
 
    mutter('external command: %s' % `cmd`)
256
 
    if os.system(cmd):
257
 
        if not ignore_errors:
258
 
            bailout('command failed')
259
 
 
 
396
def split_lines(s):
 
397
    """Split s into lines, but without removing the newline characters."""
 
398
    return StringIO(s).readlines()
 
399
 
 
400
 
 
401
def hardlinks_good():
 
402
    return sys.platform not in ('win32', 'cygwin', 'darwin')
 
403
 
 
404
 
 
405
def link_or_copy(src, dest):
 
406
    """Hardlink a file, or copy it if it can't be hardlinked."""
 
407
    if not hardlinks_good():
 
408
        copyfile(src, dest)
 
409
        return
 
410
    try:
 
411
        os.link(src, dest)
 
412
    except (OSError, IOError), e:
 
413
        if e.errno != errno.EXDEV:
 
414
            raise
 
415
        copyfile(src, dest)
 
416
 
 
417
 
 
418
def has_symlinks():
 
419
    if hasattr(os, 'symlink'):
 
420
        return True
 
421
    else:
 
422
        return False
 
423
        
 
424
 
 
425
def contains_whitespace(s):
 
426
    """True if there are any whitespace characters in s."""
 
427
    for ch in string.whitespace:
 
428
        if ch in s:
 
429
            return True
 
430
    else:
 
431
        return False
 
432
 
 
433
 
 
434
def contains_linebreaks(s):
 
435
    """True if there is any vertical whitespace in s."""
 
436
    for ch in '\f\n\r':
 
437
        if ch in s:
 
438
            return True
 
439
    else:
 
440
        return False