/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: Martin Pool
  • Date: 2005-08-18 04:25:42 UTC
  • Revision ID: mbp@sourcefrog.net-20050818042542-6af9da978f695195
- check for email address in BRANCH_ROOT/.bzr/email, so you can 
  easily use different per-project personas

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
import os, types, re, time, errno, sys
 
20
from stat import S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE
 
21
 
 
22
from bzrlib.errors import BzrError
 
23
from bzrlib.trace import mutter
 
24
import bzrlib
 
25
 
 
26
def make_readonly(filename):
 
27
    """Make a filename read-only."""
 
28
    # TODO: probably needs to be fixed for windows
 
29
    mod = os.stat(filename).st_mode
 
30
    mod = mod & 0777555
 
31
    os.chmod(filename, mod)
 
32
 
 
33
 
 
34
def make_writable(filename):
 
35
    mod = os.stat(filename).st_mode
 
36
    mod = mod | 0200
 
37
    os.chmod(filename, mod)
 
38
 
 
39
 
 
40
_QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/_~-])')
 
41
 
 
42
_SLASH_RE = re.compile(r'[\\/]+')
 
43
 
 
44
def quotefn(f):
 
45
    """Return a quoted filename filename
 
46
 
 
47
    This previously used backslash quoting, but that works poorly on
 
48
    Windows."""
 
49
    # TODO: I'm not really sure this is the best format either.x
 
50
    if _QUOTE_RE.search(f):
 
51
        return '"' + f + '"'
 
52
    else:
 
53
        return f
 
54
 
 
55
 
 
56
def file_kind(f):
 
57
    mode = os.lstat(f)[ST_MODE]
 
58
    if S_ISREG(mode):
 
59
        return 'file'
 
60
    elif S_ISDIR(mode):
 
61
        return 'directory'
 
62
    elif S_ISLNK(mode):
 
63
        return 'symlink'
 
64
    else:
 
65
        raise BzrError("can't handle file kind with mode %o of %r" % (mode, f))
 
66
 
 
67
 
 
68
def kind_marker(kind):
 
69
    if kind == 'file':
 
70
        return ''
 
71
    elif kind == 'directory':
 
72
        return '/'
 
73
    elif kind == 'symlink':
 
74
        return '@'
 
75
    else:
 
76
        raise BzrError('invalid file kind %r' % kind)
 
77
 
 
78
 
 
79
 
 
80
def backup_file(fn):
 
81
    """Copy a file to a backup.
 
82
 
 
83
    Backups are named in GNU-style, with a ~ suffix.
 
84
 
 
85
    If the file is already a backup, it's not copied.
 
86
    """
 
87
    import os
 
88
    if fn[-1] == '~':
 
89
        return
 
90
    bfn = fn + '~'
 
91
 
 
92
    inf = file(fn, 'rb')
 
93
    try:
 
94
        content = inf.read()
 
95
    finally:
 
96
        inf.close()
 
97
    
 
98
    outf = file(bfn, 'wb')
 
99
    try:
 
100
        outf.write(content)
 
101
    finally:
 
102
        outf.close()
 
103
 
 
104
def rename(path_from, path_to):
 
105
    """Basically the same as os.rename() just special for win32"""
 
106
    if sys.platform == 'win32':
 
107
        try:
 
108
            os.remove(path_to)
 
109
        except OSError, e:
 
110
            if e.errno != e.ENOENT:
 
111
                raise
 
112
    os.rename(path_from, path_to)
 
113
 
 
114
 
 
115
 
 
116
 
 
117
 
 
118
def isdir(f):
 
119
    """True if f is an accessible directory."""
 
120
    try:
 
121
        return S_ISDIR(os.lstat(f)[ST_MODE])
 
122
    except OSError:
 
123
        return False
 
124
 
 
125
 
 
126
 
 
127
def isfile(f):
 
128
    """True if f is a regular file."""
 
129
    try:
 
130
        return S_ISREG(os.lstat(f)[ST_MODE])
 
131
    except OSError:
 
132
        return False
 
133
 
 
134
 
 
135
def is_inside(dir, fname):
 
136
    """True if fname is inside dir.
 
137
    
 
138
    The parameters should typically be passed to os.path.normpath first, so
 
139
    that . and .. and repeated slashes are eliminated, and the separators
 
140
    are canonical for the platform.
 
141
    
 
142
    The empty string as a dir name is taken as top-of-tree and matches 
 
143
    everything.
 
144
    
 
145
    >>> is_inside('src', 'src/foo.c')
 
146
    True
 
147
    >>> is_inside('src', 'srccontrol')
 
148
    False
 
149
    >>> is_inside('src', 'src/a/a/a/foo.c')
 
150
    True
 
151
    >>> is_inside('foo.c', 'foo.c')
 
152
    True
 
153
    >>> is_inside('foo.c', '')
 
154
    False
 
155
    >>> is_inside('', 'foo.c')
 
156
    True
 
157
    """
 
158
    # XXX: Most callers of this can actually do something smarter by 
 
159
    # looking at the inventory
 
160
    if dir == fname:
 
161
        return True
 
162
    
 
163
    if dir == '':
 
164
        return True
 
165
    
 
166
    if dir[-1] != os.sep:
 
167
        dir += os.sep
 
168
    
 
169
    return fname.startswith(dir)
 
170
 
 
171
 
 
172
def is_inside_any(dir_list, fname):
 
173
    """True if fname is inside any of given dirs."""
 
174
    for dirname in dir_list:
 
175
        if is_inside(dirname, fname):
 
176
            return True
 
177
    else:
 
178
        return False
 
179
 
 
180
 
 
181
def pumpfile(fromfile, tofile):
 
182
    """Copy contents of one file to another."""
 
183
    tofile.write(fromfile.read())
 
184
 
 
185
 
 
186
def uuid():
 
187
    """Return a new UUID"""
 
188
    try:
 
189
        return file('/proc/sys/kernel/random/uuid').readline().rstrip('\n')
 
190
    except IOError:
 
191
        return chomp(os.popen('uuidgen').readline())
 
192
 
 
193
 
 
194
def sha_file(f):
 
195
    import sha
 
196
    if hasattr(f, 'tell'):
 
197
        assert f.tell() == 0
 
198
    s = sha.new()
 
199
    BUFSIZE = 128<<10
 
200
    while True:
 
201
        b = f.read(BUFSIZE)
 
202
        if not b:
 
203
            break
 
204
        s.update(b)
 
205
    return s.hexdigest()
 
206
 
 
207
 
 
208
def sha_string(f):
 
209
    import sha
 
210
    s = sha.new()
 
211
    s.update(f)
 
212
    return s.hexdigest()
 
213
 
 
214
 
 
215
 
 
216
def fingerprint_file(f):
 
217
    import sha
 
218
    s = sha.new()
 
219
    b = f.read()
 
220
    s.update(b)
 
221
    size = len(b)
 
222
    return {'size': size,
 
223
            'sha1': s.hexdigest()}
 
224
 
 
225
 
 
226
def config_dir():
 
227
    """Return per-user configuration directory.
 
228
 
 
229
    By default this is ~/.bzr.conf/
 
230
    
 
231
    TODO: Global option --config-dir to override this.
 
232
    """
 
233
    return os.path.expanduser("~/.bzr.conf")
 
234
 
 
235
 
 
236
def _auto_user_id():
 
237
    """Calculate automatic user identification.
 
238
 
 
239
    Returns (realname, email).
 
240
 
 
241
    Only used when none is set in the environment or the id file.
 
242
 
 
243
    This previously used the FQDN as the default domain, but that can
 
244
    be very slow on machines where DNS is broken.  So now we simply
 
245
    use the hostname.
 
246
    """
 
247
    import socket
 
248
 
 
249
    # XXX: Any good way to get real user name on win32?
 
250
 
 
251
    try:
 
252
        import pwd
 
253
        uid = os.getuid()
 
254
        w = pwd.getpwuid(uid)
 
255
        gecos = w.pw_gecos.decode(bzrlib.user_encoding)
 
256
        username = w.pw_name.decode(bzrlib.user_encoding)
 
257
        comma = gecos.find(',')
 
258
        if comma == -1:
 
259
            realname = gecos
 
260
        else:
 
261
            realname = gecos[:comma]
 
262
        if not realname:
 
263
            realname = username
 
264
 
 
265
    except ImportError:
 
266
        import getpass
 
267
        realname = username = getpass.getuser().decode(bzrlib.user_encoding)
 
268
 
 
269
    return realname, (username + '@' + socket.gethostname())
 
270
 
 
271
 
 
272
def _get_user_id(branch):
 
273
    """Return the full user id from a file or environment variable.
 
274
 
 
275
    e.g. "John Hacker <jhacker@foo.org>"
 
276
 
 
277
    branch
 
278
        A branch to use for a per-branch configuration, or None.
 
279
 
 
280
    The following are searched in order:
 
281
 
 
282
    1. $BZREMAIL
 
283
    2. .bzr/email for this branch.
 
284
    3. ~/.bzr.conf/email
 
285
    4. $EMAIL
 
286
    """
 
287
    v = os.environ.get('BZREMAIL')
 
288
    if v:
 
289
        return v.decode(bzrlib.user_encoding)
 
290
 
 
291
    if branch:
 
292
        try:
 
293
            return (branch.controlfile("email", "r") 
 
294
                    .read()
 
295
                    .decode(bzrlib.user_encoding)
 
296
                    .rstrip("\r\n"))
 
297
        except IOError, e:
 
298
            if e.errno != errno.ENOENT:
 
299
                raise
 
300
        except BzrError, e:
 
301
            pass
 
302
    
 
303
    try:
 
304
        return (open(os.path.join(config_dir(), "email"))
 
305
                .read()
 
306
                .decode(bzrlib.user_encoding)
 
307
                .rstrip("\r\n"))
 
308
    except IOError, e:
 
309
        if e.errno != errno.ENOENT:
 
310
            raise e
 
311
 
 
312
    v = os.environ.get('EMAIL')
 
313
    if v:
 
314
        return v.decode(bzrlib.user_encoding)
 
315
    else:    
 
316
        return None
 
317
 
 
318
 
 
319
def username(branch):
 
320
    """Return email-style username.
 
321
 
 
322
    Something similar to 'Martin Pool <mbp@sourcefrog.net>'
 
323
 
 
324
    TODO: Check it's reasonably well-formed.
 
325
    """
 
326
    v = _get_user_id(branch)
 
327
    if v:
 
328
        return v
 
329
    
 
330
    name, email = _auto_user_id()
 
331
    if name:
 
332
        return '%s <%s>' % (name, email)
 
333
    else:
 
334
        return email
 
335
 
 
336
 
 
337
_EMAIL_RE = re.compile(r'[\w+.-]+@[\w+.-]+')
 
338
def user_email(branch):
 
339
    """Return just the email component of a username."""
 
340
    e = _get_user_id(branch)
 
341
    if e:
 
342
        m = _EMAIL_RE.search(e)
 
343
        if not m:
 
344
            raise BzrError("%r doesn't seem to contain a reasonable email address" % e)
 
345
        return m.group(0)
 
346
 
 
347
    return _auto_user_id()[1]
 
348
    
 
349
 
 
350
 
 
351
def compare_files(a, b):
 
352
    """Returns true if equal in contents"""
 
353
    BUFSIZE = 4096
 
354
    while True:
 
355
        ai = a.read(BUFSIZE)
 
356
        bi = b.read(BUFSIZE)
 
357
        if ai != bi:
 
358
            return False
 
359
        if ai == '':
 
360
            return True
 
361
 
 
362
 
 
363
 
 
364
def local_time_offset(t=None):
 
365
    """Return offset of local zone from GMT, either at present or at time t."""
 
366
    # python2.3 localtime() can't take None
 
367
    if t == None:
 
368
        t = time.time()
 
369
        
 
370
    if time.localtime(t).tm_isdst and time.daylight:
 
371
        return -time.altzone
 
372
    else:
 
373
        return -time.timezone
 
374
 
 
375
    
 
376
def format_date(t, offset=0, timezone='original'):
 
377
    ## TODO: Perhaps a global option to use either universal or local time?
 
378
    ## Or perhaps just let people set $TZ?
 
379
    assert isinstance(t, float)
 
380
    
 
381
    if timezone == 'utc':
 
382
        tt = time.gmtime(t)
 
383
        offset = 0
 
384
    elif timezone == 'original':
 
385
        if offset == None:
 
386
            offset = 0
 
387
        tt = time.gmtime(t + offset)
 
388
    elif timezone == 'local':
 
389
        tt = time.localtime(t)
 
390
        offset = local_time_offset(t)
 
391
    else:
 
392
        raise BzrError("unsupported timezone format %r" % timezone,
 
393
                       ['options are "utc", "original", "local"'])
 
394
 
 
395
    return (time.strftime("%a %Y-%m-%d %H:%M:%S", tt)
 
396
            + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60))
 
397
 
 
398
 
 
399
def compact_date(when):
 
400
    return time.strftime('%Y%m%d%H%M%S', time.gmtime(when))
 
401
    
 
402
 
 
403
 
 
404
def filesize(f):
 
405
    """Return size of given open file."""
 
406
    return os.fstat(f.fileno())[ST_SIZE]
 
407
 
 
408
 
 
409
if hasattr(os, 'urandom'): # python 2.4 and later
 
410
    rand_bytes = os.urandom
 
411
elif sys.platform == 'linux2':
 
412
    rand_bytes = file('/dev/urandom', 'rb').read
 
413
else:
 
414
    # not well seeded, but better than nothing
 
415
    def rand_bytes(n):
 
416
        import random
 
417
        s = ''
 
418
        while n:
 
419
            s += chr(random.randint(0, 255))
 
420
            n -= 1
 
421
        return s
 
422
 
 
423
 
 
424
## TODO: We could later have path objects that remember their list
 
425
## decomposition (might be too tricksy though.)
 
426
 
 
427
def splitpath(p):
 
428
    """Turn string into list of parts.
 
429
 
 
430
    >>> splitpath('a')
 
431
    ['a']
 
432
    >>> splitpath('a/b')
 
433
    ['a', 'b']
 
434
    >>> splitpath('a/./b')
 
435
    ['a', 'b']
 
436
    >>> splitpath('a/.b')
 
437
    ['a', '.b']
 
438
    >>> splitpath('a/../b')
 
439
    Traceback (most recent call last):
 
440
    ...
 
441
    BzrError: sorry, '..' not allowed in path
 
442
    """
 
443
    assert isinstance(p, types.StringTypes)
 
444
 
 
445
    # split on either delimiter because people might use either on
 
446
    # Windows
 
447
    ps = re.split(r'[\\/]', p)
 
448
 
 
449
    rps = []
 
450
    for f in ps:
 
451
        if f == '..':
 
452
            raise BzrError("sorry, %r not allowed in path" % f)
 
453
        elif (f == '.') or (f == ''):
 
454
            pass
 
455
        else:
 
456
            rps.append(f)
 
457
    return rps
 
458
 
 
459
def joinpath(p):
 
460
    assert isinstance(p, list)
 
461
    for f in p:
 
462
        if (f == '..') or (f == None) or (f == ''):
 
463
            raise BzrError("sorry, %r not allowed in path" % f)
 
464
    return os.path.join(*p)
 
465
 
 
466
 
 
467
def appendpath(p1, p2):
 
468
    if p1 == '':
 
469
        return p2
 
470
    else:
 
471
        return os.path.join(p1, p2)
 
472
    
 
473
 
 
474
def extern_command(cmd, ignore_errors = False):
 
475
    mutter('external command: %s' % `cmd`)
 
476
    if os.system(cmd):
 
477
        if not ignore_errors:
 
478
            raise BzrError('command failed')
 
479
 
 
480
 
 
481
def _read_config_value(name):
 
482
    """Read a config value from the file ~/.bzr.conf/<name>
 
483
    Return None if the file does not exist"""
 
484
    try:
 
485
        f = file(os.path.join(config_dir(), name), "r")
 
486
        return f.read().decode(bzrlib.user_encoding).rstrip("\r\n")
 
487
    except IOError, e:
 
488
        if e.errno == errno.ENOENT:
 
489
            return None
 
490
        raise
 
491
 
 
492
 
 
493
def _get_editor():
 
494
    """Return a sequence of possible editor binaries for the current platform"""
 
495
    e = _read_config_value("editor")
 
496
    if e is not None:
 
497
        yield e
 
498
        
 
499
    if os.name == "windows":
 
500
        yield "notepad.exe"
 
501
    elif os.name == "posix":
 
502
        try:
 
503
            yield os.environ["EDITOR"]
 
504
        except KeyError:
 
505
            yield "/usr/bin/vi"
 
506
 
 
507
 
 
508
def _run_editor(filename):
 
509
    """Try to execute an editor to edit the commit message. Returns True on success,
 
510
    False on failure"""
 
511
    for e in _get_editor():
 
512
        x = os.spawnvp(os.P_WAIT, e, (e, filename))
 
513
        if x == 0:
 
514
            return True
 
515
        elif x == 127:
 
516
            continue
 
517
        else:
 
518
            break
 
519
    raise BzrError("Could not start any editor. Please specify $EDITOR or use ~/.bzr.conf/editor")
 
520
    return False
 
521
                          
 
522
 
 
523
def get_text_message(infotext, ignoreline = "default"):
 
524
    import tempfile
 
525
    
 
526
    if ignoreline == "default":
 
527
        ignoreline = "-- This line and the following will be ignored --"
 
528
        
 
529
    try:
 
530
        tmp_fileno, msgfilename = tempfile.mkstemp()
 
531
        msgfile = os.close(tmp_fileno)
 
532
        if infotext is not None and infotext != "":
 
533
            hasinfo = True
 
534
            msgfile = file(msgfilename, "w")
 
535
            msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext))
 
536
            msgfile.close()
 
537
        else:
 
538
            hasinfo = False
 
539
 
 
540
        if not _run_editor(msgfilename):
 
541
            return None
 
542
        
 
543
        started = False
 
544
        msg = []
 
545
        lastline, nlines = 0, 0
 
546
        for line in file(msgfilename, "r"):
 
547
            stripped_line = line.strip()
 
548
            # strip empty line before the log message starts
 
549
            if not started:
 
550
                if stripped_line != "":
 
551
                    started = True
 
552
                else:
 
553
                    continue
 
554
            # check for the ignore line only if there
 
555
            # is additional information at the end
 
556
            if hasinfo and stripped_line == ignoreline:
 
557
                break
 
558
            nlines += 1
 
559
            # keep track of the last line that had some content
 
560
            if stripped_line != "":
 
561
                lastline = nlines
 
562
            msg.append(line)
 
563
            
 
564
        if len(msg) == 0:
 
565
            return None
 
566
        # delete empty lines at the end
 
567
        del msg[lastline:]
 
568
        # add a newline at the end, if needed
 
569
        if not msg[-1].endswith("\n"):
 
570
            return "%s%s" % ("".join(msg), "\n")
 
571
        else:
 
572
            return "".join(msg)
 
573
    finally:
 
574
        # delete the msg file in any case
 
575
        try: os.unlink(msgfilename)
 
576
        except IOError: pass