~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tree.py

  • Committer: Martin Pool
  • Date: 2005-05-10 03:55:34 UTC
  • Revision ID: mbp@sourcefrog.net-20050510035534-643062e821052ac5
- Add fortune-cookie external plugin demonstration

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# Copyright (C) 2005 Canonical Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
21
20
from sets import Set
22
21
import os.path, os, fnmatch
23
22
 
 
23
from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \
 
24
     joinpath, splitpath, appendpath, isdir, isfile, file_kind, fingerprint_file
 
25
import errno
 
26
from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE
 
27
 
24
28
from inventory import Inventory
25
29
from trace import mutter, note
26
 
from osutils import pumpfile, compare_files, filesize, quotefn, sha_file, \
27
 
     joinpath, splitpath, appendpath, isdir, isfile, file_kind
28
30
from errors import bailout
29
31
import branch
30
 
from stat import S_ISREG, S_ISDIR, ST_MODE, ST_SIZE
31
32
 
32
33
import bzrlib
33
34
 
74
75
                         doc="Inventory of this Tree")
75
76
 
76
77
    def _check_retrieved(self, ie, f):
77
 
        # TODO: Test this check by damaging the store?
78
 
        if ie.text_size is not None:
79
 
            fs = filesize(f)
80
 
            if fs != ie.text_size:
 
78
        fp = fingerprint_file(f)
 
79
        f.seek(0)
 
80
        
 
81
        if ie.text_size != None:
 
82
            if ie.text_size != fp['size']:
81
83
                bailout("mismatched size for file %r in %r" % (ie.file_id, self._store),
82
84
                        ["inventory expects %d bytes" % ie.text_size,
83
 
                         "file is actually %d bytes" % fs,
 
85
                         "file is actually %d bytes" % fp['size'],
84
86
                         "store is probably damaged/corrupt"])
85
87
 
86
 
        f_hash = sha_file(f)
87
 
        f.seek(0)
88
 
        if ie.text_sha1 != f_hash:
 
88
        if ie.text_sha1 != fp['sha1']:
89
89
            bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store),
90
90
                    ["inventory expects %s" % ie.text_sha1,
91
 
                     "file is actually %s" % f_hash,
 
91
                     "file is actually %s" % fp['sha1'],
92
92
                     "store is probably damaged/corrupt"])
93
93
 
94
94
 
95
 
    def export(self, dest):
 
95
    def print_file(self, fileid):
 
96
        """Print file with id `fileid` to stdout."""
 
97
        import sys
 
98
        pumpfile(self.get_file(fileid), sys.stdout)
 
99
        
 
100
        
 
101
    def export(self, dest):        
96
102
        """Export this tree to a new directory.
97
103
 
98
104
        `dest` should not exist, and will be created holding the
99
105
        contents of this tree.
100
106
 
101
 
        :todo: To handle subdirectories we need to create the
 
107
        TODO: To handle subdirectories we need to create the
102
108
               directories first.
103
109
 
104
110
        :note: If the export fails, the destination directory will be
115
121
            elif kind == 'file':
116
122
                pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb'))
117
123
            else:
118
 
                bailout("don't know how to export {%s} of kind %r", fid, kind)
 
124
                bailout("don't know how to export {%s} of kind %r" % (fid, kind))
119
125
            mutter("  export {%s} kind %s to %s" % (ie.file_id, kind, fullpath))
120
126
 
121
127
 
138
144
        return "<%s of %s>" % (self.__class__.__name__,
139
145
                               self.basedir)
140
146
 
141
 
    def _rel(self, filename):
 
147
    def abspath(self, filename):
142
148
        return os.path.join(self.basedir, filename)
143
149
 
144
150
    def has_filename(self, filename):
145
 
        return os.path.exists(self._rel(filename))
 
151
        return os.path.exists(self.abspath(filename))
146
152
 
147
153
    def get_file(self, file_id):
148
154
        return self.get_file_byname(self.id2path(file_id))
149
155
 
150
156
    def get_file_byname(self, filename):
151
 
        return file(self._rel(filename), 'rb')
 
157
        return file(self.abspath(filename), 'rb')
152
158
 
153
159
    def _get_store_filename(self, file_id):
154
 
        return self._rel(self.id2path(file_id))
 
160
        ## XXX: badly named; this isn't in the store at all
 
161
        return self.abspath(self.id2path(file_id))
155
162
 
156
163
    def has_id(self, file_id):
157
164
        # files that have been deleted are excluded
158
165
        if not self.inventory.has_id(file_id):
159
166
            return False
160
 
        return os.access(self._rel(self.inventory.id2path(file_id)), os.F_OK)
 
167
        return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK)
161
168
 
162
169
    def get_file_size(self, file_id):
163
170
        return os.stat(self._get_store_filename(file_id))[ST_SIZE]
176
183
            return '?'
177
184
 
178
185
 
179
 
    def file_kind(self, filename):
180
 
        if isfile(self._rel(filename)):
181
 
            return 'file'
182
 
        elif isdir(self._rel(filename)):
183
 
            return 'directory'
184
 
        else:
185
 
            return 'unknown'
186
 
 
187
 
 
188
186
    def list_files(self):
189
187
        """Recursively list all files as (path, class, kind, id).
190
188
 
197
195
        """
198
196
        inv = self.inventory
199
197
 
200
 
        def descend(from_dir, from_dir_id, dp):
 
198
        def descend(from_dir_relpath, from_dir_id, dp):
201
199
            ls = os.listdir(dp)
202
200
            ls.sort()
203
201
            for f in ls:
 
202
                ## TODO: If we find a subdirectory with its own .bzr
 
203
                ## directory, then that is a separate tree and we
 
204
                ## should exclude it.
204
205
                if bzrlib.BZRDIR == f:
205
206
                    continue
206
207
 
207
208
                # path within tree
208
 
                fp = appendpath(from_dir, f)
 
209
                fp = appendpath(from_dir_relpath, f)
209
210
 
210
211
                # absolute path
211
212
                fap = appendpath(dp, f)
237
238
                for ff in descend(fp, f_ie.file_id, fap):
238
239
                    yield ff
239
240
 
240
 
        for f in descend('', None, self.basedir):
 
241
        for f in descend('', inv.root.file_id, self.basedir):
241
242
            yield f
242
243
            
243
244
 
244
245
 
245
 
    def unknowns(self, path='', dir_id=None):
246
 
        """Yield names of unknown files in this WorkingTree.
 
246
    def unknowns(self):
 
247
        for subp in self.extras():
 
248
            if not self.is_ignored(subp):
 
249
                yield subp
 
250
 
 
251
 
 
252
    def extras(self):
 
253
        """Yield all unknown files in this WorkingTree.
247
254
 
248
255
        If there are any unknown directories then only the directory is
249
256
        returned, not all its children.  But if there are unknown files
251
258
 
252
259
        Currently returned depth-first, sorted by name within directories.
253
260
        """
254
 
        for fpath, fclass, fkind, fid in self.list_files():
255
 
            if fclass == '?':
256
 
                yield fpath
257
 
                
 
261
        ## TODO: Work from given directory downwards
 
262
        
 
263
        for path, dir_entry in self.inventory.directories():
 
264
            mutter("search for unknowns in %r" % path)
 
265
            dirabs = self.abspath(path)
 
266
            if not isdir(dirabs):
 
267
                # e.g. directory deleted
 
268
                continue
 
269
 
 
270
            fl = []
 
271
            for subf in os.listdir(dirabs):
 
272
                if (subf != '.bzr'
 
273
                    and (subf not in dir_entry.children)):
 
274
                    fl.append(subf)
 
275
            
 
276
            fl.sort()
 
277
            for subf in fl:
 
278
                subp = appendpath(path, subf)
 
279
                yield subp
 
280
 
258
281
 
259
282
    def ignored_files(self):
260
 
        for fpath, fclass, fkind, fid in self.list_files():
261
 
            if fclass == 'I':
262
 
                yield fpath
 
283
        """Yield list of PATH, IGNORE_PATTERN"""
 
284
        for subp in self.extras():
 
285
            pat = self.is_ignored(subp)
 
286
            if pat != None:
 
287
                yield subp, pat
263
288
 
264
289
 
265
290
    def get_ignore_list(self):
266
 
        """Return list of ignore patterns."""
 
291
        """Return list of ignore patterns.
 
292
 
 
293
        Cached in the Tree object after the first call.
 
294
        """
 
295
        if hasattr(self, '_ignorelist'):
 
296
            return self._ignorelist
 
297
 
 
298
        l = bzrlib.DEFAULT_IGNORE[:]
267
299
        if self.has_filename(bzrlib.IGNORE_FILENAME):
268
300
            f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
269
 
            return [line.rstrip("\n\r") for line in f.readlines()]
270
 
        else:
271
 
            return bzrlib.DEFAULT_IGNORE
 
301
            l.extend([line.rstrip("\n\r") for line in f.readlines()])
 
302
        self._ignorelist = l
 
303
        return l
272
304
 
273
305
 
274
306
    def is_ignored(self, filename):
275
 
        """Check whether the filename matches an ignore pattern.
276
 
 
277
 
        Patterns containing '/' need to match the whole path; others
278
 
        match against only the last component."""
279
 
        ## TODO: Take them from a file, not hardcoded
280
 
        ## TODO: Use extended zsh-style globs maybe?
281
 
        ## TODO: Use '**' to match directories?
 
307
        r"""Check whether the filename matches an ignore pattern.
 
308
 
 
309
        Patterns containing '/' or '\' need to match the whole path;
 
310
        others match against only the last component.
 
311
 
 
312
        If the file is ignored, returns the pattern which caused it to
 
313
        be ignored, otherwise None.  So this can simply be used as a
 
314
        boolean if desired."""
 
315
 
 
316
        # TODO: Use '**' to match directories, and other extended
 
317
        # globbing stuff from cvs/rsync.
 
318
 
 
319
        # XXX: fnmatch is actually not quite what we want: it's only
 
320
        # approximately the same as real Unix fnmatch, and doesn't
 
321
        # treat dotfiles correctly and allows * to match /.
 
322
        # Eventually it should be replaced with something more
 
323
        # accurate.
 
324
        
282
325
        for pat in self.get_ignore_list():
283
 
            if '/' in pat:
284
 
                if fnmatch.fnmatchcase(filename, pat):
285
 
                    return True
 
326
            if '/' in pat or '\\' in pat:
 
327
                
 
328
                # as a special case, you can put ./ at the start of a
 
329
                # pattern; this is good to match in the top-level
 
330
                # only;
 
331
                
 
332
                if (pat[:2] == './') or (pat[:2] == '.\\'):
 
333
                    newpat = pat[2:]
 
334
                else:
 
335
                    newpat = pat
 
336
                if fnmatch.fnmatchcase(filename, newpat):
 
337
                    return pat
286
338
            else:
287
339
                if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
288
 
                    return True
289
 
        return False
 
340
                    return pat
 
341
        return None
290
342
        
291
343
 
292
344
        
297
349
 
298
350
    File text can be retrieved from the text store.
299
351
 
300
 
    :todo: Some kind of `__repr__` method, but a good one
 
352
    TODO: Some kind of `__repr__` method, but a good one
301
353
           probably means knowing the branch and revision number,
302
354
           or at least passing a description to the constructor.
303
355
    """
310
362
        ie = self._inventory[file_id]
311
363
        f = self._store[ie.text_id]
312
364
        mutter("  get fileid{%s} from %r" % (file_id, self))
313
 
        fs = filesize(f)
314
 
        if ie.text_size is None:
315
 
            note("warning: no text size recorded on %r" % ie)
316
365
        self._check_retrieved(ie, f)
317
366
        return f
318
367
 
400
449
 
401
450
    
402
451
 
 
452
def find_renames(old_inv, new_inv):
 
453
    for file_id in old_inv:
 
454
        if file_id not in new_inv:
 
455
            continue
 
456
        old_name = old_inv.id2path(file_id)
 
457
        new_name = new_inv.id2path(file_id)
 
458
        if old_name != new_name:
 
459
            yield (old_name, new_name)
 
460