~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/workingtree.py

  • Committer: Martin Pool
  • Date: 2005-05-17 06:56:16 UTC
  • Revision ID: mbp@sourcefrog.net-20050517065616-6f23381d6184a8aa
- add space for un-merged patches

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
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 WorkingTree(dir[, branch])
29
 
"""
30
 
 
31
 
 
32
 
# FIXME: I don't know if writing out the cache from the destructor is really a
33
 
# good idea, because destructors are considered poor taste in Python, and it's
34
 
# not predictable when it will be written out.
35
 
 
36
 
# TODO: Give the workingtree sole responsibility for the working inventory;
37
 
# remove the variable and references to it from the branch.  This may require
38
 
# updating the commit code so as to update the inventory within the working
39
 
# copy, and making sure there's only one WorkingTree for any directory on disk.
40
 
# At the momenthey may alias the inventory and have old copies of it in memory.
41
 
 
42
 
from copy import deepcopy
43
 
from cStringIO import StringIO
44
 
import errno
45
 
import fnmatch
 
17
 
46
18
import os
47
 
import stat
48
 
 
49
 
 
50
 
from bzrlib.atomicfile import AtomicFile
51
 
from bzrlib.branch import (Branch,
52
 
                           BzrBranchFormat4,
53
 
                           BzrBranchFormat5,
54
 
                           BzrBranchFormat6,
55
 
                           is_control_file,
56
 
                           quotefn)
57
 
from bzrlib.decorators import needs_read_lock, needs_write_lock
58
 
from bzrlib.errors import (BzrCheckError,
59
 
                           BzrError,
60
 
                           DivergedBranches,
61
 
                           WeaveRevisionNotPresent,
62
 
                           NotBranchError,
63
 
                           NoSuchFile,
64
 
                           NotVersionedError)
65
 
from bzrlib.inventory import InventoryEntry
66
 
from bzrlib.lockable_files import LockableFiles
67
 
from bzrlib.osutils import (appendpath,
68
 
                            compact_date,
69
 
                            file_kind,
70
 
                            isdir,
71
 
                            getcwd,
72
 
                            pathjoin,
73
 
                            pumpfile,
74
 
                            safe_unicode,
75
 
                            splitpath,
76
 
                            rand_bytes,
77
 
                            abspath,
78
 
                            normpath,
79
 
                            realpath,
80
 
                            relpath,
81
 
                            rename)
82
 
from bzrlib.symbol_versioning import *
83
 
from bzrlib.textui import show_status
 
19
    
84
20
import bzrlib.tree
85
 
from bzrlib.trace import mutter
86
 
from bzrlib.transport import get_transport
87
 
import bzrlib.xml5
88
 
 
89
 
 
90
 
def gen_file_id(name):
91
 
    """Return new file id.
92
 
 
93
 
    This should probably generate proper UUIDs, but for the moment we
94
 
    cope with just randomness because running uuidgen every time is
95
 
    slow."""
96
 
    import re
97
 
    from binascii import hexlify
98
 
    from time import time
99
 
 
100
 
    # get last component
101
 
    idx = name.rfind('/')
102
 
    if idx != -1:
103
 
        name = name[idx+1 : ]
104
 
    idx = name.rfind('\\')
105
 
    if idx != -1:
106
 
        name = name[idx+1 : ]
107
 
 
108
 
    # make it not a hidden file
109
 
    name = name.lstrip('.')
110
 
 
111
 
    # remove any wierd characters; we don't escape them but rather
112
 
    # just pull them out
113
 
    name = re.sub(r'[^\w.]', '', name)
114
 
 
115
 
    s = hexlify(rand_bytes(8))
116
 
    return '-'.join((name, compact_date(time()), s))
117
 
 
118
 
 
119
 
def gen_root_id():
120
 
    """Return a new tree-root file id."""
121
 
    return gen_file_id('TREE_ROOT')
122
 
 
123
 
 
124
 
class TreeEntry(object):
125
 
    """An entry that implements the minium interface used by commands.
126
 
 
127
 
    This needs further inspection, it may be better to have 
128
 
    InventoryEntries without ids - though that seems wrong. For now,
129
 
    this is a parallel hierarchy to InventoryEntry, and needs to become
130
 
    one of several things: decorates to that hierarchy, children of, or
131
 
    parents of it.
132
 
    Another note is that these objects are currently only used when there is
133
 
    no InventoryEntry available - i.e. for unversioned objects.
134
 
    Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
135
 
    """
136
 
 
137
 
    def __eq__(self, other):
138
 
        # yes, this us ugly, TODO: best practice __eq__ style.
139
 
        return (isinstance(other, TreeEntry)
140
 
                and other.__class__ == self.__class__)
141
 
 
142
 
    def kind_character(self):
143
 
        return "???"
144
 
 
145
 
 
146
 
class TreeDirectory(TreeEntry):
147
 
    """See TreeEntry. This is a directory in a working tree."""
148
 
 
149
 
    def __eq__(self, other):
150
 
        return (isinstance(other, TreeDirectory)
151
 
                and other.__class__ == self.__class__)
152
 
 
153
 
    def kind_character(self):
154
 
        return "/"
155
 
 
156
 
 
157
 
class TreeFile(TreeEntry):
158
 
    """See TreeEntry. This is a regular file in a working tree."""
159
 
 
160
 
    def __eq__(self, other):
161
 
        return (isinstance(other, TreeFile)
162
 
                and other.__class__ == self.__class__)
163
 
 
164
 
    def kind_character(self):
165
 
        return ''
166
 
 
167
 
 
168
 
class TreeLink(TreeEntry):
169
 
    """See TreeEntry. This is a symlink in a working tree."""
170
 
 
171
 
    def __eq__(self, other):
172
 
        return (isinstance(other, TreeLink)
173
 
                and other.__class__ == self.__class__)
174
 
 
175
 
    def kind_character(self):
176
 
        return ''
177
 
 
 
21
from errors import BzrCheckError
 
22
from trace import mutter
 
23
import statcache
178
24
 
179
25
class WorkingTree(bzrlib.tree.Tree):
180
26
    """Working copy tree.
185
31
    It is possible for a `WorkingTree` to have a filename which is
186
32
    not listed in the Inventory and vice versa.
187
33
    """
188
 
 
189
 
    def __init__(self, basedir='.', branch=None, _inventory=None, _control_files=None):
190
 
        """Construct a WorkingTree for basedir.
191
 
 
192
 
        If the branch is not supplied, it is opened automatically.
193
 
        If the branch is supplied, it must be the branch for this basedir.
194
 
        (branch.base is not cross checked, because for remote branches that
195
 
        would be meaningless).
196
 
        """
197
 
        from bzrlib.hashcache import HashCache
198
 
        from bzrlib.trace import note, mutter
199
 
        assert isinstance(basedir, basestring), \
200
 
            "base directory %r is not a string" % basedir
201
 
        basedir = safe_unicode(basedir)
202
 
        mutter("openeing working tree %r", basedir)
203
 
        if branch is None:
204
 
            branch = Branch.open(basedir)
205
 
        assert isinstance(branch, Branch), \
206
 
            "branch %r is not a Branch" % branch
207
 
        self.branch = branch
208
 
        self.basedir = realpath(basedir)
209
 
        # if branch is at our basedir and is a format 6 or less
210
 
        if (isinstance(self.branch._branch_format,
211
 
                       (BzrBranchFormat4, BzrBranchFormat5, BzrBranchFormat6))
212
 
            # might be able to share control object
213
 
            and self.branch.base.split('/')[-2] == self.basedir.split('/')[-1]):
214
 
            self._control_files = self.branch.control_files
215
 
        elif _control_files is not None:
216
 
            assert False, "not done yet"
217
 
#            self._control_files = _control_files
218
 
        else:
219
 
            self._control_files = LockableFiles(
220
 
                get_transport(self.basedir).clone(bzrlib.BZRDIR), 'branch-lock')
221
 
 
222
 
        # update the whole cache up front and write to disk if anything changed;
223
 
        # in the future we might want to do this more selectively
224
 
        # two possible ways offer themselves : in self._unlock, write the cache
225
 
        # if needed, or, when the cache sees a change, append it to the hash
226
 
        # cache file, and have the parser take the most recent entry for a
227
 
        # given path only.
228
 
        hc = self._hashcache = HashCache(basedir)
229
 
        hc.read()
230
 
        hc.scan()
231
 
 
232
 
        if hc.needs_write:
233
 
            mutter("write hc")
234
 
            hc.write()
235
 
 
236
 
        if _inventory is None:
237
 
            self._set_inventory(self.read_working_inventory())
238
 
        else:
239
 
            self._set_inventory(_inventory)
240
 
 
241
 
    def _set_inventory(self, inv):
 
34
    _statcache = None
 
35
    
 
36
    def __init__(self, basedir, inv):
242
37
        self._inventory = inv
243
 
        self.path2id = self._inventory.path2id
244
 
 
245
 
    @staticmethod
246
 
    def open_containing(path=None):
247
 
        """Open an existing working tree which has its root about path.
248
 
        
249
 
        This probes for a working tree at path and searches upwards from there.
250
 
 
251
 
        Basically we keep looking up until we find the control directory or
252
 
        run into /.  If there isn't one, raises NotBranchError.
253
 
        TODO: give this a new exception.
254
 
        If there is one, it is returned, along with the unused portion of path.
255
 
        """
256
 
        if path is None:
257
 
            path = getcwd()
258
 
        else:
259
 
            # sanity check.
260
 
            if path.find('://') != -1:
261
 
                raise NotBranchError(path=path)
262
 
        path = abspath(path)
263
 
        orig_path = path[:]
264
 
        tail = u''
265
 
        while True:
266
 
            try:
267
 
                return WorkingTree(path), tail
268
 
            except NotBranchError:
269
 
                pass
270
 
            if tail:
271
 
                tail = pathjoin(os.path.basename(path), tail)
272
 
            else:
273
 
                tail = os.path.basename(path)
274
 
            lastpath = path
275
 
            path = os.path.dirname(path)
276
 
            if lastpath == path:
277
 
                # reached the root, whatever that may be
278
 
                raise NotBranchError(path=orig_path)
 
38
        self.basedir = basedir
 
39
        self.path2id = inv.path2id
 
40
        self._update_statcache()
279
41
 
280
42
    def __iter__(self):
281
43
        """Iterate through file_ids for this tree.
284
46
        and the working file exists.
285
47
        """
286
48
        inv = self._inventory
287
 
        for path, ie in inv.iter_entries():
288
 
            if bzrlib.osutils.lexists(self.abspath(path)):
289
 
                yield ie.file_id
 
49
        for file_id in self._inventory:
 
50
            # TODO: This is slightly redundant; we should be able to just
 
51
            # check the statcache but it only includes regular files.
 
52
            # only include files which still exist on disk
 
53
            ie = inv[file_id]
 
54
            if ie.kind == 'file':
 
55
                if ((file_id in self._statcache)
 
56
                    or (os.path.exists(self.abspath(inv.id2path(file_id))))):
 
57
                    yield file_id
 
58
 
 
59
 
290
60
 
291
61
    def __repr__(self):
292
62
        return "<%s of %s>" % (self.__class__.__name__,
293
 
                               getattr(self, 'basedir', None))
 
63
                               self.basedir)
294
64
 
295
65
    def abspath(self, filename):
296
 
        return pathjoin(self.basedir, filename)
297
 
 
298
 
    @staticmethod
299
 
    def create(branch, directory):
300
 
        """Create a workingtree for branch at directory.
301
 
 
302
 
        If existing_directory already exists it must have a .bzr directory.
303
 
        If it does not exist, it will be created.
304
 
 
305
 
        This returns a new WorkingTree object for the new checkout.
306
 
 
307
 
        TODO FIXME RBC 20060124 when we have checkout formats in place this
308
 
        should accept an optional revisionid to checkout [and reject this if
309
 
        checking out into the same dir as a pre-checkout-aware branch format.]
310
 
 
311
 
        XXX: When BzrDir is present, these should be created through that 
312
 
        interface instead.
313
 
        """
314
 
        try:
315
 
            os.mkdir(directory)
316
 
        except OSError, e:
317
 
            if e.errno != errno.EEXIST:
318
 
                raise
319
 
        try:
320
 
            os.mkdir(pathjoin(directory, '.bzr'))
321
 
        except OSError, e:
322
 
            if e.errno != errno.EEXIST:
323
 
                raise
324
 
        inv = branch.repository.revision_tree(branch.last_revision()).inventory
325
 
        wt = WorkingTree(directory, branch, inv)
326
 
        wt._write_inventory(inv)
327
 
        if branch.last_revision() is not None:
328
 
            wt.set_last_revision(branch.last_revision())
329
 
        wt.set_pending_merges([])
330
 
        wt.revert([])
331
 
        return wt
332
 
 
333
 
    @staticmethod
334
 
    def create_standalone(directory):
335
 
        """Create a checkout and a branch at directory.
336
 
 
337
 
        Directory must exist and be empty.
338
 
 
339
 
        XXX: When BzrDir is present, these should be created through that 
340
 
        interface instead.
341
 
        """
342
 
        directory = safe_unicode(directory)
343
 
        b = Branch.create(directory)
344
 
        return WorkingTree.create(b, directory)
345
 
 
346
 
    def relpath(self, abs):
347
 
        """Return the local path portion from a given absolute path."""
348
 
        return relpath(self.basedir, abs)
 
66
        return os.path.join(self.basedir, filename)
349
67
 
350
68
    def has_filename(self, filename):
351
 
        return bzrlib.osutils.lexists(self.abspath(filename))
 
69
        return os.path.exists(self.abspath(filename))
352
70
 
353
71
    def get_file(self, file_id):
354
72
        return self.get_file_byname(self.id2path(file_id))
356
74
    def get_file_byname(self, filename):
357
75
        return file(self.abspath(filename), 'rb')
358
76
 
359
 
    def get_root_id(self):
360
 
        """Return the id of this trees root"""
361
 
        inv = self.read_working_inventory()
362
 
        return inv.root.file_id
363
 
        
364
77
    def _get_store_filename(self, file_id):
365
 
        ## XXX: badly named; this is not in the store at all
366
 
        return self.abspath(self.id2path(file_id))
367
 
 
368
 
    @needs_write_lock
369
 
    def commit(self, *args, **kwargs):
370
 
        from bzrlib.commit import Commit
371
 
        # args for wt.commit start at message from the Commit.commit method,
372
 
        # but with branch a kwarg now, passing in args as is results in the
373
 
        #message being used for the branch
374
 
        args = (DEPRECATED_PARAMETER, ) + args
375
 
        Commit().commit(working_tree=self, *args, **kwargs)
376
 
        self._set_inventory(self.read_working_inventory())
377
 
 
378
 
    def id2abspath(self, file_id):
379
 
        return self.abspath(self.id2path(file_id))
380
 
 
 
78
        ## XXX: badly named; this isn't in the store at all
 
79
        return self.abspath(self.id2path(file_id))
 
80
 
 
81
                
381
82
    def has_id(self, file_id):
382
83
        # files that have been deleted are excluded
383
 
        inv = self._inventory
384
 
        if not inv.has_id(file_id):
 
84
        if not self.inventory.has_id(file_id):
385
85
            return False
386
 
        path = inv.id2path(file_id)
387
 
        return bzrlib.osutils.lexists(self.abspath(path))
388
 
 
389
 
    def has_or_had_id(self, file_id):
390
 
        if file_id == self.inventory.root.file_id:
 
86
        if file_id in self._statcache:
391
87
            return True
392
 
        return self.inventory.has_id(file_id)
 
88
        return os.path.exists(self.abspath(self.id2path(file_id)))
 
89
 
393
90
 
394
91
    __contains__ = has_id
 
92
    
 
93
 
 
94
    def _update_statcache(self):
 
95
        import statcache
 
96
        if not self._statcache:
 
97
            self._statcache = statcache.update_cache(self.basedir, self.inventory)
395
98
 
396
99
    def get_file_size(self, file_id):
397
 
        return os.path.getsize(self.id2abspath(file_id))
398
 
 
399
 
    @needs_read_lock
 
100
        import os, stat
 
101
        return os.stat(self._get_store_filename(file_id))[stat.ST_SIZE]
 
102
 
 
103
 
400
104
    def get_file_sha1(self, file_id):
401
 
        path = self._inventory.id2path(file_id)
402
 
        return self._hashcache.get_sha1(path)
403
 
 
404
 
    def is_executable(self, file_id):
405
 
        if os.name == "nt":
406
 
            return self._inventory[file_id].executable
407
 
        else:
408
 
            path = self._inventory.id2path(file_id)
409
 
            mode = os.lstat(self.abspath(path)).st_mode
410
 
            return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
411
 
 
412
 
    @needs_write_lock
413
 
    def add(self, files, ids=None):
414
 
        """Make files versioned.
415
 
 
416
 
        Note that the command line normally calls smart_add instead,
417
 
        which can automatically recurse.
418
 
 
419
 
        This adds the files to the inventory, so that they will be
420
 
        recorded by the next commit.
421
 
 
422
 
        files
423
 
            List of paths to add, relative to the base of the tree.
424
 
 
425
 
        ids
426
 
            If set, use these instead of automatically generated ids.
427
 
            Must be the same length as the list of files, but may
428
 
            contain None for ids that are to be autogenerated.
429
 
 
430
 
        TODO: Perhaps have an option to add the ids even if the files do
431
 
              not (yet) exist.
432
 
 
433
 
        TODO: Perhaps callback with the ids and paths as they're added.
434
 
        """
435
 
        # TODO: Re-adding a file that is removed in the working copy
436
 
        # should probably put it back with the previous ID.
437
 
        if isinstance(files, basestring):
438
 
            assert(ids is None or isinstance(ids, basestring))
439
 
            files = [files]
440
 
            if ids is not None:
441
 
                ids = [ids]
442
 
 
443
 
        if ids is None:
444
 
            ids = [None] * len(files)
445
 
        else:
446
 
            assert(len(ids) == len(files))
447
 
 
448
 
        inv = self.read_working_inventory()
449
 
        for f,file_id in zip(files, ids):
450
 
            if is_control_file(f):
451
 
                raise BzrError("cannot add control file %s" % quotefn(f))
452
 
 
453
 
            fp = splitpath(f)
454
 
 
455
 
            if len(fp) == 0:
456
 
                raise BzrError("cannot add top-level %r" % f)
457
 
 
458
 
            fullpath = normpath(self.abspath(f))
459
 
 
460
 
            try:
461
 
                kind = file_kind(fullpath)
462
 
            except OSError:
463
 
                # maybe something better?
464
 
                raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
465
 
 
466
 
            if not InventoryEntry.versionable_kind(kind):
467
 
                raise BzrError('cannot add: not a versionable file ('
468
 
                               'i.e. regular file, symlink or directory): %s' % quotefn(f))
469
 
 
470
 
            if file_id is None:
471
 
                file_id = gen_file_id(f)
472
 
            inv.add_path(f, kind=kind, file_id=file_id)
473
 
 
474
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
475
 
        self._write_inventory(inv)
476
 
 
477
 
    @needs_write_lock
478
 
    def add_pending_merge(self, *revision_ids):
479
 
        # TODO: Perhaps should check at this point that the
480
 
        # history of the revision is actually present?
481
 
        p = self.pending_merges()
482
 
        updated = False
483
 
        for rev_id in revision_ids:
484
 
            if rev_id in p:
485
 
                continue
486
 
            p.append(rev_id)
487
 
            updated = True
488
 
        if updated:
489
 
            self.set_pending_merges(p)
490
 
 
491
 
    @needs_read_lock
492
 
    def pending_merges(self):
493
 
        """Return a list of pending merges.
494
 
 
495
 
        These are revisions that have been merged into the working
496
 
        directory but not yet committed.
497
 
        """
498
 
        try:
499
 
            merges_file = self._control_files.get_utf8('pending-merges')
500
 
        except OSError, e:
501
 
            if e.errno != errno.ENOENT:
502
 
                raise
503
 
            return []
504
 
        p = []
505
 
        for l in merges_file.readlines():
506
 
            p.append(l.rstrip('\n'))
507
 
        return p
508
 
 
509
 
    @needs_write_lock
510
 
    def set_pending_merges(self, rev_list):
511
 
        self._control_files.put_utf8('pending-merges', '\n'.join(rev_list))
512
 
 
513
 
    def get_symlink_target(self, file_id):
514
 
        return os.readlink(self.id2abspath(file_id))
 
105
        return self._statcache[file_id][statcache.SC_SHA1]
 
106
 
515
107
 
516
108
    def file_class(self, filename):
517
109
        if self.path2id(filename):
532
124
 
533
125
        Skips the control directory.
534
126
        """
535
 
        inv = self._inventory
 
127
        from osutils import appendpath, file_kind
 
128
        import os
 
129
 
 
130
        inv = self.inventory
536
131
 
537
132
        def descend(from_dir_relpath, from_dir_id, dp):
538
133
            ls = os.listdir(dp)
566
161
                                            "now of kind %r"
567
162
                                            % (fap, f_ie.kind, f_ie.file_id, fk))
568
163
 
569
 
                # make a last minute entry
570
 
                if f_ie:
571
 
                    entry = f_ie
572
 
                else:
573
 
                    if fk == 'directory':
574
 
                        entry = TreeDirectory()
575
 
                    elif fk == 'file':
576
 
                        entry = TreeFile()
577
 
                    elif fk == 'symlink':
578
 
                        entry = TreeLink()
579
 
                    else:
580
 
                        entry = TreeEntry()
581
 
                
582
 
                yield fp, c, fk, (f_ie and f_ie.file_id), entry
 
164
                yield fp, c, fk, (f_ie and f_ie.file_id)
583
165
 
584
166
                if fk != 'directory':
585
167
                    continue
591
173
                for ff in descend(fp, f_ie.file_id, fap):
592
174
                    yield ff
593
175
 
594
 
        for f in descend(u'', inv.root.file_id, self.basedir):
 
176
        for f in descend('', inv.root.file_id, self.basedir):
595
177
            yield f
596
 
 
597
 
    @needs_write_lock
598
 
    def move(self, from_paths, to_name):
599
 
        """Rename files.
600
 
 
601
 
        to_name must exist in the inventory.
602
 
 
603
 
        If to_name exists and is a directory, the files are moved into
604
 
        it, keeping their old names.  
605
 
 
606
 
        Note that to_name is only the last component of the new name;
607
 
        this doesn't change the directory.
608
 
 
609
 
        This returns a list of (from_path, to_path) pairs for each
610
 
        entry that is moved.
611
 
        """
612
 
        result = []
613
 
        ## TODO: Option to move IDs only
614
 
        assert not isinstance(from_paths, basestring)
615
 
        inv = self.inventory
616
 
        to_abs = self.abspath(to_name)
617
 
        if not isdir(to_abs):
618
 
            raise BzrError("destination %r is not a directory" % to_abs)
619
 
        if not self.has_filename(to_name):
620
 
            raise BzrError("destination %r not in working directory" % to_abs)
621
 
        to_dir_id = inv.path2id(to_name)
622
 
        if to_dir_id == None and to_name != '':
623
 
            raise BzrError("destination %r is not a versioned directory" % to_name)
624
 
        to_dir_ie = inv[to_dir_id]
625
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
626
 
            raise BzrError("destination %r is not a directory" % to_abs)
627
 
 
628
 
        to_idpath = inv.get_idpath(to_dir_id)
629
 
 
630
 
        for f in from_paths:
631
 
            if not self.has_filename(f):
632
 
                raise BzrError("%r does not exist in working tree" % f)
633
 
            f_id = inv.path2id(f)
634
 
            if f_id == None:
635
 
                raise BzrError("%r is not versioned" % f)
636
 
            name_tail = splitpath(f)[-1]
637
 
            dest_path = appendpath(to_name, name_tail)
638
 
            if self.has_filename(dest_path):
639
 
                raise BzrError("destination %r already exists" % dest_path)
640
 
            if f_id in to_idpath:
641
 
                raise BzrError("can't move %r to a subdirectory of itself" % f)
642
 
 
643
 
        # OK, so there's a race here, it's possible that someone will
644
 
        # create a file in this interval and then the rename might be
645
 
        # left half-done.  But we should have caught most problems.
646
 
        orig_inv = deepcopy(self.inventory)
647
 
        try:
648
 
            for f in from_paths:
649
 
                name_tail = splitpath(f)[-1]
650
 
                dest_path = appendpath(to_name, name_tail)
651
 
                result.append((f, dest_path))
652
 
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
653
 
                try:
654
 
                    rename(self.abspath(f), self.abspath(dest_path))
655
 
                except OSError, e:
656
 
                    raise BzrError("failed to rename %r to %r: %s" %
657
 
                                   (f, dest_path, e[1]),
658
 
                            ["rename rolled back"])
659
 
        except:
660
 
            # restore the inventory on error
661
 
            self._set_inventory(orig_inv)
662
 
            raise
663
 
        self._write_inventory(inv)
664
 
        return result
665
 
 
666
 
    @needs_write_lock
667
 
    def rename_one(self, from_rel, to_rel):
668
 
        """Rename one file.
669
 
 
670
 
        This can change the directory or the filename or both.
671
 
        """
672
 
        inv = self.inventory
673
 
        if not self.has_filename(from_rel):
674
 
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
675
 
        if self.has_filename(to_rel):
676
 
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
677
 
 
678
 
        file_id = inv.path2id(from_rel)
679
 
        if file_id == None:
680
 
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
681
 
 
682
 
        entry = inv[file_id]
683
 
        from_parent = entry.parent_id
684
 
        from_name = entry.name
685
 
        
686
 
        if inv.path2id(to_rel):
687
 
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
688
 
 
689
 
        to_dir, to_tail = os.path.split(to_rel)
690
 
        to_dir_id = inv.path2id(to_dir)
691
 
        if to_dir_id == None and to_dir != '':
692
 
            raise BzrError("can't determine destination directory id for %r" % to_dir)
693
 
 
694
 
        mutter("rename_one:")
695
 
        mutter("  file_id    {%s}" % file_id)
696
 
        mutter("  from_rel   %r" % from_rel)
697
 
        mutter("  to_rel     %r" % to_rel)
698
 
        mutter("  to_dir     %r" % to_dir)
699
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
700
 
 
701
 
        inv.rename(file_id, to_dir_id, to_tail)
702
 
 
703
 
        from_abs = self.abspath(from_rel)
704
 
        to_abs = self.abspath(to_rel)
705
 
        try:
706
 
            rename(from_abs, to_abs)
707
 
        except OSError, e:
708
 
            inv.rename(file_id, from_parent, from_name)
709
 
            raise BzrError("failed to rename %r to %r: %s"
710
 
                    % (from_abs, to_abs, e[1]),
711
 
                    ["rename rolled back"])
712
 
        self._write_inventory(inv)
713
 
 
714
 
    @needs_read_lock
 
178
            
 
179
 
 
180
 
715
181
    def unknowns(self):
716
 
        """Return all unknown files.
717
 
 
718
 
        These are files in the working directory that are not versioned or
719
 
        control files or ignored.
720
 
        
721
 
        >>> from bzrlib.branch import ScratchBranch
722
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
723
 
        >>> tree = WorkingTree(b.base, b)
724
 
        >>> map(str, tree.unknowns())
725
 
        ['foo']
726
 
        >>> tree.add('foo')
727
 
        >>> list(b.unknowns())
728
 
        []
729
 
        >>> tree.remove('foo')
730
 
        >>> list(b.unknowns())
731
 
        [u'foo']
732
 
        """
733
182
        for subp in self.extras():
734
183
            if not self.is_ignored(subp):
735
184
                yield subp
736
185
 
737
 
    def iter_conflicts(self):
738
 
        conflicted = set()
739
 
        for path in (s[0] for s in self.list_files()):
740
 
            stem = get_conflicted_stem(path)
741
 
            if stem is None:
742
 
                continue
743
 
            if stem not in conflicted:
744
 
                conflicted.add(stem)
745
 
                yield stem
746
 
 
747
 
    @needs_write_lock
748
 
    def pull(self, source, overwrite=False, stop_revision=None):
749
 
        from bzrlib.merge import merge_inner
750
 
        source.lock_read()
751
 
        try:
752
 
            old_revision_history = self.branch.revision_history()
753
 
            count = self.branch.pull(source, overwrite,stop_revision)
754
 
            new_revision_history = self.branch.revision_history()
755
 
            if new_revision_history != old_revision_history:
756
 
                if len(old_revision_history):
757
 
                    other_revision = old_revision_history[-1]
758
 
                else:
759
 
                    other_revision = None
760
 
                repository = self.branch.repository
761
 
                merge_inner(self.branch,
762
 
                            self.branch.basis_tree(), 
763
 
                            repository.revision_tree(other_revision),
764
 
                            this_tree=self)
765
 
                self.set_last_revision(self.branch.last_revision())
766
 
            return count
767
 
        finally:
768
 
            source.unlock()
769
186
 
770
187
    def extras(self):
771
188
        """Yield all unknown files in this WorkingTree.
777
194
        Currently returned depth-first, sorted by name within directories.
778
195
        """
779
196
        ## TODO: Work from given directory downwards
 
197
        from osutils import isdir, appendpath
 
198
        
780
199
        for path, dir_entry in self.inventory.directories():
781
 
            mutter("search for unknowns in %r", path)
 
200
            mutter("search for unknowns in %r" % path)
782
201
            dirabs = self.abspath(path)
783
202
            if not isdir(dirabs):
784
203
                # e.g. directory deleted
839
258
        # Eventually it should be replaced with something more
840
259
        # accurate.
841
260
        
 
261
        import fnmatch
 
262
        from osutils import splitpath
 
263
        
842
264
        for pat in self.get_ignore_list():
843
265
            if '/' in pat or '\\' in pat:
844
266
                
857
279
                    return pat
858
280
        else:
859
281
            return None
860
 
 
861
 
    def kind(self, file_id):
862
 
        return file_kind(self.id2abspath(file_id))
863
 
 
864
 
    def lock_read(self):
865
 
        """See Branch.lock_read, and WorkingTree.unlock."""
866
 
        return self.branch.lock_read()
867
 
 
868
 
    def lock_write(self):
869
 
        """See Branch.lock_write, and WorkingTree.unlock."""
870
 
        return self.branch.lock_write()
871
 
 
872
 
    def _basis_inventory_name(self, revision_id):
873
 
        return 'basis-inventory.%s' % revision_id
874
 
 
875
 
    def set_last_revision(self, new_revision, old_revision=None):
876
 
        if old_revision is not None:
877
 
            try:
878
 
                path = self._basis_inventory_name(old_revision)
879
 
                path = self.branch.control_files._escape(path)
880
 
                self.branch.control_files._transport.delete(path)
881
 
            except NoSuchFile:
882
 
                pass
883
 
        try:
884
 
            xml = self.branch.repository.get_inventory_xml(new_revision)
885
 
            path = self._basis_inventory_name(new_revision)
886
 
            self.branch.control_files.put_utf8(path, xml)
887
 
        except WeaveRevisionNotPresent:
888
 
            pass
889
 
 
890
 
    def read_basis_inventory(self, revision_id):
891
 
        """Read the cached basis inventory."""
892
 
        path = self._basis_inventory_name(revision_id)
893
 
        return self.branch.control_files.get_utf8(path).read()
894
 
        
895
 
    @needs_read_lock
896
 
    def read_working_inventory(self):
897
 
        """Read the working inventory."""
898
 
        # ElementTree does its own conversion from UTF-8, so open in
899
 
        # binary.
900
 
        return bzrlib.xml5.serializer_v5.read_inventory(
901
 
            self._control_files.get('inventory'))
902
 
 
903
 
    @needs_write_lock
904
 
    def remove(self, files, verbose=False):
905
 
        """Remove nominated files from the working inventory..
906
 
 
907
 
        This does not remove their text.  This does not run on XXX on what? RBC
908
 
 
909
 
        TODO: Refuse to remove modified files unless --force is given?
910
 
 
911
 
        TODO: Do something useful with directories.
912
 
 
913
 
        TODO: Should this remove the text or not?  Tough call; not
914
 
        removing may be useful and the user can just use use rm, and
915
 
        is the opposite of add.  Removing it is consistent with most
916
 
        other tools.  Maybe an option.
917
 
        """
918
 
        ## TODO: Normalize names
919
 
        ## TODO: Remove nested loops; better scalability
920
 
        if isinstance(files, basestring):
921
 
            files = [files]
922
 
 
923
 
        inv = self.inventory
924
 
 
925
 
        # do this before any modifications
926
 
        for f in files:
927
 
            fid = inv.path2id(f)
928
 
            if not fid:
929
 
                # TODO: Perhaps make this just a warning, and continue?
930
 
                # This tends to happen when 
931
 
                raise NotVersionedError(path=f)
932
 
            mutter("remove inventory entry %s {%s}", quotefn(f), fid)
933
 
            if verbose:
934
 
                # having remove it, it must be either ignored or unknown
935
 
                if self.is_ignored(f):
936
 
                    new_status = 'I'
937
 
                else:
938
 
                    new_status = '?'
939
 
                show_status(new_status, inv[fid].kind, quotefn(f))
940
 
            del inv[fid]
941
 
 
942
 
        self._write_inventory(inv)
943
 
 
944
 
    @needs_write_lock
945
 
    def revert(self, filenames, old_tree=None, backups=True):
946
 
        from bzrlib.merge import merge_inner
947
 
        if old_tree is None:
948
 
            old_tree = self.branch.basis_tree()
949
 
        merge_inner(self.branch, old_tree,
950
 
                    self, ignore_zero=True,
951
 
                    backup_files=backups, 
952
 
                    interesting_files=filenames,
953
 
                    this_tree=self)
954
 
        if not len(filenames):
955
 
            self.set_pending_merges([])
956
 
 
957
 
    @needs_write_lock
958
 
    def set_inventory(self, new_inventory_list):
959
 
        from bzrlib.inventory import (Inventory,
960
 
                                      InventoryDirectory,
961
 
                                      InventoryEntry,
962
 
                                      InventoryFile,
963
 
                                      InventoryLink)
964
 
        inv = Inventory(self.get_root_id())
965
 
        for path, file_id, parent, kind in new_inventory_list:
966
 
            name = os.path.basename(path)
967
 
            if name == "":
968
 
                continue
969
 
            # fixme, there should be a factory function inv,add_?? 
970
 
            if kind == 'directory':
971
 
                inv.add(InventoryDirectory(file_id, name, parent))
972
 
            elif kind == 'file':
973
 
                inv.add(InventoryFile(file_id, name, parent))
974
 
            elif kind == 'symlink':
975
 
                inv.add(InventoryLink(file_id, name, parent))
976
 
            else:
977
 
                raise BzrError("unknown kind %r" % kind)
978
 
        self._write_inventory(inv)
979
 
 
980
 
    @needs_write_lock
981
 
    def set_root_id(self, file_id):
982
 
        """Set the root id for this tree."""
983
 
        inv = self.read_working_inventory()
984
 
        orig_root_id = inv.root.file_id
985
 
        del inv._byid[inv.root.file_id]
986
 
        inv.root.file_id = file_id
987
 
        inv._byid[inv.root.file_id] = inv.root
988
 
        for fid in inv:
989
 
            entry = inv[fid]
990
 
            if entry.parent_id in (None, orig_root_id):
991
 
                entry.parent_id = inv.root.file_id
992
 
        self._write_inventory(inv)
993
 
 
994
 
    def unlock(self):
995
 
        """See Branch.unlock.
996
 
        
997
 
        WorkingTree locking just uses the Branch locking facilities.
998
 
        This is current because all working trees have an embedded branch
999
 
        within them. IF in the future, we were to make branch data shareable
1000
 
        between multiple working trees, i.e. via shared storage, then we 
1001
 
        would probably want to lock both the local tree, and the branch.
1002
 
        """
1003
 
        # FIXME: We want to write out the hashcache only when the last lock on
1004
 
        # this working copy is released.  Peeking at the lock count is a bit
1005
 
        # of a nasty hack; probably it's better to have a transaction object,
1006
 
        # which can do some finalization when it's either successfully or
1007
 
        # unsuccessfully completed.  (Denys's original patch did that.)
1008
 
        if self._hashcache.needs_write and self.branch.control_files._lock_count==1:
1009
 
            self._hashcache.write()
1010
 
        return self.branch.unlock()
1011
 
 
1012
 
    @needs_write_lock
1013
 
    def _write_inventory(self, inv):
1014
 
        """Write inventory as the current inventory."""
1015
 
        sio = StringIO()
1016
 
        bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
1017
 
        sio.seek(0)
1018
 
        self._control_files.put('inventory', sio)
1019
 
        self._set_inventory(inv)
1020
 
        mutter('wrote working inventory')
1021
 
            
1022
 
 
1023
 
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
1024
 
def get_conflicted_stem(path):
1025
 
    for suffix in CONFLICT_SUFFIXES:
1026
 
        if path.endswith(suffix):
1027
 
            return path[:-len(suffix)]
 
282
        
 
283
 
 
284
        
 
285
        
 
286