~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/osutils.py

  • Committer: Martin Pool
  • Date: 2005-07-11 03:42:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050711034253-412281abeb9f56ad
doc

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
 
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 sys
28
 
import time
29
 
import types
 
19
import os, types, re, time, errno, sys
 
20
from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE
30
21
 
31
 
import bzrlib
32
22
from bzrlib.errors import BzrError
33
23
from bzrlib.trace import mutter
34
 
 
 
24
import bzrlib
35
25
 
36
26
def make_readonly(filename):
37
27
    """Make a filename read-only."""
 
28
    # TODO: probably needs to be fixed for windows
38
29
    mod = os.stat(filename).st_mode
39
30
    mod = mod & 0777555
40
31
    os.chmod(filename, mod)
46
37
    os.chmod(filename, mod)
47
38
 
48
39
 
49
 
_QUOTE_RE = None
50
 
 
51
 
 
 
40
_QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])')
52
41
def quotefn(f):
53
42
    """Return a quoted filename filename
54
43
 
55
44
    This previously used backslash quoting, but that works poorly on
56
45
    Windows."""
57
46
    # TODO: I'm not really sure this is the best format either.x
58
 
    global _QUOTE_RE
59
 
    if _QUOTE_RE == None:
60
 
        _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/\\_~-])')
61
 
        
62
47
    if _QUOTE_RE.search(f):
63
48
        return '"' + f + '"'
64
49
    else:
73
58
        return 'directory'
74
59
    elif S_ISLNK(mode):
75
60
        return 'symlink'
76
 
    elif S_ISCHR(mode):
77
 
        return 'chardev'
78
 
    elif S_ISBLK(mode):
79
 
        return 'block'
80
 
    elif S_ISFIFO(mode):
81
 
        return 'fifo'
82
 
    elif S_ISSOCK(mode):
83
 
        return 'socket'
84
61
    else:
85
 
        return 'unknown'
 
62
        raise BzrError("can't handle file kind with mode %o of %r" % (mode, f))
86
63
 
87
64
 
88
65
def kind_marker(kind):
95
72
    else:
96
73
        raise BzrError('invalid file kind %r' % kind)
97
74
 
98
 
def lexists(f):
99
 
    try:
100
 
        if hasattr(os, 'lstat'):
101
 
            os.lstat(f)
102
 
        else:
103
 
            os.stat(f)
104
 
        return True
105
 
    except OSError,e:
106
 
        if e.errno == errno.ENOENT:
107
 
            return False;
108
 
        else:
109
 
            raise BzrError("lstat/stat of (%r): %r" % (f, e))
110
75
 
111
 
def normalizepath(f):
112
 
    if hasattr(os.path, 'realpath'):
113
 
        F = os.path.realpath
114
 
    else:
115
 
        F = os.path.abspath
116
 
    [p,e] = os.path.split(f)
117
 
    if e == "" or e == "." or e == "..":
118
 
        return F(f)
119
 
    else:
120
 
        return os.path.join(F(p), e)
121
 
    
122
76
 
123
77
def backup_file(fn):
124
78
    """Copy a file to a backup.
127
81
 
128
82
    If the file is already a backup, it's not copied.
129
83
    """
 
84
    import os
130
85
    if fn[-1] == '~':
131
86
        return
132
87
    bfn = fn + '~'
143
98
    finally:
144
99
        outf.close()
145
100
 
146
 
if os.name == 'nt':
147
 
    import shutil
148
 
    rename = shutil.move
149
 
else:
150
 
    rename = os.rename
 
101
 
151
102
 
152
103
 
153
104
def isdir(f):
158
109
        return False
159
110
 
160
111
 
 
112
 
161
113
def isfile(f):
162
114
    """True if f is a regular file."""
163
115
    try:
165
117
    except OSError:
166
118
        return False
167
119
 
168
 
def islink(f):
169
 
    """True if f is a symlink."""
170
 
    try:
171
 
        return S_ISLNK(os.lstat(f)[ST_MODE])
172
 
    except OSError:
173
 
        return False
174
120
 
175
121
def is_inside(dir, fname):
176
122
    """True if fname is inside dir.
177
 
    
178
 
    The parameters should typically be passed to os.path.normpath first, so
179
 
    that . and .. and repeated slashes are eliminated, and the separators
180
 
    are canonical for the platform.
181
 
    
182
 
    The empty string as a dir name is taken as top-of-tree and matches 
183
 
    everything.
184
 
    
185
 
    >>> is_inside('src', os.path.join('src', 'foo.c'))
186
 
    True
187
 
    >>> is_inside('src', 'srccontrol')
188
 
    False
189
 
    >>> is_inside('src', os.path.join('src', 'a', 'a', 'a', 'foo.c'))
190
 
    True
191
 
    >>> is_inside('foo.c', 'foo.c')
192
 
    True
193
 
    >>> is_inside('foo.c', '')
194
 
    False
195
 
    >>> is_inside('', 'foo.c')
196
 
    True
197
123
    """
198
 
    # XXX: Most callers of this can actually do something smarter by 
199
 
    # looking at the inventory
200
 
    if dir == fname:
201
 
        return True
202
 
    
203
 
    if dir == '':
204
 
        return True
205
 
 
206
 
    if dir[-1] != os.sep:
207
 
        dir += os.sep
208
 
 
209
 
    return fname.startswith(dir)
 
124
    return os.path.commonprefix([dir, fname]) == dir
210
125
 
211
126
 
212
127
def is_inside_any(dir_list, fname):
213
128
    """True if fname is inside any of given dirs."""
 
129
    # quick scan for perfect match
 
130
    if fname in dir_list:
 
131
        return True
 
132
    
214
133
    for dirname in dir_list:
215
134
        if is_inside(dirname, fname):
216
135
            return True
223
142
    tofile.write(fromfile.read())
224
143
 
225
144
 
 
145
def uuid():
 
146
    """Return a new UUID"""
 
147
    try:
 
148
        return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n')
 
149
    except IOError:
 
150
        return chomp(os.popen('uuidgen').readline())
 
151
 
 
152
 
226
153
def sha_file(f):
 
154
    import sha
227
155
    if hasattr(f, 'tell'):
228
156
        assert f.tell() == 0
229
157
    s = sha.new()
236
164
    return s.hexdigest()
237
165
 
238
166
 
239
 
 
240
 
def sha_strings(strings):
241
 
    """Return the sha-1 of concatenation of strings"""
242
 
    s = sha.new()
243
 
    map(s.update, strings)
244
 
    return s.hexdigest()
245
 
 
246
 
 
247
167
def sha_string(f):
 
168
    import sha
248
169
    s = sha.new()
249
170
    s.update(f)
250
171
    return s.hexdigest()
251
172
 
252
173
 
 
174
 
253
175
def fingerprint_file(f):
 
176
    import sha
254
177
    s = sha.new()
255
178
    b = f.read()
256
179
    s.update(b)
266
189
    
267
190
    TODO: Global option --config-dir to override this.
268
191
    """
269
 
    return os.path.join(os.path.expanduser("~"), ".bzr.conf")
 
192
    return os.path.expanduser("~/.bzr.conf")
270
193
 
271
194
 
272
195
def _auto_user_id():
305
228
    return realname, (username + '@' + socket.gethostname())
306
229
 
307
230
 
308
 
def _get_user_id(branch):
 
231
def _get_user_id():
309
232
    """Return the full user id from a file or environment variable.
310
233
 
311
 
    e.g. "John Hacker <jhacker@foo.org>"
312
 
 
313
 
    branch
314
 
        A branch to use for a per-branch configuration, or None.
315
 
 
316
 
    The following are searched in order:
317
 
 
318
 
    1. $BZREMAIL
319
 
    2. .bzr/email for this branch.
320
 
    3. ~/.bzr.conf/email
321
 
    4. $EMAIL
322
 
    """
 
234
    TODO: Allow taking this from a file in the branch directory too
 
235
    for per-branch ids."""
323
236
    v = os.environ.get('BZREMAIL')
324
237
    if v:
325
238
        return v.decode(bzrlib.user_encoding)
326
 
 
327
 
    if branch:
328
 
        try:
329
 
            return (branch.controlfile("email", "r") 
330
 
                    .read()
331
 
                    .decode(bzrlib.user_encoding)
332
 
                    .rstrip("\r\n"))
333
 
        except IOError, e:
334
 
            if e.errno != errno.ENOENT:
335
 
                raise
336
 
        except BzrError, e:
337
 
            pass
338
239
    
339
240
    try:
340
241
        return (open(os.path.join(config_dir(), "email"))
352
253
        return None
353
254
 
354
255
 
355
 
def username(branch):
 
256
def username():
356
257
    """Return email-style username.
357
258
 
358
259
    Something similar to 'Martin Pool <mbp@sourcefrog.net>'
359
260
 
360
261
    TODO: Check it's reasonably well-formed.
361
262
    """
362
 
    v = _get_user_id(branch)
 
263
    v = _get_user_id()
363
264
    if v:
364
265
        return v
365
266
    
370
271
        return email
371
272
 
372
273
 
373
 
def user_email(branch):
 
274
_EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+')
 
275
def user_email():
374
276
    """Return just the email component of a username."""
375
 
    e = _get_user_id(branch)
 
277
    e = _get_user_id()
376
278
    if e:
377
 
        m = re.search(r'[\w+.-]+@[\w+.-]+', e)
 
279
        m = _EMAIL_RE.search(e)
378
280
        if not m:
379
 
            raise BzrError("%r doesn't seem to contain "
380
 
                           "a reasonable email address" % e)
 
281
            raise BzrError("%r doesn't seem to contain a reasonable email address" % e)
381
282
        return m.group(0)
382
283
 
383
284
    return _auto_user_id()[1]
 
285
    
384
286
 
385
287
 
386
288
def compare_files(a, b):
395
297
            return True
396
298
 
397
299
 
 
300
 
398
301
def local_time_offset(t=None):
399
302
    """Return offset of local zone from GMT, either at present or at time t."""
400
303
    # python2.3 localtime() can't take None
423
326
        tt = time.localtime(t)
424
327
        offset = local_time_offset(t)
425
328
    else:
426
 
        raise BzrError("unsupported timezone format %r" % timezone,
427
 
                       ['options are "utc", "original", "local"'])
 
329
        raise BzrError("unsupported timezone format %r",
 
330
                ['options are "utc", "original", "local"'])
428
331
 
429
332
    return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt)
430
333
            + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60))
439
342
    """Return size of given open file."""
440
343
    return os.fstat(f.fileno())[ST_SIZE]
441
344
 
442
 
# Define rand_bytes based on platform.
443
 
try:
444
 
    # Python 2.4 and later have os.urandom,
445
 
    # but it doesn't work on some arches
446
 
    os.urandom(1)
 
345
 
 
346
if hasattr(os, 'urandom'): # python 2.4 and later
447
347
    rand_bytes = os.urandom
448
 
except (NotImplementedError, AttributeError):
449
 
    # If python doesn't have os.urandom, or it doesn't work,
450
 
    # then try to first pull random data from /dev/urandom
451
 
    if os.path.exists("/dev/urandom"):
452
 
        rand_bytes = file('/dev/urandom', 'rb').read
453
 
    # Otherwise, use this hack as a last resort
454
 
    else:
455
 
        # not well seeded, but better than nothing
456
 
        def rand_bytes(n):
457
 
            import random
458
 
            s = ''
459
 
            while n:
460
 
                s += chr(random.randint(0, 255))
461
 
                n -= 1
462
 
            return s
 
348
elif sys.platform == 'linux2':
 
349
    rand_bytes = file('/dev/urandom', 'rb').read
 
350
else:
 
351
    # not well seeded, but better than nothing
 
352
    def rand_bytes(n):
 
353
        import random
 
354
        s = ''
 
355
        while n:
 
356
            s += chr(random.randint(0, 255))
 
357
            n -= 1
 
358
        return s
 
359
 
463
360
 
464
361
## TODO: We could later have path objects that remember their list
465
362
## decomposition (might be too tricksy though.)
511
408
        return os.path.join(p1, p2)
512
409
    
513
410
 
 
411
def extern_command(cmd, ignore_errors = False):
 
412
    mutter('external command: %s' % `cmd`)
 
413
    if os.system(cmd):
 
414
        if not ignore_errors:
 
415
            raise BzrError('command failed')
 
416
 
 
417
 
514
418
def _read_config_value(name):
515
419
    """Read a config value from the file ~/.bzr.conf/<name>
516
420
    Return None if the file does not exist"""
523
427
        raise
524
428
 
525
429
 
526
 
def split_lines(s):
527
 
    """Split s into lines, but without removing the newline characters."""
528
 
    return StringIO(s).readlines()
529
 
 
530
 
 
531
 
def hardlinks_good():
532
 
    return sys.platform not in ('win32', 'cygwin', 'darwin')
533
 
 
534
 
 
535
 
def link_or_copy(src, dest):
536
 
    """Hardlink a file, or copy it if it can't be hardlinked."""
537
 
    if not hardlinks_good():
538
 
        copyfile(src, dest)
539
 
        return
 
430
def _get_editor():
 
431
    """Return a sequence of possible editor binaries for the current platform"""
 
432
    e = _read_config_value("editor")
 
433
    if e is not None:
 
434
        yield e
 
435
        
 
436
    if os.name == "windows":
 
437
        yield "notepad.exe"
 
438
    elif os.name == "posix":
 
439
        try:
 
440
            yield os.environ["EDITOR"]
 
441
        except KeyError:
 
442
            yield "/usr/bin/vi"
 
443
 
 
444
 
 
445
def _run_editor(filename):
 
446
    """Try to execute an editor to edit the commit message. Returns True on success,
 
447
    False on failure"""
 
448
    for e in _get_editor():
 
449
        x = os.spawnvp(os.P_WAIT, e, (e, filename))
 
450
        if x == 0:
 
451
            return True
 
452
        elif x == 127:
 
453
            continue
 
454
        else:
 
455
            break
 
456
    raise BzrError("Could not start any editor. Please specify $EDITOR or use ~/.bzr.conf/editor")
 
457
    return False
 
458
                          
 
459
 
 
460
def get_text_message(infotext, ignoreline = "default"):
 
461
    import tempfile
 
462
    
 
463
    if ignoreline == "default":
 
464
        ignoreline = "-- This line and the following will be ignored --"
 
465
        
540
466
    try:
541
 
        os.link(src, dest)
542
 
    except (OSError, IOError), e:
543
 
        if e.errno != errno.EXDEV:
544
 
            raise
545
 
        copyfile(src, dest)
546
 
 
547
 
 
548
 
def has_symlinks():
549
 
    if hasattr(os, 'symlink'):
550
 
        return True
551
 
    else:
552
 
        return False
 
467
        tmp_fileno, msgfilename = tempfile.mkstemp()
 
468
        msgfile = os.close(tmp_fileno)
 
469
        if infotext is not None and infotext != "":
 
470
            hasinfo = True
 
471
            msgfile = file(msgfilename, "w")
 
472
            msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext))
 
473
            msgfile.close()
 
474
        else:
 
475
            hasinfo = False
 
476
 
 
477
        if not _run_editor(msgfilename):
 
478
            return None
 
479
        
 
480
        started = False
 
481
        msg = []
 
482
        lastline, nlines = 0, 0
 
483
        for line in file(msgfilename, "r"):
 
484
            stripped_line = line.strip()
 
485
            # strip empty line before the log message starts
 
486
            if not started:
 
487
                if stripped_line != "":
 
488
                    started = True
 
489
                else:
 
490
                    continue
 
491
            # check for the ignore line only if there
 
492
            # is additional information at the end
 
493
            if hasinfo and stripped_line == ignoreline:
 
494
                break
 
495
            nlines += 1
 
496
            # keep track of the last line that had some content
 
497
            if stripped_line != "":
 
498
                lastline = nlines
 
499
            msg.append(line)
 
500
            
 
501
        if len(msg) == 0:
 
502
            return None
 
503
        # delete empty lines at the end
 
504
        del msg[lastline:]
 
505
        # add a newline at the end, if needed
 
506
        if not msg[-1].endswith("\n"):
 
507
            return "%s%s" % ("".join(msg), "\n")
 
508
        else:
 
509
            return "".join(msg)
 
510
    finally:
 
511
        # delete the msg file in any case
 
512
        try: os.unlink(msgfilename)
 
513
        except IOError: pass