/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/workingtree.py

  • Committer: Aaron Bentley
  • Date: 2005-12-25 23:06:24 UTC
  • mto: (1185.67.11 bzr.revision-storage)
  • mto: This revision was merged to the branch mainline in revision 1550.
  • Revision ID: aaron.bentley@utoronto.ca-20051225230624-f61a1538912a578a
Added tests and fixes for LockableFiles.put_utf8(); imported IterableFile

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
"""WorkingTree object and friends.
 
18
 
 
19
A WorkingTree represents the editable working copy of a branch.
 
20
Operations which represent the WorkingTree are also done here, 
 
21
such as renaming or adding files.  The WorkingTree has an inventory 
 
22
which is updated by these operations.  A commit produces a 
 
23
new revision based on the workingtree and its inventory.
 
24
 
 
25
At the moment every WorkingTree has its own branch.  Remote
 
26
WorkingTrees aren't supported.
 
27
 
 
28
To get a WorkingTree, call Branch.working_tree():
 
29
"""
 
30
 
 
31
 
 
32
# TODO: Don't allow WorkingTrees to be constructed for remote branches if 
 
33
# they don't work.
 
34
 
 
35
# FIXME: I don't know if writing out the cache from the destructor is really a
 
36
# good idea, because destructors are considered poor taste in Python, and it's
 
37
# not predictable when it will be written out.
 
38
 
 
39
# TODO: Give the workingtree sole responsibility for the working inventory;
 
40
# remove the variable and references to it from the branch.  This may require
 
41
# updating the commit code so as to update the inventory within the working
 
42
# copy, and making sure there's only one WorkingTree for any directory on disk.
 
43
# At the momenthey may alias the inventory and have old copies of it in memory.
 
44
 
 
45
from copy import deepcopy
 
46
import os
 
47
import stat
 
48
import fnmatch
 
49
 
 
50
from bzrlib.branch import (Branch,
 
51
                           is_control_file,
 
52
                           needs_read_lock,
 
53
                           needs_write_lock,
 
54
                           quotefn)
 
55
from bzrlib.errors import (BzrCheckError,
 
56
                           BzrError,
 
57
                           DivergedBranches,
 
58
                           WeaveRevisionNotPresent,
 
59
                           NotBranchError,
 
60
                           NotVersionedError)
 
61
from bzrlib.inventory import InventoryEntry
 
62
from bzrlib.osutils import (appendpath,
 
63
                            compact_date,
 
64
                            file_kind,
 
65
                            isdir,
 
66
                            pumpfile,
 
67
                            splitpath,
 
68
                            rand_bytes,
 
69
                            realpath,
 
70
                            relpath,
 
71
                            rename)
 
72
from bzrlib.textui import show_status
 
73
import bzrlib.tree
 
74
from bzrlib.trace import mutter
 
75
import bzrlib.xml5
 
76
 
 
77
 
 
78
def gen_file_id(name):
 
79
    """Return new file id.
 
80
 
 
81
    This should probably generate proper UUIDs, but for the moment we
 
82
    cope with just randomness because running uuidgen every time is
 
83
    slow."""
 
84
    import re
 
85
    from binascii import hexlify
 
86
    from time import time
 
87
 
 
88
    # get last component
 
89
    idx = name.rfind('/')
 
90
    if idx != -1:
 
91
        name = name[idx+1 : ]
 
92
    idx = name.rfind('\\')
 
93
    if idx != -1:
 
94
        name = name[idx+1 : ]
 
95
 
 
96
    # make it not a hidden file
 
97
    name = name.lstrip('.')
 
98
 
 
99
    # remove any wierd characters; we don't escape them but rather
 
100
    # just pull them out
 
101
    name = re.sub(r'[^\w.]', '', name)
 
102
 
 
103
    s = hexlify(rand_bytes(8))
 
104
    return '-'.join((name, compact_date(time()), s))
 
105
 
 
106
 
 
107
def gen_root_id():
 
108
    """Return a new tree-root file id."""
 
109
    return gen_file_id('TREE_ROOT')
 
110
 
 
111
 
 
112
class TreeEntry(object):
 
113
    """An entry that implements the minium interface used by commands.
 
114
 
 
115
    This needs further inspection, it may be better to have 
 
116
    InventoryEntries without ids - though that seems wrong. For now,
 
117
    this is a parallel hierarchy to InventoryEntry, and needs to become
 
118
    one of several things: decorates to that hierarchy, children of, or
 
119
    parents of it.
 
120
    Another note is that these objects are currently only used when there is
 
121
    no InventoryEntry available - i.e. for unversioned objects.
 
122
    Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
 
123
    """
 
124
 
 
125
    def __eq__(self, other):
 
126
        # yes, this us ugly, TODO: best practice __eq__ style.
 
127
        return (isinstance(other, TreeEntry)
 
128
                and other.__class__ == self.__class__)
 
129
 
 
130
    def kind_character(self):
 
131
        return "???"
 
132
 
 
133
 
 
134
class TreeDirectory(TreeEntry):
 
135
    """See TreeEntry. This is a directory in a working tree."""
 
136
 
 
137
    def __eq__(self, other):
 
138
        return (isinstance(other, TreeDirectory)
 
139
                and other.__class__ == self.__class__)
 
140
 
 
141
    def kind_character(self):
 
142
        return "/"
 
143
 
 
144
 
 
145
class TreeFile(TreeEntry):
 
146
    """See TreeEntry. This is a regular file in a working tree."""
 
147
 
 
148
    def __eq__(self, other):
 
149
        return (isinstance(other, TreeFile)
 
150
                and other.__class__ == self.__class__)
 
151
 
 
152
    def kind_character(self):
 
153
        return ''
 
154
 
 
155
 
 
156
class TreeLink(TreeEntry):
 
157
    """See TreeEntry. This is a symlink in a working tree."""
 
158
 
 
159
    def __eq__(self, other):
 
160
        return (isinstance(other, TreeLink)
 
161
                and other.__class__ == self.__class__)
 
162
 
 
163
    def kind_character(self):
 
164
        return ''
 
165
 
 
166
 
 
167
class WorkingTree(bzrlib.tree.Tree):
 
168
    """Working copy tree.
 
169
 
 
170
    The inventory is held in the `Branch` working-inventory, and the
 
171
    files are in a directory on disk.
 
172
 
 
173
    It is possible for a `WorkingTree` to have a filename which is
 
174
    not listed in the Inventory and vice versa.
 
175
    """
 
176
 
 
177
    def __init__(self, basedir=u'.', branch=None):
 
178
        """Construct a WorkingTree for basedir.
 
179
 
 
180
        If the branch is not supplied, it is opened automatically.
 
181
        If the branch is supplied, it must be the branch for this basedir.
 
182
        (branch.base is not cross checked, because for remote branches that
 
183
        would be meaningless).
 
184
        """
 
185
        from bzrlib.hashcache import HashCache
 
186
        from bzrlib.trace import note, mutter
 
187
        assert isinstance(basedir, basestring), \
 
188
            "base directory %r is not a string" % basedir
 
189
        if branch is None:
 
190
            branch = Branch.open(basedir)
 
191
        assert isinstance(branch, Branch), \
 
192
            "branch %r is not a Branch" % branch
 
193
        self.branch = branch
 
194
        self.basedir = realpath(basedir)
 
195
 
 
196
        self._set_inventory(self.read_working_inventory())
 
197
 
 
198
        # update the whole cache up front and write to disk if anything changed;
 
199
        # in the future we might want to do this more selectively
 
200
        # two possible ways offer themselves : in self._unlock, write the cache
 
201
        # if needed, or, when the cache sees a change, append it to the hash
 
202
        # cache file, and have the parser take the most recent entry for a
 
203
        # given path only.
 
204
        hc = self._hashcache = HashCache(basedir)
 
205
        hc.read()
 
206
        hc.scan()
 
207
 
 
208
        if hc.needs_write:
 
209
            mutter("write hc")
 
210
            hc.write()
 
211
 
 
212
    def _set_inventory(self, inv):
 
213
        self._inventory = inv
 
214
        self.path2id = self._inventory.path2id
 
215
 
 
216
    @staticmethod
 
217
    def open_containing(path=None):
 
218
        """Open an existing working tree which has its root about path.
 
219
        
 
220
        This probes for a working tree at path and searches upwards from there.
 
221
 
 
222
        Basically we keep looking up until we find the control directory or
 
223
        run into /.  If there isn't one, raises NotBranchError.
 
224
        TODO: give this a new exception.
 
225
        If there is one, it is returned, along with the unused portion of path.
 
226
        """
 
227
        if path is None:
 
228
            path = os.getcwdu()
 
229
        else:
 
230
            # sanity check.
 
231
            if path.find('://') != -1:
 
232
                raise NotBranchError(path=path)
 
233
        path = os.path.abspath(path)
 
234
        tail = u''
 
235
        while True:
 
236
            try:
 
237
                return WorkingTree(path), tail
 
238
            except NotBranchError:
 
239
                pass
 
240
            if tail:
 
241
                tail = os.path.join(os.path.basename(path), tail)
 
242
            else:
 
243
                tail = os.path.basename(path)
 
244
            path = os.path.dirname(path)
 
245
            # FIXME: top in windows is indicated how ???
 
246
            if path == os.path.sep:
 
247
                # reached the root, whatever that may be
 
248
                raise NotBranchError(path=path)
 
249
 
 
250
    def __iter__(self):
 
251
        """Iterate through file_ids for this tree.
 
252
 
 
253
        file_ids are in a WorkingTree if they are in the working inventory
 
254
        and the working file exists.
 
255
        """
 
256
        inv = self._inventory
 
257
        for path, ie in inv.iter_entries():
 
258
            if bzrlib.osutils.lexists(self.abspath(path)):
 
259
                yield ie.file_id
 
260
 
 
261
    def __repr__(self):
 
262
        return "<%s of %s>" % (self.__class__.__name__,
 
263
                               getattr(self, 'basedir', None))
 
264
 
 
265
    def abspath(self, filename):
 
266
        return os.path.join(self.basedir, filename)
 
267
 
 
268
    def relpath(self, abspath):
 
269
        """Return the local path portion from a given absolute path."""
 
270
        return relpath(self.basedir, abspath)
 
271
 
 
272
    def has_filename(self, filename):
 
273
        return bzrlib.osutils.lexists(self.abspath(filename))
 
274
 
 
275
    def get_file(self, file_id):
 
276
        return self.get_file_byname(self.id2path(file_id))
 
277
 
 
278
    def get_file_byname(self, filename):
 
279
        return file(self.abspath(filename), 'rb')
 
280
 
 
281
    def get_root_id(self):
 
282
        """Return the id of this trees root"""
 
283
        inv = self.read_working_inventory()
 
284
        return inv.root.file_id
 
285
        
 
286
    def _get_store_filename(self, file_id):
 
287
        ## XXX: badly named; this is not in the store at all
 
288
        return self.abspath(self.id2path(file_id))
 
289
 
 
290
    @needs_write_lock
 
291
    def commit(self, *args, **kw):
 
292
        from bzrlib.commit import Commit
 
293
        Commit().commit(self.branch, *args, **kw)
 
294
        self._set_inventory(self.read_working_inventory())
 
295
 
 
296
    def id2abspath(self, file_id):
 
297
        return self.abspath(self.id2path(file_id))
 
298
 
 
299
    def has_id(self, file_id):
 
300
        # files that have been deleted are excluded
 
301
        inv = self._inventory
 
302
        if not inv.has_id(file_id):
 
303
            return False
 
304
        path = inv.id2path(file_id)
 
305
        return bzrlib.osutils.lexists(self.abspath(path))
 
306
 
 
307
    def has_or_had_id(self, file_id):
 
308
        if file_id == self.inventory.root.file_id:
 
309
            return True
 
310
        return self.inventory.has_id(file_id)
 
311
 
 
312
    __contains__ = has_id
 
313
 
 
314
    def get_file_size(self, file_id):
 
315
        return os.path.getsize(self.id2abspath(file_id))
 
316
 
 
317
    def get_file_sha1(self, file_id):
 
318
        path = self._inventory.id2path(file_id)
 
319
        return self._hashcache.get_sha1(path)
 
320
 
 
321
    def is_executable(self, file_id):
 
322
        if os.name == "nt":
 
323
            return self._inventory[file_id].executable
 
324
        else:
 
325
            path = self._inventory.id2path(file_id)
 
326
            mode = os.lstat(self.abspath(path)).st_mode
 
327
            return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
 
328
 
 
329
    @needs_write_lock
 
330
    def add(self, files, ids=None):
 
331
        """Make files versioned.
 
332
 
 
333
        Note that the command line normally calls smart_add instead,
 
334
        which can automatically recurse.
 
335
 
 
336
        This adds the files to the inventory, so that they will be
 
337
        recorded by the next commit.
 
338
 
 
339
        files
 
340
            List of paths to add, relative to the base of the tree.
 
341
 
 
342
        ids
 
343
            If set, use these instead of automatically generated ids.
 
344
            Must be the same length as the list of files, but may
 
345
            contain None for ids that are to be autogenerated.
 
346
 
 
347
        TODO: Perhaps have an option to add the ids even if the files do
 
348
              not (yet) exist.
 
349
 
 
350
        TODO: Perhaps callback with the ids and paths as they're added.
 
351
        """
 
352
        # TODO: Re-adding a file that is removed in the working copy
 
353
        # should probably put it back with the previous ID.
 
354
        if isinstance(files, basestring):
 
355
            assert(ids is None or isinstance(ids, basestring))
 
356
            files = [files]
 
357
            if ids is not None:
 
358
                ids = [ids]
 
359
 
 
360
        if ids is None:
 
361
            ids = [None] * len(files)
 
362
        else:
 
363
            assert(len(ids) == len(files))
 
364
 
 
365
        inv = self.read_working_inventory()
 
366
        for f,file_id in zip(files, ids):
 
367
            if is_control_file(f):
 
368
                raise BzrError("cannot add control file %s" % quotefn(f))
 
369
 
 
370
            fp = splitpath(f)
 
371
 
 
372
            if len(fp) == 0:
 
373
                raise BzrError("cannot add top-level %r" % f)
 
374
 
 
375
            fullpath = os.path.normpath(self.abspath(f))
 
376
 
 
377
            try:
 
378
                kind = file_kind(fullpath)
 
379
            except OSError:
 
380
                # maybe something better?
 
381
                raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
382
 
 
383
            if not InventoryEntry.versionable_kind(kind):
 
384
                raise BzrError('cannot add: not a versionable file ('
 
385
                               'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
386
 
 
387
            if file_id is None:
 
388
                file_id = gen_file_id(f)
 
389
            inv.add_path(f, kind=kind, file_id=file_id)
 
390
 
 
391
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
392
        self._write_inventory(inv)
 
393
 
 
394
    @needs_write_lock
 
395
    def add_pending_merge(self, *revision_ids):
 
396
        # TODO: Perhaps should check at this point that the
 
397
        # history of the revision is actually present?
 
398
        p = self.pending_merges()
 
399
        updated = False
 
400
        for rev_id in revision_ids:
 
401
            if rev_id in p:
 
402
                continue
 
403
            p.append(rev_id)
 
404
            updated = True
 
405
        if updated:
 
406
            self.set_pending_merges(p)
 
407
 
 
408
    def pending_merges(self):
 
409
        """Return a list of pending merges.
 
410
 
 
411
        These are revisions that have been merged into the working
 
412
        directory but not yet committed.
 
413
        """
 
414
        cfn = self.branch.control_files._rel_controlfilename('pending-merges')
 
415
        if not self.branch.control_files._transport.has(cfn):
 
416
            return []
 
417
        p = []
 
418
        for l in self.branch.control_files.controlfile('pending-merges', 'r').readlines():
 
419
            p.append(l.rstrip('\n'))
 
420
        return p
 
421
 
 
422
    @needs_write_lock
 
423
    def set_pending_merges(self, rev_list):
 
424
        self.branch.control_files.put_utf8('pending-merges', '\n'.join(rev_list))
 
425
 
 
426
    def get_symlink_target(self, file_id):
 
427
        return os.readlink(self.id2abspath(file_id))
 
428
 
 
429
    def file_class(self, filename):
 
430
        if self.path2id(filename):
 
431
            return 'V'
 
432
        elif self.is_ignored(filename):
 
433
            return 'I'
 
434
        else:
 
435
            return '?'
 
436
 
 
437
 
 
438
    def list_files(self):
 
439
        """Recursively list all files as (path, class, kind, id).
 
440
 
 
441
        Lists, but does not descend into unversioned directories.
 
442
 
 
443
        This does not include files that have been deleted in this
 
444
        tree.
 
445
 
 
446
        Skips the control directory.
 
447
        """
 
448
        inv = self._inventory
 
449
 
 
450
        def descend(from_dir_relpath, from_dir_id, dp):
 
451
            ls = os.listdir(dp)
 
452
            ls.sort()
 
453
            for f in ls:
 
454
                ## TODO: If we find a subdirectory with its own .bzr
 
455
                ## directory, then that is a separate tree and we
 
456
                ## should exclude it.
 
457
                if bzrlib.BZRDIR == f:
 
458
                    continue
 
459
 
 
460
                # path within tree
 
461
                fp = appendpath(from_dir_relpath, f)
 
462
 
 
463
                # absolute path
 
464
                fap = appendpath(dp, f)
 
465
                
 
466
                f_ie = inv.get_child(from_dir_id, f)
 
467
                if f_ie:
 
468
                    c = 'V'
 
469
                elif self.is_ignored(fp):
 
470
                    c = 'I'
 
471
                else:
 
472
                    c = '?'
 
473
 
 
474
                fk = file_kind(fap)
 
475
 
 
476
                if f_ie:
 
477
                    if f_ie.kind != fk:
 
478
                        raise BzrCheckError("file %r entered as kind %r id %r, "
 
479
                                            "now of kind %r"
 
480
                                            % (fap, f_ie.kind, f_ie.file_id, fk))
 
481
 
 
482
                # make a last minute entry
 
483
                if f_ie:
 
484
                    entry = f_ie
 
485
                else:
 
486
                    if fk == 'directory':
 
487
                        entry = TreeDirectory()
 
488
                    elif fk == 'file':
 
489
                        entry = TreeFile()
 
490
                    elif fk == 'symlink':
 
491
                        entry = TreeLink()
 
492
                    else:
 
493
                        entry = TreeEntry()
 
494
                
 
495
                yield fp, c, fk, (f_ie and f_ie.file_id), entry
 
496
 
 
497
                if fk != 'directory':
 
498
                    continue
 
499
 
 
500
                if c != 'V':
 
501
                    # don't descend unversioned directories
 
502
                    continue
 
503
                
 
504
                for ff in descend(fp, f_ie.file_id, fap):
 
505
                    yield ff
 
506
 
 
507
        for f in descend(u'', inv.root.file_id, self.basedir):
 
508
            yield f
 
509
 
 
510
    @needs_write_lock
 
511
    def move(self, from_paths, to_name):
 
512
        """Rename files.
 
513
 
 
514
        to_name must exist in the inventory.
 
515
 
 
516
        If to_name exists and is a directory, the files are moved into
 
517
        it, keeping their old names.  
 
518
 
 
519
        Note that to_name is only the last component of the new name;
 
520
        this doesn't change the directory.
 
521
 
 
522
        This returns a list of (from_path, to_path) pairs for each
 
523
        entry that is moved.
 
524
        """
 
525
        result = []
 
526
        ## TODO: Option to move IDs only
 
527
        assert not isinstance(from_paths, basestring)
 
528
        inv = self.inventory
 
529
        to_abs = self.abspath(to_name)
 
530
        if not isdir(to_abs):
 
531
            raise BzrError("destination %r is not a directory" % to_abs)
 
532
        if not self.has_filename(to_name):
 
533
            raise BzrError("destination %r not in working directory" % to_abs)
 
534
        to_dir_id = inv.path2id(to_name)
 
535
        if to_dir_id == None and to_name != '':
 
536
            raise BzrError("destination %r is not a versioned directory" % to_name)
 
537
        to_dir_ie = inv[to_dir_id]
 
538
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
539
            raise BzrError("destination %r is not a directory" % to_abs)
 
540
 
 
541
        to_idpath = inv.get_idpath(to_dir_id)
 
542
 
 
543
        for f in from_paths:
 
544
            if not self.has_filename(f):
 
545
                raise BzrError("%r does not exist in working tree" % f)
 
546
            f_id = inv.path2id(f)
 
547
            if f_id == None:
 
548
                raise BzrError("%r is not versioned" % f)
 
549
            name_tail = splitpath(f)[-1]
 
550
            dest_path = appendpath(to_name, name_tail)
 
551
            if self.has_filename(dest_path):
 
552
                raise BzrError("destination %r already exists" % dest_path)
 
553
            if f_id in to_idpath:
 
554
                raise BzrError("can't move %r to a subdirectory of itself" % f)
 
555
 
 
556
        # OK, so there's a race here, it's possible that someone will
 
557
        # create a file in this interval and then the rename might be
 
558
        # left half-done.  But we should have caught most problems.
 
559
        orig_inv = deepcopy(self.inventory)
 
560
        try:
 
561
            for f in from_paths:
 
562
                name_tail = splitpath(f)[-1]
 
563
                dest_path = appendpath(to_name, name_tail)
 
564
                result.append((f, dest_path))
 
565
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
566
                try:
 
567
                    rename(self.abspath(f), self.abspath(dest_path))
 
568
                except OSError, e:
 
569
                    raise BzrError("failed to rename %r to %r: %s" %
 
570
                                   (f, dest_path, e[1]),
 
571
                            ["rename rolled back"])
 
572
        except:
 
573
            # restore the inventory on error
 
574
            self._set_inventory(orig_inv)
 
575
            raise
 
576
        self._write_inventory(inv)
 
577
        return result
 
578
 
 
579
    @needs_write_lock
 
580
    def rename_one(self, from_rel, to_rel):
 
581
        """Rename one file.
 
582
 
 
583
        This can change the directory or the filename or both.
 
584
        """
 
585
        inv = self.inventory
 
586
        if not self.has_filename(from_rel):
 
587
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
588
        if self.has_filename(to_rel):
 
589
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
590
 
 
591
        file_id = inv.path2id(from_rel)
 
592
        if file_id == None:
 
593
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
594
 
 
595
        entry = inv[file_id]
 
596
        from_parent = entry.parent_id
 
597
        from_name = entry.name
 
598
        
 
599
        if inv.path2id(to_rel):
 
600
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
601
 
 
602
        to_dir, to_tail = os.path.split(to_rel)
 
603
        to_dir_id = inv.path2id(to_dir)
 
604
        if to_dir_id == None and to_dir != '':
 
605
            raise BzrError("can't determine destination directory id for %r" % to_dir)
 
606
 
 
607
        mutter("rename_one:")
 
608
        mutter("  file_id    {%s}" % file_id)
 
609
        mutter("  from_rel   %r" % from_rel)
 
610
        mutter("  to_rel     %r" % to_rel)
 
611
        mutter("  to_dir     %r" % to_dir)
 
612
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
613
 
 
614
        inv.rename(file_id, to_dir_id, to_tail)
 
615
 
 
616
        from_abs = self.abspath(from_rel)
 
617
        to_abs = self.abspath(to_rel)
 
618
        try:
 
619
            rename(from_abs, to_abs)
 
620
        except OSError, e:
 
621
            inv.rename(file_id, from_parent, from_name)
 
622
            raise BzrError("failed to rename %r to %r: %s"
 
623
                    % (from_abs, to_abs, e[1]),
 
624
                    ["rename rolled back"])
 
625
        self._write_inventory(inv)
 
626
 
 
627
    @needs_read_lock
 
628
    def unknowns(self):
 
629
        """Return all unknown files.
 
630
 
 
631
        These are files in the working directory that are not versioned or
 
632
        control files or ignored.
 
633
        
 
634
        >>> from bzrlib.branch import ScratchBranch
 
635
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
636
        >>> tree = WorkingTree(b.base, b)
 
637
        >>> map(str, tree.unknowns())
 
638
        ['foo']
 
639
        >>> tree.add('foo')
 
640
        >>> list(b.unknowns())
 
641
        []
 
642
        >>> tree.remove('foo')
 
643
        >>> list(b.unknowns())
 
644
        [u'foo']
 
645
        """
 
646
        for subp in self.extras():
 
647
            if not self.is_ignored(subp):
 
648
                yield subp
 
649
 
 
650
    def iter_conflicts(self):
 
651
        conflicted = set()
 
652
        for path in (s[0] for s in self.list_files()):
 
653
            stem = get_conflicted_stem(path)
 
654
            if stem is None:
 
655
                continue
 
656
            if stem not in conflicted:
 
657
                conflicted.add(stem)
 
658
                yield stem
 
659
 
 
660
    @needs_write_lock
 
661
    def pull(self, source, overwrite=False):
 
662
        from bzrlib.merge import merge_inner
 
663
        source.lock_read()
 
664
        try:
 
665
            old_revision_history = self.branch.revision_history()
 
666
            count = self.branch.pull(source, overwrite)
 
667
            new_revision_history = self.branch.revision_history()
 
668
            if new_revision_history != old_revision_history:
 
669
                if len(old_revision_history):
 
670
                    other_revision = old_revision_history[-1]
 
671
                else:
 
672
                    other_revision = None
 
673
                repository = self.branch.repository
 
674
                merge_inner(self.branch,
 
675
                            self.branch.basis_tree(), 
 
676
                            repository.revision_tree(other_revision))
 
677
            return count
 
678
        finally:
 
679
            source.unlock()
 
680
 
 
681
    def extras(self):
 
682
        """Yield all unknown files in this WorkingTree.
 
683
 
 
684
        If there are any unknown directories then only the directory is
 
685
        returned, not all its children.  But if there are unknown files
 
686
        under a versioned subdirectory, they are returned.
 
687
 
 
688
        Currently returned depth-first, sorted by name within directories.
 
689
        """
 
690
        ## TODO: Work from given directory downwards
 
691
        for path, dir_entry in self.inventory.directories():
 
692
            mutter("search for unknowns in %r", path)
 
693
            dirabs = self.abspath(path)
 
694
            if not isdir(dirabs):
 
695
                # e.g. directory deleted
 
696
                continue
 
697
 
 
698
            fl = []
 
699
            for subf in os.listdir(dirabs):
 
700
                if (subf != '.bzr'
 
701
                    and (subf not in dir_entry.children)):
 
702
                    fl.append(subf)
 
703
            
 
704
            fl.sort()
 
705
            for subf in fl:
 
706
                subp = appendpath(path, subf)
 
707
                yield subp
 
708
 
 
709
 
 
710
    def ignored_files(self):
 
711
        """Yield list of PATH, IGNORE_PATTERN"""
 
712
        for subp in self.extras():
 
713
            pat = self.is_ignored(subp)
 
714
            if pat != None:
 
715
                yield subp, pat
 
716
 
 
717
 
 
718
    def get_ignore_list(self):
 
719
        """Return list of ignore patterns.
 
720
 
 
721
        Cached in the Tree object after the first call.
 
722
        """
 
723
        if hasattr(self, '_ignorelist'):
 
724
            return self._ignorelist
 
725
 
 
726
        l = bzrlib.DEFAULT_IGNORE[:]
 
727
        if self.has_filename(bzrlib.IGNORE_FILENAME):
 
728
            f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
 
729
            l.extend([line.rstrip("\n\r") for line in f.readlines()])
 
730
        self._ignorelist = l
 
731
        return l
 
732
 
 
733
 
 
734
    def is_ignored(self, filename):
 
735
        r"""Check whether the filename matches an ignore pattern.
 
736
 
 
737
        Patterns containing '/' or '\' need to match the whole path;
 
738
        others match against only the last component.
 
739
 
 
740
        If the file is ignored, returns the pattern which caused it to
 
741
        be ignored, otherwise None.  So this can simply be used as a
 
742
        boolean if desired."""
 
743
 
 
744
        # TODO: Use '**' to match directories, and other extended
 
745
        # globbing stuff from cvs/rsync.
 
746
 
 
747
        # XXX: fnmatch is actually not quite what we want: it's only
 
748
        # approximately the same as real Unix fnmatch, and doesn't
 
749
        # treat dotfiles correctly and allows * to match /.
 
750
        # Eventually it should be replaced with something more
 
751
        # accurate.
 
752
        
 
753
        for pat in self.get_ignore_list():
 
754
            if '/' in pat or '\\' in pat:
 
755
                
 
756
                # as a special case, you can put ./ at the start of a
 
757
                # pattern; this is good to match in the top-level
 
758
                # only;
 
759
                
 
760
                if (pat[:2] == './') or (pat[:2] == '.\\'):
 
761
                    newpat = pat[2:]
 
762
                else:
 
763
                    newpat = pat
 
764
                if fnmatch.fnmatchcase(filename, newpat):
 
765
                    return pat
 
766
            else:
 
767
                if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
 
768
                    return pat
 
769
        else:
 
770
            return None
 
771
 
 
772
    def kind(self, file_id):
 
773
        return file_kind(self.id2abspath(file_id))
 
774
 
 
775
    def lock_read(self):
 
776
        """See Branch.lock_read, and WorkingTree.unlock."""
 
777
        return self.branch.lock_read()
 
778
 
 
779
    def lock_write(self):
 
780
        """See Branch.lock_write, and WorkingTree.unlock."""
 
781
        return self.branch.lock_write()
 
782
 
 
783
    def _basis_inventory_name(self, revision_id):
 
784
        return 'basis-inventory.%s' % revision_id
 
785
 
 
786
    def set_last_revision(self, new_revision, old_revision=None):
 
787
        if old_revision:
 
788
            try:
 
789
                path = self._basis_inventory_name(old_revision)
 
790
                path = self.branch._rel_controlfilename(path)
 
791
                self.branch._transport.delete(path)
 
792
            except:
 
793
                pass
 
794
        try:
 
795
            xml = self.branch.repository.get_inventory_xml(new_revision)
 
796
            path = self._basis_inventory_name(new_revision)
 
797
            self.branch.control_files.put_utf8(path, xml)
 
798
        except WeaveRevisionNotPresent:
 
799
            pass
 
800
 
 
801
    def read_basis_inventory(self, revision_id):
 
802
        """Read the cached basis inventory."""
 
803
        path = self._basis_inventory_name(revision_id)
 
804
        return self.branch.control_files.controlfile(path, 'r').read()
 
805
        
 
806
    @needs_read_lock
 
807
    def read_working_inventory(self):
 
808
        """Read the working inventory."""
 
809
        # ElementTree does its own conversion from UTF-8, so open in
 
810
        # binary.
 
811
        f = self.branch.control_files.controlfile('inventory', 'rb')
 
812
        return bzrlib.xml5.serializer_v5.read_inventory(f)
 
813
 
 
814
    @needs_write_lock
 
815
    def remove(self, files, verbose=False):
 
816
        """Remove nominated files from the working inventory..
 
817
 
 
818
        This does not remove their text.  This does not run on XXX on what? RBC
 
819
 
 
820
        TODO: Refuse to remove modified files unless --force is given?
 
821
 
 
822
        TODO: Do something useful with directories.
 
823
 
 
824
        TODO: Should this remove the text or not?  Tough call; not
 
825
        removing may be useful and the user can just use use rm, and
 
826
        is the opposite of add.  Removing it is consistent with most
 
827
        other tools.  Maybe an option.
 
828
        """
 
829
        ## TODO: Normalize names
 
830
        ## TODO: Remove nested loops; better scalability
 
831
        if isinstance(files, basestring):
 
832
            files = [files]
 
833
 
 
834
        inv = self.inventory
 
835
 
 
836
        # do this before any modifications
 
837
        for f in files:
 
838
            fid = inv.path2id(f)
 
839
            if not fid:
 
840
                # TODO: Perhaps make this just a warning, and continue?
 
841
                # This tends to happen when 
 
842
                raise NotVersionedError(path=f)
 
843
            mutter("remove inventory entry %s {%s}", quotefn(f), fid)
 
844
            if verbose:
 
845
                # having remove it, it must be either ignored or unknown
 
846
                if self.is_ignored(f):
 
847
                    new_status = 'I'
 
848
                else:
 
849
                    new_status = '?'
 
850
                show_status(new_status, inv[fid].kind, quotefn(f))
 
851
            del inv[fid]
 
852
 
 
853
        self._write_inventory(inv)
 
854
 
 
855
    @needs_write_lock
 
856
    def revert(self, filenames, old_tree=None, backups=True):
 
857
        from bzrlib.merge import merge_inner
 
858
        if old_tree is None:
 
859
            old_tree = self.branch.basis_tree()
 
860
        merge_inner(self.branch, old_tree,
 
861
                    self, ignore_zero=True,
 
862
                    backup_files=backups, 
 
863
                    interesting_files=filenames)
 
864
        if not len(filenames):
 
865
            self.set_pending_merges([])
 
866
 
 
867
    @needs_write_lock
 
868
    def set_inventory(self, new_inventory_list):
 
869
        from bzrlib.inventory import (Inventory,
 
870
                                      InventoryDirectory,
 
871
                                      InventoryEntry,
 
872
                                      InventoryFile,
 
873
                                      InventoryLink)
 
874
        inv = Inventory(self.get_root_id())
 
875
        for path, file_id, parent, kind in new_inventory_list:
 
876
            name = os.path.basename(path)
 
877
            if name == "":
 
878
                continue
 
879
            # fixme, there should be a factory function inv,add_?? 
 
880
            if kind == 'directory':
 
881
                inv.add(InventoryDirectory(file_id, name, parent))
 
882
            elif kind == 'file':
 
883
                inv.add(InventoryFile(file_id, name, parent))
 
884
            elif kind == 'symlink':
 
885
                inv.add(InventoryLink(file_id, name, parent))
 
886
            else:
 
887
                raise BzrError("unknown kind %r" % kind)
 
888
        self._write_inventory(inv)
 
889
 
 
890
    @needs_write_lock
 
891
    def set_root_id(self, file_id):
 
892
        """Set the root id for this tree."""
 
893
        inv = self.read_working_inventory()
 
894
        orig_root_id = inv.root.file_id
 
895
        del inv._byid[inv.root.file_id]
 
896
        inv.root.file_id = file_id
 
897
        inv._byid[inv.root.file_id] = inv.root
 
898
        for fid in inv:
 
899
            entry = inv[fid]
 
900
            if entry.parent_id in (None, orig_root_id):
 
901
                entry.parent_id = inv.root.file_id
 
902
        self._write_inventory(inv)
 
903
 
 
904
    def unlock(self):
 
905
        """See Branch.unlock.
 
906
        
 
907
        WorkingTree locking just uses the Branch locking facilities.
 
908
        This is current because all working trees have an embedded branch
 
909
        within them. IF in the future, we were to make branch data shareable
 
910
        between multiple working trees, i.e. via shared storage, then we 
 
911
        would probably want to lock both the local tree, and the branch.
 
912
        """
 
913
        return self.branch.unlock()
 
914
 
 
915
    @needs_write_lock
 
916
    def _write_inventory(self, inv):
 
917
        """Write inventory as the current inventory."""
 
918
        from cStringIO import StringIO
 
919
        from bzrlib.atomicfile import AtomicFile
 
920
        sio = StringIO()
 
921
        bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
922
        sio.seek(0)
 
923
        f = AtomicFile(self.branch.control_files.controlfilename('inventory'))
 
924
        try:
 
925
            pumpfile(sio, f)
 
926
            f.commit()
 
927
        finally:
 
928
            f.close()
 
929
        self._set_inventory(inv)
 
930
        mutter('wrote working inventory')
 
931
            
 
932
 
 
933
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
934
def get_conflicted_stem(path):
 
935
    for suffix in CONFLICT_SUFFIXES:
 
936
        if path.endswith(suffix):
 
937
            return path[:-len(suffix)]