~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/workingtree.py

  • Committer: Robert Collins
  • Date: 2005-10-16 22:04:54 UTC
  • mto: This revision was merged to the branch mainline in revision 1458.
  • Revision ID: robertc@lifelesslap.robertcollins.net-20051016220454-0418f1911d37b342
move branch._relpath into osutils as relpath

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
# TODO: Don't allow WorkingTrees to be constructed for remote branches.
 
18
 
 
19
# FIXME: I don't know if writing out the cache from the destructor is really a
 
20
# good idea, because destructors are considered poor taste in Python, and
 
21
# it's not predictable when it will be written out.
 
22
 
 
23
import os
 
24
import stat
 
25
import fnmatch
 
26
 
 
27
from bzrlib.branch import Branch
 
28
import bzrlib.tree
 
29
from bzrlib.osutils import appendpath, file_kind, isdir, splitpath
 
30
from bzrlib.errors import BzrCheckError
 
31
from bzrlib.trace import mutter
 
32
 
 
33
class TreeEntry(object):
 
34
    """An entry that implements the minium interface used by commands.
 
35
 
 
36
    This needs further inspection, it may be better to have 
 
37
    InventoryEntries without ids - though that seems wrong. For now,
 
38
    this is a parallel hierarchy to InventoryEntry, and needs to become
 
39
    one of several things: decorates to that hierarchy, children of, or
 
40
    parents of it.
 
41
    Another note is that these objects are currently only used when there is
 
42
    no InventoryEntry available - i.e. for unversioned objects.
 
43
    Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
 
44
    """
 
45
 
 
46
    def __eq__(self, other):
 
47
        # yes, this us ugly, TODO: best practice __eq__ style.
 
48
        return (isinstance(other, TreeEntry)
 
49
                and other.__class__ == self.__class__)
 
50
 
 
51
    def kind_character(self):
 
52
        return "???"
 
53
 
 
54
 
 
55
class TreeDirectory(TreeEntry):
 
56
    """See TreeEntry. This is a directory in a working tree."""
 
57
 
 
58
    def __eq__(self, other):
 
59
        return (isinstance(other, TreeDirectory)
 
60
                and other.__class__ == self.__class__)
 
61
 
 
62
    def kind_character(self):
 
63
        return "/"
 
64
 
 
65
 
 
66
class TreeFile(TreeEntry):
 
67
    """See TreeEntry. This is a regular file in a working tree."""
 
68
 
 
69
    def __eq__(self, other):
 
70
        return (isinstance(other, TreeFile)
 
71
                and other.__class__ == self.__class__)
 
72
 
 
73
    def kind_character(self):
 
74
        return ''
 
75
 
 
76
 
 
77
class TreeLink(TreeEntry):
 
78
    """See TreeEntry. This is a symlink in a working tree."""
 
79
 
 
80
    def __eq__(self, other):
 
81
        return (isinstance(other, TreeLink)
 
82
                and other.__class__ == self.__class__)
 
83
 
 
84
    def kind_character(self):
 
85
        return ''
 
86
 
 
87
 
 
88
class WorkingTree(bzrlib.tree.Tree):
 
89
    """Working copy tree.
 
90
 
 
91
    The inventory is held in the `Branch` working-inventory, and the
 
92
    files are in a directory on disk.
 
93
 
 
94
    It is possible for a `WorkingTree` to have a filename which is
 
95
    not listed in the Inventory and vice versa.
 
96
    """
 
97
    def __init__(self, basedir, branch=None):
 
98
        """Construct a WorkingTree for basedir.
 
99
 
 
100
        If the branch is not supplied, it is opened automatically.
 
101
        If the branch is supplied, it must be the branch for this basedir.
 
102
        (branch.base is not cross checked, because for remote branches that
 
103
        would be meaningless).
 
104
        """
 
105
        from bzrlib.hashcache import HashCache
 
106
        from bzrlib.trace import note, mutter
 
107
 
 
108
        if branch is None:
 
109
            branch = Branch.open(basedir)
 
110
        self._inventory = branch.inventory
 
111
        self.path2id = self._inventory.path2id
 
112
        self.branch = branch
 
113
        self.basedir = basedir
 
114
 
 
115
        # update the whole cache up front and write to disk if anything changed;
 
116
        # in the future we might want to do this more selectively
 
117
        hc = self._hashcache = HashCache(basedir)
 
118
        hc.read()
 
119
        hc.scan()
 
120
 
 
121
        if hc.needs_write:
 
122
            mutter("write hc")
 
123
            hc.write()
 
124
            
 
125
            
 
126
    def __del__(self):
 
127
        if self._hashcache.needs_write:
 
128
            self._hashcache.write()
 
129
 
 
130
 
 
131
    def __iter__(self):
 
132
        """Iterate through file_ids for this tree.
 
133
 
 
134
        file_ids are in a WorkingTree if they are in the working inventory
 
135
        and the working file exists.
 
136
        """
 
137
        inv = self._inventory
 
138
        for path, ie in inv.iter_entries():
 
139
            if bzrlib.osutils.lexists(self.abspath(path)):
 
140
                yield ie.file_id
 
141
 
 
142
 
 
143
    def __repr__(self):
 
144
        return "<%s of %s>" % (self.__class__.__name__,
 
145
                               getattr(self, 'basedir', None))
 
146
 
 
147
 
 
148
 
 
149
    def abspath(self, filename):
 
150
        return os.path.join(self.basedir, filename)
 
151
 
 
152
    def has_filename(self, filename):
 
153
        return bzrlib.osutils.lexists(self.abspath(filename))
 
154
 
 
155
    def get_file(self, file_id):
 
156
        return self.get_file_byname(self.id2path(file_id))
 
157
 
 
158
    def get_file_byname(self, filename):
 
159
        return file(self.abspath(filename), 'rb')
 
160
 
 
161
    def _get_store_filename(self, file_id):
 
162
        ## XXX: badly named; this isn't in the store at all
 
163
        return self.abspath(self.id2path(file_id))
 
164
 
 
165
 
 
166
    def id2abspath(self, file_id):
 
167
        return self.abspath(self.id2path(file_id))
 
168
 
 
169
                
 
170
    def has_id(self, file_id):
 
171
        # files that have been deleted are excluded
 
172
        inv = self._inventory
 
173
        if not inv.has_id(file_id):
 
174
            return False
 
175
        path = inv.id2path(file_id)
 
176
        return bzrlib.osutils.lexists(self.abspath(path))
 
177
 
 
178
 
 
179
    __contains__ = has_id
 
180
    
 
181
 
 
182
    def get_file_size(self, file_id):
 
183
        return os.path.getsize(self.id2abspath(file_id))
 
184
 
 
185
    def get_file_sha1(self, file_id):
 
186
        path = self._inventory.id2path(file_id)
 
187
        return self._hashcache.get_sha1(path)
 
188
 
 
189
 
 
190
    def is_executable(self, file_id):
 
191
        if os.name == "nt":
 
192
            return self._inventory[file_id].executable
 
193
        else:
 
194
            path = self._inventory.id2path(file_id)
 
195
            mode = os.lstat(self.abspath(path)).st_mode
 
196
            return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
 
197
 
 
198
    def get_symlink_target(self, file_id):
 
199
        return os.readlink(self.id2abspath(file_id))
 
200
 
 
201
    def file_class(self, filename):
 
202
        if self.path2id(filename):
 
203
            return 'V'
 
204
        elif self.is_ignored(filename):
 
205
            return 'I'
 
206
        else:
 
207
            return '?'
 
208
 
 
209
 
 
210
    def list_files(self):
 
211
        """Recursively list all files as (path, class, kind, id).
 
212
 
 
213
        Lists, but does not descend into unversioned directories.
 
214
 
 
215
        This does not include files that have been deleted in this
 
216
        tree.
 
217
 
 
218
        Skips the control directory.
 
219
        """
 
220
        inv = self._inventory
 
221
 
 
222
        def descend(from_dir_relpath, from_dir_id, dp):
 
223
            ls = os.listdir(dp)
 
224
            ls.sort()
 
225
            for f in ls:
 
226
                ## TODO: If we find a subdirectory with its own .bzr
 
227
                ## directory, then that is a separate tree and we
 
228
                ## should exclude it.
 
229
                if bzrlib.BZRDIR == f:
 
230
                    continue
 
231
 
 
232
                # path within tree
 
233
                fp = appendpath(from_dir_relpath, f)
 
234
 
 
235
                # absolute path
 
236
                fap = appendpath(dp, f)
 
237
                
 
238
                f_ie = inv.get_child(from_dir_id, f)
 
239
                if f_ie:
 
240
                    c = 'V'
 
241
                elif self.is_ignored(fp):
 
242
                    c = 'I'
 
243
                else:
 
244
                    c = '?'
 
245
 
 
246
                fk = file_kind(fap)
 
247
 
 
248
                if f_ie:
 
249
                    if f_ie.kind != fk:
 
250
                        raise BzrCheckError("file %r entered as kind %r id %r, "
 
251
                                            "now of kind %r"
 
252
                                            % (fap, f_ie.kind, f_ie.file_id, fk))
 
253
 
 
254
                # make a last minute entry
 
255
                if f_ie:
 
256
                    entry = f_ie
 
257
                else:
 
258
                    if fk == 'directory':
 
259
                        entry = TreeDirectory()
 
260
                    elif fk == 'file':
 
261
                        entry = TreeFile()
 
262
                    elif fk == 'symlink':
 
263
                        entry = TreeLink()
 
264
                    else:
 
265
                        entry = TreeEntry()
 
266
                
 
267
                yield fp, c, fk, (f_ie and f_ie.file_id), entry
 
268
 
 
269
                if fk != 'directory':
 
270
                    continue
 
271
 
 
272
                if c != 'V':
 
273
                    # don't descend unversioned directories
 
274
                    continue
 
275
                
 
276
                for ff in descend(fp, f_ie.file_id, fap):
 
277
                    yield ff
 
278
 
 
279
        for f in descend('', inv.root.file_id, self.basedir):
 
280
            yield f
 
281
            
 
282
 
 
283
 
 
284
    def unknowns(self):
 
285
        for subp in self.extras():
 
286
            if not self.is_ignored(subp):
 
287
                yield subp
 
288
 
 
289
    def iter_conflicts(self):
 
290
        conflicted = set()
 
291
        for path in (s[0] for s in self.list_files()):
 
292
            stem = get_conflicted_stem(path)
 
293
            if stem is None:
 
294
                continue
 
295
            if stem not in conflicted:
 
296
                conflicted.add(stem)
 
297
                yield stem
 
298
 
 
299
    def extras(self):
 
300
        """Yield all unknown files in this WorkingTree.
 
301
 
 
302
        If there are any unknown directories then only the directory is
 
303
        returned, not all its children.  But if there are unknown files
 
304
        under a versioned subdirectory, they are returned.
 
305
 
 
306
        Currently returned depth-first, sorted by name within directories.
 
307
        """
 
308
        ## TODO: Work from given directory downwards
 
309
        for path, dir_entry in self.inventory.directories():
 
310
            mutter("search for unknowns in %r" % path)
 
311
            dirabs = self.abspath(path)
 
312
            if not isdir(dirabs):
 
313
                # e.g. directory deleted
 
314
                continue
 
315
 
 
316
            fl = []
 
317
            for subf in os.listdir(dirabs):
 
318
                if (subf != '.bzr'
 
319
                    and (subf not in dir_entry.children)):
 
320
                    fl.append(subf)
 
321
            
 
322
            fl.sort()
 
323
            for subf in fl:
 
324
                subp = appendpath(path, subf)
 
325
                yield subp
 
326
 
 
327
 
 
328
    def ignored_files(self):
 
329
        """Yield list of PATH, IGNORE_PATTERN"""
 
330
        for subp in self.extras():
 
331
            pat = self.is_ignored(subp)
 
332
            if pat != None:
 
333
                yield subp, pat
 
334
 
 
335
 
 
336
    def get_ignore_list(self):
 
337
        """Return list of ignore patterns.
 
338
 
 
339
        Cached in the Tree object after the first call.
 
340
        """
 
341
        if hasattr(self, '_ignorelist'):
 
342
            return self._ignorelist
 
343
 
 
344
        l = bzrlib.DEFAULT_IGNORE[:]
 
345
        if self.has_filename(bzrlib.IGNORE_FILENAME):
 
346
            f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
 
347
            l.extend([line.rstrip("\n\r") for line in f.readlines()])
 
348
        self._ignorelist = l
 
349
        return l
 
350
 
 
351
 
 
352
    def is_ignored(self, filename):
 
353
        r"""Check whether the filename matches an ignore pattern.
 
354
 
 
355
        Patterns containing '/' or '\' need to match the whole path;
 
356
        others match against only the last component.
 
357
 
 
358
        If the file is ignored, returns the pattern which caused it to
 
359
        be ignored, otherwise None.  So this can simply be used as a
 
360
        boolean if desired."""
 
361
 
 
362
        # TODO: Use '**' to match directories, and other extended
 
363
        # globbing stuff from cvs/rsync.
 
364
 
 
365
        # XXX: fnmatch is actually not quite what we want: it's only
 
366
        # approximately the same as real Unix fnmatch, and doesn't
 
367
        # treat dotfiles correctly and allows * to match /.
 
368
        # Eventually it should be replaced with something more
 
369
        # accurate.
 
370
        
 
371
        for pat in self.get_ignore_list():
 
372
            if '/' in pat or '\\' in pat:
 
373
                
 
374
                # as a special case, you can put ./ at the start of a
 
375
                # pattern; this is good to match in the top-level
 
376
                # only;
 
377
                
 
378
                if (pat[:2] == './') or (pat[:2] == '.\\'):
 
379
                    newpat = pat[2:]
 
380
                else:
 
381
                    newpat = pat
 
382
                if fnmatch.fnmatchcase(filename, newpat):
 
383
                    return pat
 
384
            else:
 
385
                if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
 
386
                    return pat
 
387
        else:
 
388
            return None
 
389
 
 
390
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
391
def get_conflicted_stem(path):
 
392
    for suffix in CONFLICT_SUFFIXES:
 
393
        if path.endswith(suffix):
 
394
            return path[:-len(suffix)]