~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tree.py

  • Committer: Martin Pool
  • Date: 2006-03-09 03:28:52 UTC
  • mto: This revision was merged to the branch mainline in revision 1602.
  • Revision ID: mbp@sourcefrog.net-20060309032852-1097eb1947d9bceb
doc

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
"""Tree classes, representing directory at point in time.
18
18
"""
19
19
 
20
 
from sets import Set
21
 
import os.path, os, fnmatch
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
 
 
28
 
from inventory import Inventory
29
 
from trace import mutter, note
30
 
from errors import bailout
31
 
import branch
 
20
import os
 
21
from cStringIO import StringIO
32
22
 
33
23
import bzrlib
 
24
from bzrlib.trace import mutter, note
 
25
from bzrlib.errors import BzrError, BzrCheckError
 
26
from bzrlib.inventory import Inventory
 
27
from bzrlib.osutils import appendpath, fingerprint_file
34
28
 
35
 
class Tree:
 
29
class Tree(object):
36
30
    """Abstract file tree.
37
31
 
38
32
    There are several subclasses:
61
55
    def has_id(self, file_id):
62
56
        return self.inventory.has_id(file_id)
63
57
 
64
 
    def id_set(self):
65
 
        """Return set of all ids in this tree."""
66
 
        return self.inventory.id_set()
 
58
    def has_or_had_id(self, file_id):
 
59
        if file_id == self.inventory.root.file_id:
 
60
            return True
 
61
        return self.inventory.has_id(file_id)
 
62
 
 
63
    __contains__ = has_id
 
64
 
 
65
    def __iter__(self):
 
66
        return iter(self.inventory)
67
67
 
68
68
    def id2path(self, file_id):
69
69
        return self.inventory.id2path(file_id)
70
70
 
 
71
    def kind(self, file_id):
 
72
        raise NotImplementedError("subclasses must implement kind")
 
73
 
71
74
    def _get_inventory(self):
72
75
        return self._inventory
 
76
    
 
77
    def get_file_by_path(self, path):
 
78
        return self.get_file(self._inventory.path2id(path))
73
79
 
74
80
    inventory = property(_get_inventory,
75
81
                         doc="Inventory of this Tree")
76
82
 
77
83
    def _check_retrieved(self, ie, f):
 
84
        if not __debug__:
 
85
            return  
78
86
        fp = fingerprint_file(f)
79
87
        f.seek(0)
80
88
        
81
 
        if ie.text_size is not None:
 
89
        if ie.text_size != None:
82
90
            if ie.text_size != fp['size']:
83
 
                bailout("mismatched size for file %r in %r" % (ie.file_id, self._store),
 
91
                raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store),
84
92
                        ["inventory expects %d bytes" % ie.text_size,
85
93
                         "file is actually %d bytes" % fp['size'],
86
94
                         "store is probably damaged/corrupt"])
87
95
 
88
96
        if ie.text_sha1 != fp['sha1']:
89
 
            bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store),
 
97
            raise BzrError("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store),
90
98
                    ["inventory expects %s" % ie.text_sha1,
91
99
                     "file is actually %s" % fp['sha1'],
92
100
                     "store is probably damaged/corrupt"])
93
101
 
94
102
 
95
 
    def export(self, dest):
96
 
        """Export this tree to a new directory.
97
 
 
98
 
        `dest` should not exist, and will be created holding the
99
 
        contents of this tree.
100
 
 
101
 
        :todo: To handle subdirectories we need to create the
102
 
               directories first.
103
 
 
104
 
        :note: If the export fails, the destination directory will be
105
 
               left in a half-assed state.
106
 
        """
107
 
        os.mkdir(dest)
108
 
        mutter('export version %r' % self)
109
 
        inv = self.inventory
110
 
        for dp, ie in inv.iter_entries():
111
 
            kind = ie.kind
112
 
            fullpath = appendpath(dest, dp)
113
 
            if kind == 'directory':
114
 
                os.mkdir(fullpath)
115
 
            elif kind == 'file':
116
 
                pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb'))
117
 
            else:
118
 
                bailout("don't know how to export {%s} of kind %r", fid, kind)
119
 
            mutter("  export {%s} kind %s to %s" % (ie.file_id, kind, fullpath))
120
 
 
121
 
 
122
 
 
123
 
class WorkingTree(Tree):
124
 
    """Working copy tree.
125
 
 
126
 
    The inventory is held in the `Branch` working-inventory, and the
127
 
    files are in a directory on disk.
128
 
 
129
 
    It is possible for a `WorkingTree` to have a filename which is
130
 
    not listed in the Inventory and vice versa.
131
 
    """
132
 
    def __init__(self, basedir, inv):
133
 
        self._inventory = inv
134
 
        self.basedir = basedir
135
 
        self.path2id = inv.path2id
136
 
 
137
 
    def __repr__(self):
138
 
        return "<%s of %s>" % (self.__class__.__name__,
139
 
                               self.basedir)
140
 
 
141
 
    def abspath(self, filename):
142
 
        return os.path.join(self.basedir, filename)
143
 
 
144
 
    def has_filename(self, filename):
145
 
        return os.path.exists(self.abspath(filename))
146
 
 
147
 
    def get_file(self, file_id):
148
 
        return self.get_file_byname(self.id2path(file_id))
149
 
 
150
 
    def get_file_byname(self, filename):
151
 
        return file(self.abspath(filename), 'rb')
152
 
 
153
 
    def _get_store_filename(self, file_id):
154
 
        return self.abspath(self.id2path(file_id))
155
 
 
156
 
    def has_id(self, file_id):
157
 
        # files that have been deleted are excluded
158
 
        if not self.inventory.has_id(file_id):
159
 
            return False
160
 
        return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK)
161
 
 
162
 
    def get_file_size(self, file_id):
163
 
        return os.stat(self._get_store_filename(file_id))[ST_SIZE]
164
 
 
165
 
    def get_file_sha1(self, file_id):
166
 
        f = self.get_file(file_id)
167
 
        return sha_file(f)
168
 
 
169
 
 
170
 
    def file_class(self, filename):
171
 
        if self.path2id(filename):
172
 
            return 'V'
173
 
        elif self.is_ignored(filename):
174
 
            return 'I'
175
 
        else:
176
 
            return '?'
177
 
 
178
 
 
179
 
    def list_files(self):
180
 
        """Recursively list all files as (path, class, kind, id).
181
 
 
182
 
        Lists, but does not descend into unversioned directories.
183
 
 
184
 
        This does not include files that have been deleted in this
185
 
        tree.
186
 
 
187
 
        Skips the control directory.
188
 
        """
189
 
        inv = self.inventory
190
 
 
191
 
        def descend(from_dir, from_dir_id, dp):
192
 
            ls = os.listdir(dp)
193
 
            ls.sort()
194
 
            for f in ls:
195
 
                ## TODO: If we find a subdirectory with its own .bzr
196
 
                ## directory, then that is a separate tree and we
197
 
                ## should exclude it.
198
 
                if bzrlib.BZRDIR == f:
199
 
                    continue
200
 
 
201
 
                # path within tree
202
 
                fp = appendpath(from_dir, f)
203
 
 
204
 
                # absolute path
205
 
                fap = appendpath(dp, f)
206
 
                
207
 
                f_ie = inv.get_child(from_dir_id, f)
208
 
                if f_ie:
209
 
                    c = 'V'
210
 
                elif self.is_ignored(fp):
211
 
                    c = 'I'
212
 
                else:
213
 
                    c = '?'
214
 
 
215
 
                fk = file_kind(fap)
216
 
 
217
 
                if f_ie:
218
 
                    if f_ie.kind != fk:
219
 
                        bailout("file %r entered as kind %r id %r, now of kind %r"
220
 
                                % (fap, f_ie.kind, f_ie.file_id, fk))
221
 
 
222
 
                yield fp, c, fk, (f_ie and f_ie.file_id)
223
 
 
224
 
                if fk != 'directory':
225
 
                    continue
226
 
 
227
 
                if c != 'V':
228
 
                    # don't descend unversioned directories
229
 
                    continue
230
 
                
231
 
                for ff in descend(fp, f_ie.file_id, fap):
232
 
                    yield ff
233
 
 
234
 
        for f in descend('', None, self.basedir):
235
 
            yield f
236
 
            
237
 
 
238
 
 
239
 
    def unknowns(self):
240
 
        for subp in self.extras():
241
 
            if not self.is_ignored(subp):
242
 
                yield subp
243
 
 
244
 
 
245
 
    def extras(self):
246
 
        """Yield all unknown files in this WorkingTree.
247
 
 
248
 
        If there are any unknown directories then only the directory is
249
 
        returned, not all its children.  But if there are unknown files
250
 
        under a versioned subdirectory, they are returned.
251
 
 
252
 
        Currently returned depth-first, sorted by name within directories.
253
 
        """
254
 
        ## TODO: Work from given directory downwards
255
 
        
256
 
        for path, dir_entry in self.inventory.directories():
257
 
            mutter("search for unknowns in %r" % path)
258
 
            dirabs = self.abspath(path)
259
 
            if not isdir(dirabs):
260
 
                # e.g. directory deleted
261
 
                continue
262
 
 
263
 
            fl = []
264
 
            for subf in os.listdir(dirabs):
265
 
                if (subf != '.bzr'
266
 
                    and (subf not in dir_entry.children)):
267
 
                    fl.append(subf)
268
 
            
269
 
            fl.sort()
270
 
            for subf in fl:
271
 
                subp = appendpath(path, subf)
272
 
                yield subp
273
 
 
274
 
 
275
 
    def ignored_files(self):
276
 
        """Yield list of PATH, IGNORE_PATTERN"""
277
 
        for subp in self.extras():
278
 
            pat = self.is_ignored(subp)
279
 
            if pat != None:
280
 
                yield subp, pat
281
 
 
282
 
 
283
 
    def get_ignore_list(self):
284
 
        """Return list of ignore patterns.
285
 
 
286
 
        Cached in the Tree object after the first call.
287
 
        """
288
 
        if hasattr(self, '_ignorelist'):
289
 
            return self._ignorelist
290
 
 
291
 
        l = bzrlib.DEFAULT_IGNORE[:]
292
 
        if self.has_filename(bzrlib.IGNORE_FILENAME):
293
 
            f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
294
 
            l.extend([line.rstrip("\n\r") for line in f.readlines()])
295
 
        self._ignorelist = l
296
 
        return l
297
 
 
298
 
 
299
 
    def is_ignored(self, filename):
300
 
        """Check whether the filename matches an ignore pattern.
301
 
 
302
 
        Patterns containing '/' need to match the whole path; others
303
 
        match against only the last component.
304
 
 
305
 
        If the file is ignored, returns the pattern which caused it to
306
 
        be ignored, otherwise None.  So this can simply be used as a
307
 
        boolean if desired."""
308
 
 
309
 
        ## TODO: Use '**' to match directories, and other extended globbing stuff from cvs/rsync.
310
 
        
311
 
        for pat in self.get_ignore_list():
312
 
            if '/' in pat:
313
 
                # as a special case, you can put ./ at the start of a pattern;
314
 
                # this is good to match in the top-level only;
315
 
                if pat[:2] == './':
316
 
                    newpat = pat[2:]
317
 
                else:
318
 
                    newpat = pat
319
 
                if fnmatch.fnmatchcase(filename, newpat):
320
 
                    return pat
321
 
            else:
322
 
                if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
323
 
                    return pat
324
 
        return None
325
 
        
326
 
 
327
 
        
328
 
        
329
 
 
 
103
    def print_file(self, file_id):
 
104
        """Print file with id `file_id` to stdout."""
 
105
        import sys
 
106
        sys.stdout.write(self.get_file_text(file_id))
 
107
 
 
108
    def lock_read(self):
 
109
        pass
 
110
 
 
111
    def unlock(self):
 
112
        pass
 
113
        
 
114
        
330
115
class RevisionTree(Tree):
331
116
    """Tree viewing a previous revision.
332
117
 
333
118
    File text can be retrieved from the text store.
334
119
 
335
 
    :todo: Some kind of `__repr__` method, but a good one
 
120
    TODO: Some kind of `__repr__` method, but a good one
336
121
           probably means knowing the branch and revision number,
337
122
           or at least passing a description to the constructor.
338
123
    """
339
124
    
340
 
    def __init__(self, store, inv):
341
 
        self._store = store
 
125
    def __init__(self, branch, inv, revision_id):
 
126
        self._branch = branch
 
127
        self._weave_store = branch.weave_store
342
128
        self._inventory = inv
 
129
        self._revision_id = revision_id
 
130
 
 
131
    def get_weave(self, file_id):
 
132
        import bzrlib.transactions as transactions
 
133
        return self._weave_store.get_weave(file_id,
 
134
                self._branch.get_transaction())
 
135
 
 
136
    def get_weave_prelude(self, file_id):
 
137
        import bzrlib.transactions as transactions
 
138
        return self._weave_store.get_weave_prelude(file_id,
 
139
                self._branch.get_transaction())
 
140
 
 
141
    def get_file_lines(self, file_id):
 
142
        ie = self._inventory[file_id]
 
143
        weave = self.get_weave(file_id)
 
144
        return weave.get(ie.revision)
 
145
 
 
146
    def get_file_text(self, file_id):
 
147
        return ''.join(self.get_file_lines(file_id))
343
148
 
344
149
    def get_file(self, file_id):
345
 
        ie = self._inventory[file_id]
346
 
        f = self._store[ie.text_id]
347
 
        mutter("  get fileid{%s} from %r" % (file_id, self))
348
 
        self._check_retrieved(ie, f)
349
 
        return f
 
150
        return StringIO(self.get_file_text(file_id))
350
151
 
351
152
    def get_file_size(self, file_id):
352
153
        return self._inventory[file_id].text_size
353
154
 
354
155
    def get_file_sha1(self, file_id):
355
156
        ie = self._inventory[file_id]
356
 
        return ie.text_sha1
 
157
        if ie.kind == "file":
 
158
            return ie.text_sha1
 
159
 
 
160
    def is_executable(self, file_id):
 
161
        ie = self._inventory[file_id]
 
162
        if ie.kind != "file":
 
163
            return None 
 
164
        return self._inventory[file_id].executable
357
165
 
358
166
    def has_filename(self, filename):
359
167
        return bool(self.inventory.path2id(filename))
361
169
    def list_files(self):
362
170
        # The only files returned by this are those from the version
363
171
        for path, entry in self.inventory.iter_entries():
364
 
            yield path, 'V', entry.kind, entry.file_id
 
172
            yield path, 'V', entry.kind, entry.file_id, entry
 
173
 
 
174
    def get_symlink_target(self, file_id):
 
175
        ie = self._inventory[file_id]
 
176
        return ie.symlink_target;
 
177
 
 
178
    def kind(self, file_id):
 
179
        return self._inventory[file_id].kind
 
180
 
 
181
    def lock_read(self):
 
182
        self._branch.lock_read()
 
183
 
 
184
    def unlock(self):
 
185
        self._branch.unlock()
365
186
 
366
187
 
367
188
class EmptyTree(Tree):
368
189
    def __init__(self):
369
190
        self._inventory = Inventory()
370
191
 
 
192
    def get_symlink_target(self, file_id):
 
193
        return None
 
194
 
371
195
    def has_filename(self, filename):
372
196
        return False
373
197
 
 
198
    def kind(self, file_id):
 
199
        assert self._inventory[file_id].kind == "root_directory"
 
200
        return "root_directory"
 
201
 
374
202
    def list_files(self):
375
 
        if False:  # just to make it a generator
376
 
            yield None
 
203
        return iter([])
377
204
    
 
205
    def __contains__(self, file_id):
 
206
        return file_id in self._inventory
 
207
 
 
208
    def get_file_sha1(self, file_id):
 
209
        assert self._inventory[file_id].kind == "root_directory"
 
210
        return None
378
211
 
379
212
 
380
213
######################################################################
441
274
        if old_name != new_name:
442
275
            yield (old_name, new_name)
443
276
            
 
277
 
 
278