~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/osutils.py

  • Committer: Martin Pool
  • Date: 2005-08-29 05:13:23 UTC
  • Revision ID: mbp@sourcefrog.net-20050829051323-4f42d0a5c0e57d03
- robustness of trace.log_exception

Show diffs side-by-side

added added

removed removed

Lines of Context:
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, errno
 
19
import os, types, re, time, errno, sys
20
20
from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE
21
21
 
22
 
from errors import bailout, BzrError
23
 
from trace import mutter
 
22
from bzrlib.errors import BzrError
 
23
from bzrlib.trace import mutter
24
24
import bzrlib
25
25
 
26
26
def make_readonly(filename):
37
37
    os.chmod(filename, mod)
38
38
 
39
39
 
40
 
_QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])')
 
40
_QUOTE_RE = None
 
41
 
 
42
 
41
43
def quotefn(f):
42
 
    """Return shell-quoted filename"""
43
 
    ## We could be a bit more terse by using double-quotes etc
44
 
    f = _QUOTE_RE.sub(r'\\\1', f)
45
 
    if f[0] == '~':
46
 
        f[0:1] = r'\~' 
47
 
    return f
 
44
    """Return a quoted filename filename
 
45
 
 
46
    This previously used backslash quoting, but that works poorly on
 
47
    Windows."""
 
48
    # TODO: I'm not really sure this is the best format either.x
 
49
    global _QUOTE_RE
 
50
    if _QUOTE_RE == None:
 
51
        _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])')
 
52
        
 
53
    if _QUOTE_RE.search(f):
 
54
        return '"' + f + '"'
 
55
    else:
 
56
        return f
48
57
 
49
58
 
50
59
def file_kind(f):
56
65
    elif S_ISLNK(mode):
57
66
        return 'symlink'
58
67
    else:
59
 
        raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) 
 
68
        raise BzrError("can't handle file kind with mode %o of %r" % (mode, f))
 
69
 
 
70
 
 
71
def kind_marker(kind):
 
72
    if kind == 'file':
 
73
        return ''
 
74
    elif kind == 'directory':
 
75
        return '/'
 
76
    elif kind == 'symlink':
 
77
        return '@'
 
78
    else:
 
79
        raise BzrError('invalid file kind %r' % kind)
 
80
 
 
81
 
 
82
 
 
83
def backup_file(fn):
 
84
    """Copy a file to a backup.
 
85
 
 
86
    Backups are named in GNU-style, with a ~ suffix.
 
87
 
 
88
    If the file is already a backup, it's not copied.
 
89
    """
 
90
    import os
 
91
    if fn[-1] == '~':
 
92
        return
 
93
    bfn = fn + '~'
 
94
 
 
95
    inf = file(fn, 'rb')
 
96
    try:
 
97
        content = inf.read()
 
98
    finally:
 
99
        inf.close()
 
100
    
 
101
    outf = file(bfn, 'wb')
 
102
    try:
 
103
        outf.write(content)
 
104
    finally:
 
105
        outf.close()
 
106
 
 
107
def rename(path_from, path_to):
 
108
    """Basically the same as os.rename() just special for win32"""
 
109
    if sys.platform == 'win32':
 
110
        try:
 
111
            os.remove(path_to)
 
112
        except OSError, e:
 
113
            if e.errno != e.ENOENT:
 
114
                raise
 
115
    os.rename(path_from, path_to)
 
116
 
 
117
 
60
118
 
61
119
 
62
120
 
77
135
        return False
78
136
 
79
137
 
 
138
def is_inside(dir, fname):
 
139
    """True if fname is inside dir.
 
140
    
 
141
    The parameters should typically be passed to os.path.normpath first, so
 
142
    that . and .. and repeated slashes are eliminated, and the separators
 
143
    are canonical for the platform.
 
144
    
 
145
    The empty string as a dir name is taken as top-of-tree and matches 
 
146
    everything.
 
147
    
 
148
    >>> is_inside('src', 'src/foo.c')
 
149
    True
 
150
    >>> is_inside('src', 'srccontrol')
 
151
    False
 
152
    >>> is_inside('src', 'src/a/a/a/foo.c')
 
153
    True
 
154
    >>> is_inside('foo.c', 'foo.c')
 
155
    True
 
156
    >>> is_inside('foo.c', '')
 
157
    False
 
158
    >>> is_inside('', 'foo.c')
 
159
    True
 
160
    """
 
161
    # XXX: Most callers of this can actually do something smarter by 
 
162
    # looking at the inventory
 
163
    if dir == fname:
 
164
        return True
 
165
    
 
166
    if dir == '':
 
167
        return True
 
168
    
 
169
    if dir[-1] != os.sep:
 
170
        dir += os.sep
 
171
    
 
172
    return fname.startswith(dir)
 
173
 
 
174
 
 
175
def is_inside_any(dir_list, fname):
 
176
    """True if fname is inside any of given dirs."""
 
177
    for dirname in dir_list:
 
178
        if is_inside(dirname, fname):
 
179
            return True
 
180
    else:
 
181
        return False
 
182
 
 
183
 
80
184
def pumpfile(fromfile, tofile):
81
185
    """Copy contents of one file to another."""
82
186
    tofile.write(fromfile.read())
84
188
 
85
189
def uuid():
86
190
    """Return a new UUID"""
87
 
    
88
 
    ## XXX: Could alternatively read /proc/sys/kernel/random/uuid on
89
 
    ## Linux, but we need something portable for other systems;
90
 
    ## preferably an implementation in Python.
91
191
    try:
92
 
        return chomp(file('/proc/sys/kernel/random/uuid').readline())
 
192
        return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n')
93
193
    except IOError:
94
194
        return chomp(os.popen('uuidgen').readline())
95
195
 
96
196
 
97
 
def chomp(s):
98
 
    if s and (s[-1] == '\n'):
99
 
        return s[:-1]
100
 
    else:
101
 
        return s
102
 
 
103
 
 
104
197
def sha_file(f):
105
198
    import sha
106
 
    ## TODO: Maybe read in chunks to handle big files
107
199
    if hasattr(f, 'tell'):
108
200
        assert f.tell() == 0
109
201
    s = sha.new()
110
 
    s.update(f.read())
 
202
    BUFSIZE = 128<<10
 
203
    while True:
 
204
        b = f.read(BUFSIZE)
 
205
        if not b:
 
206
            break
 
207
        s.update(b)
111
208
    return s.hexdigest()
112
209
 
113
210
 
175
272
    return realname, (username + '@' + socket.gethostname())
176
273
 
177
274
 
178
 
def _get_user_id():
 
275
def _get_user_id(branch):
179
276
    """Return the full user id from a file or environment variable.
180
277
 
181
 
    TODO: Allow taking this from a file in the branch directory too
182
 
    for per-branch ids."""
 
278
    e.g. "John Hacker <jhacker@foo.org>"
 
279
 
 
280
    branch
 
281
        A branch to use for a per-branch configuration, or None.
 
282
 
 
283
    The following are searched in order:
 
284
 
 
285
    1. $BZREMAIL
 
286
    2. .bzr/email for this branch.
 
287
    3. ~/.bzr.conf/email
 
288
    4. $EMAIL
 
289
    """
183
290
    v = os.environ.get('BZREMAIL')
184
291
    if v:
185
292
        return v.decode(bzrlib.user_encoding)
 
293
 
 
294
    if branch:
 
295
        try:
 
296
            return (branch.controlfile("email", "r") 
 
297
                    .read()
 
298
                    .decode(bzrlib.user_encoding)
 
299
                    .rstrip("\r\n"))
 
300
        except IOError, e:
 
301
            if e.errno != errno.ENOENT:
 
302
                raise
 
303
        except BzrError, e:
 
304
            pass
186
305
    
187
306
    try:
188
307
        return (open(os.path.join(config_dir(), "email"))
200
319
        return None
201
320
 
202
321
 
203
 
def username():
 
322
def username(branch):
204
323
    """Return email-style username.
205
324
 
206
325
    Something similar to 'Martin Pool <mbp@sourcefrog.net>'
207
326
 
208
327
    TODO: Check it's reasonably well-formed.
209
328
    """
210
 
    v = _get_user_id()
 
329
    v = _get_user_id(branch)
211
330
    if v:
212
331
        return v
213
332
    
218
337
        return email
219
338
 
220
339
 
221
 
_EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+')
222
 
def user_email():
 
340
def user_email(branch):
223
341
    """Return just the email component of a username."""
224
 
    e = _get_user_id()
 
342
    e = _get_user_id(branch)
225
343
    if e:
226
 
        m = _EMAIL_RE.search(e)
 
344
        m = re.search(r'[\w+.-]+@[\w+.-]+', e)
227
345
        if not m:
228
 
            bailout("%r doesn't seem to contain a reasonable email address" % e)
 
346
            raise BzrError("%r doesn't seem to contain a reasonable email address" % e)
229
347
        return m.group(0)
230
348
 
231
349
    return _auto_user_id()[1]
234
352
 
235
353
def compare_files(a, b):
236
354
    """Returns true if equal in contents"""
237
 
    # TODO: don't read the whole thing in one go.
238
355
    BUFSIZE = 4096
239
356
    while True:
240
357
        ai = a.read(BUFSIZE)
274
391
        tt = time.localtime(t)
275
392
        offset = local_time_offset(t)
276
393
    else:
277
 
        bailout("unsupported timezone format %r",
278
 
                ['options are "utc", "original", "local"'])
 
394
        raise BzrError("unsupported timezone format %r" % timezone,
 
395
                       ['options are "utc", "original", "local"'])
279
396
 
280
397
    return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt)
281
398
            + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60))
293
410
 
294
411
if hasattr(os, 'urandom'): # python 2.4 and later
295
412
    rand_bytes = os.urandom
 
413
elif sys.platform == 'linux2':
 
414
    rand_bytes = file('/dev/urandom', 'rb').read
296
415
else:
297
 
    # FIXME: No good on non-Linux
298
 
    _rand_file = file('/dev/urandom', 'rb')
299
 
    rand_bytes = _rand_file.read
 
416
    # not well seeded, but better than nothing
 
417
    def rand_bytes(n):
 
418
        import random
 
419
        s = ''
 
420
        while n:
 
421
            s += chr(random.randint(0, 255))
 
422
            n -= 1
 
423
        return s
300
424
 
301
425
 
302
426
## TODO: We could later have path objects that remember their list
316
440
    >>> splitpath('a/../b')
317
441
    Traceback (most recent call last):
318
442
    ...
319
 
    BzrError: ("sorry, '..' not allowed in path", [])
 
443
    BzrError: sorry, '..' not allowed in path
320
444
    """
321
445
    assert isinstance(p, types.StringTypes)
322
446
 
327
451
    rps = []
328
452
    for f in ps:
329
453
        if f == '..':
330
 
            bailout("sorry, %r not allowed in path" % f)
 
454
            raise BzrError("sorry, %r not allowed in path" % f)
331
455
        elif (f == '.') or (f == ''):
332
456
            pass
333
457
        else:
338
462
    assert isinstance(p, list)
339
463
    for f in p:
340
464
        if (f == '..') or (f == None) or (f == ''):
341
 
            bailout("sorry, %r not allowed in path" % f)
 
465
            raise BzrError("sorry, %r not allowed in path" % f)
342
466
    return os.path.join(*p)
343
467
 
344
468
 
353
477
    mutter('external command: %s' % `cmd`)
354
478
    if os.system(cmd):
355
479
        if not ignore_errors:
356
 
            bailout('command failed')
357
 
 
 
480
            raise BzrError('command failed')
 
481
 
 
482
 
 
483
def _read_config_value(name):
 
484
    """Read a config value from the file ~/.bzr.conf/<name>
 
485
    Return None if the file does not exist"""
 
486
    try:
 
487
        f = file(os.path.join(config_dir(), name), "r")
 
488
        return f.read().decode(bzrlib.user_encoding).rstrip("\r\n")
 
489
    except IOError, e:
 
490
        if e.errno == errno.ENOENT:
 
491
            return None
 
492
        raise
 
493
 
 
494
 
 
495
def _get_editor():
 
496
    """Return a sequence of possible editor binaries for the current platform"""
 
497
    e = _read_config_value("editor")
 
498
    if e is not None:
 
499
        yield e
 
500
        
 
501
    if os.name == "windows":
 
502
        yield "notepad.exe"
 
503
    elif os.name == "posix":
 
504
        try:
 
505
            yield os.environ["EDITOR"]
 
506
        except KeyError:
 
507
            yield "/usr/bin/vi"
 
508
 
 
509
 
 
510
def _run_editor(filename):
 
511
    """Try to execute an editor to edit the commit message. Returns True on success,
 
512
    False on failure"""
 
513
    for e in _get_editor():
 
514
        x = os.spawnvp(os.P_WAIT, e, (e, filename))
 
515
        if x == 0:
 
516
            return True
 
517
        elif x == 127:
 
518
            continue
 
519
        else:
 
520
            break
 
521
    raise BzrError("Could not start any editor. Please specify $EDITOR or use ~/.bzr.conf/editor")
 
522
    return False
 
523
                          
 
524
 
 
525
def get_text_message(infotext, ignoreline = "default"):
 
526
    import tempfile
 
527
    
 
528
    if ignoreline == "default":
 
529
        ignoreline = "-- This line and the following will be ignored --"
 
530
        
 
531
    try:
 
532
        tmp_fileno, msgfilename = tempfile.mkstemp()
 
533
        msgfile = os.close(tmp_fileno)
 
534
        if infotext is not None and infotext != "":
 
535
            hasinfo = True
 
536
            msgfile = file(msgfilename, "w")
 
537
            msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext))
 
538
            msgfile.close()
 
539
        else:
 
540
            hasinfo = False
 
541
 
 
542
        if not _run_editor(msgfilename):
 
543
            return None
 
544
        
 
545
        started = False
 
546
        msg = []
 
547
        lastline, nlines = 0, 0
 
548
        for line in file(msgfilename, "r"):
 
549
            stripped_line = line.strip()
 
550
            # strip empty line before the log message starts
 
551
            if not started:
 
552
                if stripped_line != "":
 
553
                    started = True
 
554
                else:
 
555
                    continue
 
556
            # check for the ignore line only if there
 
557
            # is additional information at the end
 
558
            if hasinfo and stripped_line == ignoreline:
 
559
                break
 
560
            nlines += 1
 
561
            # keep track of the last line that had some content
 
562
            if stripped_line != "":
 
563
                lastline = nlines
 
564
            msg.append(line)
 
565
            
 
566
        if len(msg) == 0:
 
567
            return None
 
568
        # delete empty lines at the end
 
569
        del msg[lastline:]
 
570
        # add a newline at the end, if needed
 
571
        if not msg[-1].endswith("\n"):
 
572
            return "%s%s" % ("".join(msg), "\n")
 
573
        else:
 
574
            return "".join(msg)
 
575
    finally:
 
576
        # delete the msg file in any case
 
577
        try: os.unlink(msgfilename)
 
578
        except IOError: pass