~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tree.py

  • Committer: Martin Pool
  • Date: 2005-05-06 03:20:15 UTC
  • Revision ID: mbp@sourcefrog.net-20050506032014-decf4918803147d2
- split out notes on storing annotations in revfiles

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
 
53
54
    trees or versioned trees.
54
55
    """
55
56
    
56
 
    def get_file(self, file_id):
57
 
        """Return an open file-like object for given file id."""
58
 
        raise NotImplementedError()
59
 
 
60
57
    def has_filename(self, filename):
61
58
        """True if the tree has given filename."""
62
59
        raise NotImplementedError()
78
75
                         doc="Inventory of this Tree")
79
76
 
80
77
    def _check_retrieved(self, ie, f):
81
 
        # TODO: Test this check by damaging the store?
82
 
        if ie.text_size is not None:
83
 
            fs = filesize(f)
84
 
            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']:
85
83
                bailout("mismatched size for file %r in %r" % (ie.file_id, self._store),
86
84
                        ["inventory expects %d bytes" % ie.text_size,
87
 
                         "file is actually %d bytes" % fs,
 
85
                         "file is actually %d bytes" % fp['size'],
88
86
                         "store is probably damaged/corrupt"])
89
87
 
90
 
        f_hash = sha_file(f)
91
 
        f.seek(0)
92
 
        if ie.text_sha1 != f_hash:
 
88
        if ie.text_sha1 != fp['sha1']:
93
89
            bailout("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store),
94
90
                    ["inventory expects %s" % ie.text_sha1,
95
 
                     "file is actually %s" % f_hash,
 
91
                     "file is actually %s" % fp['sha1'],
96
92
                     "store is probably damaged/corrupt"])
97
93
 
98
94
 
99
 
    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):        
100
102
        """Export this tree to a new directory.
101
103
 
102
104
        `dest` should not exist, and will be created holding the
103
105
        contents of this tree.
104
106
 
105
 
        :todo: To handle subdirectories we need to create the
 
107
        TODO: To handle subdirectories we need to create the
106
108
               directories first.
107
109
 
108
110
        :note: If the export fails, the destination directory will be
119
121
            elif kind == 'file':
120
122
                pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb'))
121
123
            else:
122
 
                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))
123
125
            mutter("  export {%s} kind %s to %s" % (ie.file_id, kind, fullpath))
124
126
 
125
127
 
142
144
        return "<%s of %s>" % (self.__class__.__name__,
143
145
                               self.basedir)
144
146
 
145
 
    def _rel(self, filename):
 
147
    def abspath(self, filename):
146
148
        return os.path.join(self.basedir, filename)
147
149
 
148
150
    def has_filename(self, filename):
149
 
        return os.path.exists(self._rel(filename))
 
151
        return os.path.exists(self.abspath(filename))
150
152
 
151
153
    def get_file(self, file_id):
152
 
        return file(self._get_store_filename(file_id), 'rb')
 
154
        return self.get_file_byname(self.id2path(file_id))
 
155
 
 
156
    def get_file_byname(self, filename):
 
157
        return file(self.abspath(filename), 'rb')
153
158
 
154
159
    def _get_store_filename(self, file_id):
155
 
        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))
 
162
 
 
163
    def has_id(self, file_id):
 
164
        # files that have been deleted are excluded
 
165
        if not self.inventory.has_id(file_id):
 
166
            return False
 
167
        return os.access(self.abspath(self.inventory.id2path(file_id)), os.F_OK)
156
168
 
157
169
    def get_file_size(self, file_id):
158
170
        return os.stat(self._get_store_filename(file_id))[ST_SIZE]
171
183
            return '?'
172
184
 
173
185
 
174
 
    def file_kind(self, filename):
175
 
        if isfile(self._rel(filename)):
176
 
            return 'file'
177
 
        elif isdir(self._rel(filename)):
178
 
            return 'directory'
179
 
        else:
180
 
            return 'unknown'
181
 
 
182
 
 
183
186
    def list_files(self):
184
187
        """Recursively list all files as (path, class, kind, id).
185
188
 
192
195
        """
193
196
        inv = self.inventory
194
197
 
195
 
        def descend(from_dir, from_dir_id, dp):
 
198
        def descend(from_dir_relpath, from_dir_id, dp):
196
199
            ls = os.listdir(dp)
197
200
            ls.sort()
198
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.
199
205
                if bzrlib.BZRDIR == f:
200
206
                    continue
201
207
 
202
208
                # path within tree
203
 
                fp = appendpath(from_dir, f)
 
209
                fp = appendpath(from_dir_relpath, f)
204
210
 
205
211
                # absolute path
206
212
                fap = appendpath(dp, f)
232
238
                for ff in descend(fp, f_ie.file_id, fap):
233
239
                    yield ff
234
240
 
235
 
        for f in descend('', None, self.basedir):
 
241
        for f in descend('', inv.root.file_id, self.basedir):
236
242
            yield f
237
243
            
238
244
 
239
245
 
240
 
    def unknowns(self, path='', dir_id=None):
241
 
        """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.
242
254
 
243
255
        If there are any unknown directories then only the directory is
244
256
        returned, not all its children.  But if there are unknown files
246
258
 
247
259
        Currently returned depth-first, sorted by name within directories.
248
260
        """
249
 
        for fpath, fclass, fkind, fid in self.list_files():
250
 
            if fclass == '?':
251
 
                yield fpath
252
 
                
 
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
 
253
281
 
254
282
    def ignored_files(self):
255
 
        for fpath, fclass, fkind, fid in self.list_files():
256
 
            if fclass == 'I':
257
 
                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
 
288
 
 
289
 
 
290
    def get_ignore_list(self):
 
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[:]
 
299
        if self.has_filename(bzrlib.IGNORE_FILENAME):
 
300
            f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
 
301
            l.extend([line.rstrip("\n\r") for line in f.readlines()])
 
302
        self._ignorelist = l
 
303
        return l
258
304
 
259
305
 
260
306
    def is_ignored(self, filename):
261
 
        """Check whether the filename matches an ignore pattern."""
262
 
        ## TODO: Take them from a file, not hardcoded
263
 
        ## TODO: Use extended zsh-style globs maybe?
264
 
        ## TODO: Use '**' to match directories?
265
 
        ## TODO: Patterns without / should match in subdirectories?
266
 
        for i in bzrlib.DEFAULT_IGNORE:
267
 
            if fnmatch.fnmatchcase(filename, i):
268
 
                return True
269
 
        return False
 
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
        
 
325
        for pat in self.get_ignore_list():
 
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
 
338
            else:
 
339
                if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
 
340
                    return pat
 
341
        return None
270
342
        
271
343
 
272
344
        
277
349
 
278
350
    File text can be retrieved from the text store.
279
351
 
280
 
    :todo: Some kind of `__repr__` method, but a good one
 
352
    TODO: Some kind of `__repr__` method, but a good one
281
353
           probably means knowing the branch and revision number,
282
354
           or at least passing a description to the constructor.
283
355
    """
290
362
        ie = self._inventory[file_id]
291
363
        f = self._store[ie.text_id]
292
364
        mutter("  get fileid{%s} from %r" % (file_id, self))
293
 
        fs = filesize(f)
294
 
        if ie.text_size is None:
295
 
            note("warning: no text size recorded on %r" % ie)
296
365
        self._check_retrieved(ie, f)
297
366
        return f
298
367
 
380
449
 
381
450
    
382
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