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

  • Committer: Aaron Bentley
  • Date: 2007-02-09 15:56:49 UTC
  • mto: This revision was merged to the branch mainline in revision 2288.
  • Revision ID: abentley@panoramicfeedback.com-20070209155649-qxg6cqptrwyd4xof
Apply change reporting to merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 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
import os
 
18
import errno
 
19
from stat import S_ISREG
 
20
 
 
21
from bzrlib import bzrdir, errors
 
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
 
23
                           ReusingTransform, NotVersionedError, CantMoveRoot,
 
24
                           ExistingLimbo, ImmortalLimbo, NoFinalPath)
 
25
from bzrlib.inventory import InventoryEntry
 
26
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
 
27
                            delete_any)
 
28
from bzrlib.progress import DummyProgress, ProgressPhase
 
29
from bzrlib.trace import mutter, warning
 
30
from bzrlib import tree
 
31
import bzrlib.ui 
 
32
import bzrlib.urlutils as urlutils
 
33
 
 
34
 
 
35
ROOT_PARENT = "root-parent"
 
36
 
 
37
 
 
38
def unique_add(map, key, value):
 
39
    if key in map:
 
40
        raise DuplicateKey(key=key)
 
41
    map[key] = value
 
42
 
 
43
 
 
44
class _TransformResults(object):
 
45
    def __init__(self, modified_paths):
 
46
        object.__init__(self)
 
47
        self.modified_paths = modified_paths
 
48
 
 
49
 
 
50
class TreeTransform(object):
 
51
    """Represent a tree transformation.
 
52
    
 
53
    This object is designed to support incremental generation of the transform,
 
54
    in any order.  
 
55
    
 
56
    It is easy to produce malformed transforms, but they are generally
 
57
    harmless.  Attempting to apply a malformed transform will cause an
 
58
    exception to be raised before any modifications are made to the tree.  
 
59
 
 
60
    Many kinds of malformed transforms can be corrected with the 
 
61
    resolve_conflicts function.  The remaining ones indicate programming error,
 
62
    such as trying to create a file with no path.
 
63
 
 
64
    Two sets of file creation methods are supplied.  Convenience methods are:
 
65
     * new_file
 
66
     * new_directory
 
67
     * new_symlink
 
68
 
 
69
    These are composed of the low-level methods:
 
70
     * create_path
 
71
     * create_file or create_directory or create_symlink
 
72
     * version_file
 
73
     * set_executability
 
74
    """
 
75
    def __init__(self, tree, pb=DummyProgress()):
 
76
        """Note: a tree_write lock is taken on the tree.
 
77
        
 
78
        Use TreeTransform.finalize() to release the lock
 
79
        """
 
80
        object.__init__(self)
 
81
        self._tree = tree
 
82
        self._tree.lock_tree_write()
 
83
        try:
 
84
            control_files = self._tree._control_files
 
85
            self._limbodir = urlutils.local_path_from_url(
 
86
                control_files.controlfilename('limbo'))
 
87
            try:
 
88
                os.mkdir(self._limbodir)
 
89
            except OSError, e:
 
90
                if e.errno == errno.EEXIST:
 
91
                    raise ExistingLimbo(self._limbodir)
 
92
        except: 
 
93
            self._tree.unlock()
 
94
            raise
 
95
 
 
96
        self._id_number = 0
 
97
        self._new_name = {}
 
98
        self._new_parent = {}
 
99
        self._new_contents = {}
 
100
        self._removed_contents = set()
 
101
        self._new_executability = {}
 
102
        self._new_id = {}
 
103
        self._non_present_ids = {}
 
104
        self._r_new_id = {}
 
105
        self._removed_id = set()
 
106
        self._tree_path_ids = {}
 
107
        self._tree_id_paths = {}
 
108
        self._realpaths = {}
 
109
        # Cache of realpath results, to speed up canonical_path
 
110
        self._relpaths = {}
 
111
        # Cache of relpath results, to speed up canonical_path
 
112
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
 
113
        self.__done = False
 
114
        self._pb = pb
 
115
 
 
116
    def __get_root(self):
 
117
        return self._new_root
 
118
 
 
119
    root = property(__get_root)
 
120
 
 
121
    def finalize(self):
 
122
        """Release the working tree lock, if held, clean up limbo dir."""
 
123
        if self._tree is None:
 
124
            return
 
125
        try:
 
126
            for trans_id, kind in self._new_contents.iteritems():
 
127
                path = self._limbo_name(trans_id)
 
128
                if kind == "directory":
 
129
                    os.rmdir(path)
 
130
                else:
 
131
                    os.unlink(path)
 
132
            try:
 
133
                os.rmdir(self._limbodir)
 
134
            except OSError:
 
135
                # We don't especially care *why* the dir is immortal.
 
136
                raise ImmortalLimbo(self._limbodir)
 
137
        finally:
 
138
            self._tree.unlock()
 
139
            self._tree = None
 
140
 
 
141
    def _assign_id(self):
 
142
        """Produce a new tranform id"""
 
143
        new_id = "new-%s" % self._id_number
 
144
        self._id_number +=1
 
145
        return new_id
 
146
 
 
147
    def create_path(self, name, parent):
 
148
        """Assign a transaction id to a new path"""
 
149
        trans_id = self._assign_id()
 
150
        unique_add(self._new_name, trans_id, name)
 
151
        unique_add(self._new_parent, trans_id, parent)
 
152
        return trans_id
 
153
 
 
154
    def adjust_path(self, name, parent, trans_id):
 
155
        """Change the path that is assigned to a transaction id."""
 
156
        if trans_id == self._new_root:
 
157
            raise CantMoveRoot
 
158
        self._new_name[trans_id] = name
 
159
        self._new_parent[trans_id] = parent
 
160
 
 
161
    def adjust_root_path(self, name, parent):
 
162
        """Emulate moving the root by moving all children, instead.
 
163
        
 
164
        We do this by undoing the association of root's transaction id with the
 
165
        current tree.  This allows us to create a new directory with that
 
166
        transaction id.  We unversion the root directory and version the 
 
167
        physically new directory, and hope someone versions the tree root
 
168
        later.
 
169
        """
 
170
        old_root = self._new_root
 
171
        old_root_file_id = self.final_file_id(old_root)
 
172
        # force moving all children of root
 
173
        for child_id in self.iter_tree_children(old_root):
 
174
            if child_id != parent:
 
175
                self.adjust_path(self.final_name(child_id), 
 
176
                                 self.final_parent(child_id), child_id)
 
177
            file_id = self.final_file_id(child_id)
 
178
            if file_id is not None:
 
179
                self.unversion_file(child_id)
 
180
            self.version_file(file_id, child_id)
 
181
        
 
182
        # the physical root needs a new transaction id
 
183
        self._tree_path_ids.pop("")
 
184
        self._tree_id_paths.pop(old_root)
 
185
        self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
 
186
        if parent == old_root:
 
187
            parent = self._new_root
 
188
        self.adjust_path(name, parent, old_root)
 
189
        self.create_directory(old_root)
 
190
        self.version_file(old_root_file_id, old_root)
 
191
        self.unversion_file(self._new_root)
 
192
 
 
193
    def trans_id_tree_file_id(self, inventory_id):
 
194
        """Determine the transaction id of a working tree file.
 
195
        
 
196
        This reflects only files that already exist, not ones that will be
 
197
        added by transactions.
 
198
        """
 
199
        path = self._tree.inventory.id2path(inventory_id)
 
200
        return self.trans_id_tree_path(path)
 
201
 
 
202
    def trans_id_file_id(self, file_id):
 
203
        """Determine or set the transaction id associated with a file ID.
 
204
        A new id is only created for file_ids that were never present.  If
 
205
        a transaction has been unversioned, it is deliberately still returned.
 
206
        (this will likely lead to an unversioned parent conflict.)
 
207
        """
 
208
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
 
209
            return self._r_new_id[file_id]
 
210
        elif file_id in self._tree.inventory:
 
211
            return self.trans_id_tree_file_id(file_id)
 
212
        elif file_id in self._non_present_ids:
 
213
            return self._non_present_ids[file_id]
 
214
        else:
 
215
            trans_id = self._assign_id()
 
216
            self._non_present_ids[file_id] = trans_id
 
217
            return trans_id
 
218
 
 
219
    def canonical_path(self, path):
 
220
        """Get the canonical tree-relative path"""
 
221
        # don't follow final symlinks
 
222
        abs = self._tree.abspath(path)
 
223
        if abs in self._relpaths:
 
224
            return self._relpaths[abs]
 
225
        dirname, basename = os.path.split(abs)
 
226
        if dirname not in self._realpaths:
 
227
            self._realpaths[dirname] = os.path.realpath(dirname)
 
228
        dirname = self._realpaths[dirname]
 
229
        abs = pathjoin(dirname, basename)
 
230
        if dirname in self._relpaths:
 
231
            relpath = pathjoin(self._relpaths[dirname], basename)
 
232
            relpath = relpath.rstrip('/\\')
 
233
        else:
 
234
            relpath = self._tree.relpath(abs)
 
235
        self._relpaths[abs] = relpath
 
236
        return relpath
 
237
 
 
238
    def trans_id_tree_path(self, path):
 
239
        """Determine (and maybe set) the transaction ID for a tree path."""
 
240
        path = self.canonical_path(path)
 
241
        if path not in self._tree_path_ids:
 
242
            self._tree_path_ids[path] = self._assign_id()
 
243
            self._tree_id_paths[self._tree_path_ids[path]] = path
 
244
        return self._tree_path_ids[path]
 
245
 
 
246
    def get_tree_parent(self, trans_id):
 
247
        """Determine id of the parent in the tree."""
 
248
        path = self._tree_id_paths[trans_id]
 
249
        if path == "":
 
250
            return ROOT_PARENT
 
251
        return self.trans_id_tree_path(os.path.dirname(path))
 
252
 
 
253
    def create_file(self, contents, trans_id, mode_id=None):
 
254
        """Schedule creation of a new file.
 
255
 
 
256
        See also new_file.
 
257
        
 
258
        Contents is an iterator of strings, all of which will be written
 
259
        to the target destination.
 
260
 
 
261
        New file takes the permissions of any existing file with that id,
 
262
        unless mode_id is specified.
 
263
        """
 
264
        name = self._limbo_name(trans_id)
 
265
        f = open(name, 'wb')
 
266
        try:
 
267
            try:
 
268
                unique_add(self._new_contents, trans_id, 'file')
 
269
            except:
 
270
                # Clean up the file, it never got registered so
 
271
                # TreeTransform.finalize() won't clean it up.
 
272
                f.close()
 
273
                os.unlink(name)
 
274
                raise
 
275
 
 
276
            f.writelines(contents)
 
277
        finally:
 
278
            f.close()
 
279
        self._set_mode(trans_id, mode_id, S_ISREG)
 
280
 
 
281
    def _set_mode(self, trans_id, mode_id, typefunc):
 
282
        """Set the mode of new file contents.
 
283
        The mode_id is the existing file to get the mode from (often the same
 
284
        as trans_id).  The operation is only performed if there's a mode match
 
285
        according to typefunc.
 
286
        """
 
287
        if mode_id is None:
 
288
            mode_id = trans_id
 
289
        try:
 
290
            old_path = self._tree_id_paths[mode_id]
 
291
        except KeyError:
 
292
            return
 
293
        try:
 
294
            mode = os.stat(self._tree.abspath(old_path)).st_mode
 
295
        except OSError, e:
 
296
            if e.errno == errno.ENOENT:
 
297
                return
 
298
            else:
 
299
                raise
 
300
        if typefunc(mode):
 
301
            os.chmod(self._limbo_name(trans_id), mode)
 
302
 
 
303
    def create_directory(self, trans_id):
 
304
        """Schedule creation of a new directory.
 
305
        
 
306
        See also new_directory.
 
307
        """
 
308
        os.mkdir(self._limbo_name(trans_id))
 
309
        unique_add(self._new_contents, trans_id, 'directory')
 
310
 
 
311
    def create_symlink(self, target, trans_id):
 
312
        """Schedule creation of a new symbolic link.
 
313
 
 
314
        target is a bytestring.
 
315
        See also new_symlink.
 
316
        """
 
317
        os.symlink(target, self._limbo_name(trans_id))
 
318
        unique_add(self._new_contents, trans_id, 'symlink')
 
319
 
 
320
    def cancel_creation(self, trans_id):
 
321
        """Cancel the creation of new file contents."""
 
322
        del self._new_contents[trans_id]
 
323
        delete_any(self._limbo_name(trans_id))
 
324
 
 
325
    def delete_contents(self, trans_id):
 
326
        """Schedule the contents of a path entry for deletion"""
 
327
        self.tree_kind(trans_id)
 
328
        self._removed_contents.add(trans_id)
 
329
 
 
330
    def cancel_deletion(self, trans_id):
 
331
        """Cancel a scheduled deletion"""
 
332
        self._removed_contents.remove(trans_id)
 
333
 
 
334
    def unversion_file(self, trans_id):
 
335
        """Schedule a path entry to become unversioned"""
 
336
        self._removed_id.add(trans_id)
 
337
 
 
338
    def delete_versioned(self, trans_id):
 
339
        """Delete and unversion a versioned file"""
 
340
        self.delete_contents(trans_id)
 
341
        self.unversion_file(trans_id)
 
342
 
 
343
    def set_executability(self, executability, trans_id):
 
344
        """Schedule setting of the 'execute' bit
 
345
        To unschedule, set to None
 
346
        """
 
347
        if executability is None:
 
348
            del self._new_executability[trans_id]
 
349
        else:
 
350
            unique_add(self._new_executability, trans_id, executability)
 
351
 
 
352
    def version_file(self, file_id, trans_id):
 
353
        """Schedule a file to become versioned."""
 
354
        assert file_id is not None
 
355
        unique_add(self._new_id, trans_id, file_id)
 
356
        unique_add(self._r_new_id, file_id, trans_id)
 
357
 
 
358
    def cancel_versioning(self, trans_id):
 
359
        """Undo a previous versioning of a file"""
 
360
        file_id = self._new_id[trans_id]
 
361
        del self._new_id[trans_id]
 
362
        del self._r_new_id[file_id]
 
363
 
 
364
    def new_paths(self):
 
365
        """Determine the paths of all new and changed files"""
 
366
        new_ids = set()
 
367
        fp = FinalPaths(self)
 
368
        for id_set in (self._new_name, self._new_parent, self._new_contents,
 
369
                       self._new_id, self._new_executability):
 
370
            new_ids.update(id_set)
 
371
        new_paths = [(fp.get_path(t), t) for t in new_ids]
 
372
        new_paths.sort()
 
373
        return new_paths
 
374
 
 
375
    def tree_kind(self, trans_id):
 
376
        """Determine the file kind in the working tree.
 
377
 
 
378
        Raises NoSuchFile if the file does not exist
 
379
        """
 
380
        path = self._tree_id_paths.get(trans_id)
 
381
        if path is None:
 
382
            raise NoSuchFile(None)
 
383
        try:
 
384
            return file_kind(self._tree.abspath(path))
 
385
        except OSError, e:
 
386
            if e.errno != errno.ENOENT:
 
387
                raise
 
388
            else:
 
389
                raise NoSuchFile(path)
 
390
 
 
391
    def final_kind(self, trans_id):
 
392
        """Determine the final file kind, after any changes applied.
 
393
        
 
394
        Raises NoSuchFile if the file does not exist/has no contents.
 
395
        (It is conceivable that a path would be created without the
 
396
        corresponding contents insertion command)
 
397
        """
 
398
        if trans_id in self._new_contents:
 
399
            return self._new_contents[trans_id]
 
400
        elif trans_id in self._removed_contents:
 
401
            raise NoSuchFile(None)
 
402
        else:
 
403
            return self.tree_kind(trans_id)
 
404
 
 
405
    def tree_file_id(self, trans_id):
 
406
        """Determine the file id associated with the trans_id in the tree"""
 
407
        try:
 
408
            path = self._tree_id_paths[trans_id]
 
409
        except KeyError:
 
410
            # the file is a new, unversioned file, or invalid trans_id
 
411
            return None
 
412
        # the file is old; the old id is still valid
 
413
        if self._new_root == trans_id:
 
414
            return self._tree.inventory.root.file_id
 
415
        return self._tree.inventory.path2id(path)
 
416
 
 
417
    def final_file_id(self, trans_id):
 
418
        """Determine the file id after any changes are applied, or None.
 
419
        
 
420
        None indicates that the file will not be versioned after changes are
 
421
        applied.
 
422
        """
 
423
        try:
 
424
            # there is a new id for this file
 
425
            assert self._new_id[trans_id] is not None
 
426
            return self._new_id[trans_id]
 
427
        except KeyError:
 
428
            if trans_id in self._removed_id:
 
429
                return None
 
430
        return self.tree_file_id(trans_id)
 
431
 
 
432
    def inactive_file_id(self, trans_id):
 
433
        """Return the inactive file_id associated with a transaction id.
 
434
        That is, the one in the tree or in non_present_ids.
 
435
        The file_id may actually be active, too.
 
436
        """
 
437
        file_id = self.tree_file_id(trans_id)
 
438
        if file_id is not None:
 
439
            return file_id
 
440
        for key, value in self._non_present_ids.iteritems():
 
441
            if value == trans_id:
 
442
                return key
 
443
 
 
444
    def final_parent(self, trans_id):
 
445
        """Determine the parent file_id, after any changes are applied.
 
446
 
 
447
        ROOT_PARENT is returned for the tree root.
 
448
        """
 
449
        try:
 
450
            return self._new_parent[trans_id]
 
451
        except KeyError:
 
452
            return self.get_tree_parent(trans_id)
 
453
 
 
454
    def final_name(self, trans_id):
 
455
        """Determine the final filename, after all changes are applied."""
 
456
        try:
 
457
            return self._new_name[trans_id]
 
458
        except KeyError:
 
459
            try:
 
460
                return os.path.basename(self._tree_id_paths[trans_id])
 
461
            except KeyError:
 
462
                raise NoFinalPath(trans_id, self)
 
463
 
 
464
    def by_parent(self):
 
465
        """Return a map of parent: children for known parents.
 
466
        
 
467
        Only new paths and parents of tree files with assigned ids are used.
 
468
        """
 
469
        by_parent = {}
 
470
        items = list(self._new_parent.iteritems())
 
471
        items.extend((t, self.final_parent(t)) for t in 
 
472
                      self._tree_id_paths.keys())
 
473
        for trans_id, parent_id in items:
 
474
            if parent_id not in by_parent:
 
475
                by_parent[parent_id] = set()
 
476
            by_parent[parent_id].add(trans_id)
 
477
        return by_parent
 
478
 
 
479
    def path_changed(self, trans_id):
 
480
        """Return True if a trans_id's path has changed."""
 
481
        return (trans_id in self._new_name) or (trans_id in self._new_parent)
 
482
 
 
483
    def new_contents(self, trans_id):
 
484
        return (trans_id in self._new_contents)
 
485
 
 
486
    def find_conflicts(self):
 
487
        """Find any violations of inventory or filesystem invariants"""
 
488
        if self.__done is True:
 
489
            raise ReusingTransform()
 
490
        conflicts = []
 
491
        # ensure all children of all existent parents are known
 
492
        # all children of non-existent parents are known, by definition.
 
493
        self._add_tree_children()
 
494
        by_parent = self.by_parent()
 
495
        conflicts.extend(self._unversioned_parents(by_parent))
 
496
        conflicts.extend(self._parent_loops())
 
497
        conflicts.extend(self._duplicate_entries(by_parent))
 
498
        conflicts.extend(self._duplicate_ids())
 
499
        conflicts.extend(self._parent_type_conflicts(by_parent))
 
500
        conflicts.extend(self._improper_versioning())
 
501
        conflicts.extend(self._executability_conflicts())
 
502
        conflicts.extend(self._overwrite_conflicts())
 
503
        return conflicts
 
504
 
 
505
    def _add_tree_children(self):
 
506
        """Add all the children of all active parents to the known paths.
 
507
 
 
508
        Active parents are those which gain children, and those which are
 
509
        removed.  This is a necessary first step in detecting conflicts.
 
510
        """
 
511
        parents = self.by_parent().keys()
 
512
        parents.extend([t for t in self._removed_contents if 
 
513
                        self.tree_kind(t) == 'directory'])
 
514
        for trans_id in self._removed_id:
 
515
            file_id = self.tree_file_id(trans_id)
 
516
            if self._tree.inventory[file_id].kind == 'directory':
 
517
                parents.append(trans_id)
 
518
 
 
519
        for parent_id in parents:
 
520
            # ensure that all children are registered with the transaction
 
521
            list(self.iter_tree_children(parent_id))
 
522
 
 
523
    def iter_tree_children(self, parent_id):
 
524
        """Iterate through the entry's tree children, if any"""
 
525
        try:
 
526
            path = self._tree_id_paths[parent_id]
 
527
        except KeyError:
 
528
            return
 
529
        try:
 
530
            children = os.listdir(self._tree.abspath(path))
 
531
        except OSError, e:
 
532
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
 
533
                raise
 
534
            return
 
535
            
 
536
        for child in children:
 
537
            childpath = joinpath(path, child)
 
538
            if self._tree.is_control_filename(childpath):
 
539
                continue
 
540
            yield self.trans_id_tree_path(childpath)
 
541
 
 
542
    def has_named_child(self, by_parent, parent_id, name):
 
543
        try:
 
544
            children = by_parent[parent_id]
 
545
        except KeyError:
 
546
            children = []
 
547
        for child in children:
 
548
            if self.final_name(child) == name:
 
549
                return True
 
550
        try:
 
551
            path = self._tree_id_paths[parent_id]
 
552
        except KeyError:
 
553
            return False
 
554
        childpath = joinpath(path, name)
 
555
        child_id = self._tree_path_ids.get(childpath)
 
556
        if child_id is None:
 
557
            return lexists(self._tree.abspath(childpath))
 
558
        else:
 
559
            if self.final_parent(child_id) != parent_id:
 
560
                return False
 
561
            if child_id in self._removed_contents:
 
562
                # XXX What about dangling file-ids?
 
563
                return False
 
564
            else:
 
565
                return True
 
566
 
 
567
    def _parent_loops(self):
 
568
        """No entry should be its own ancestor"""
 
569
        conflicts = []
 
570
        for trans_id in self._new_parent:
 
571
            seen = set()
 
572
            parent_id = trans_id
 
573
            while parent_id is not ROOT_PARENT:
 
574
                seen.add(parent_id)
 
575
                try:
 
576
                    parent_id = self.final_parent(parent_id)
 
577
                except KeyError:
 
578
                    break
 
579
                if parent_id == trans_id:
 
580
                    conflicts.append(('parent loop', trans_id))
 
581
                if parent_id in seen:
 
582
                    break
 
583
        return conflicts
 
584
 
 
585
    def _unversioned_parents(self, by_parent):
 
586
        """If parent directories are versioned, children must be versioned."""
 
587
        conflicts = []
 
588
        for parent_id, children in by_parent.iteritems():
 
589
            if parent_id is ROOT_PARENT:
 
590
                continue
 
591
            if self.final_file_id(parent_id) is not None:
 
592
                continue
 
593
            for child_id in children:
 
594
                if self.final_file_id(child_id) is not None:
 
595
                    conflicts.append(('unversioned parent', parent_id))
 
596
                    break;
 
597
        return conflicts
 
598
 
 
599
    def _improper_versioning(self):
 
600
        """Cannot version a file with no contents, or a bad type.
 
601
        
 
602
        However, existing entries with no contents are okay.
 
603
        """
 
604
        conflicts = []
 
605
        for trans_id in self._new_id.iterkeys():
 
606
            try:
 
607
                kind = self.final_kind(trans_id)
 
608
            except NoSuchFile:
 
609
                conflicts.append(('versioning no contents', trans_id))
 
610
                continue
 
611
            if not InventoryEntry.versionable_kind(kind):
 
612
                conflicts.append(('versioning bad kind', trans_id, kind))
 
613
        return conflicts
 
614
 
 
615
    def _executability_conflicts(self):
 
616
        """Check for bad executability changes.
 
617
        
 
618
        Only versioned files may have their executability set, because
 
619
        1. only versioned entries can have executability under windows
 
620
        2. only files can be executable.  (The execute bit on a directory
 
621
           does not indicate searchability)
 
622
        """
 
623
        conflicts = []
 
624
        for trans_id in self._new_executability:
 
625
            if self.final_file_id(trans_id) is None:
 
626
                conflicts.append(('unversioned executability', trans_id))
 
627
            else:
 
628
                try:
 
629
                    non_file = self.final_kind(trans_id) != "file"
 
630
                except NoSuchFile:
 
631
                    non_file = True
 
632
                if non_file is True:
 
633
                    conflicts.append(('non-file executability', trans_id))
 
634
        return conflicts
 
635
 
 
636
    def _overwrite_conflicts(self):
 
637
        """Check for overwrites (not permitted on Win32)"""
 
638
        conflicts = []
 
639
        for trans_id in self._new_contents:
 
640
            try:
 
641
                self.tree_kind(trans_id)
 
642
            except NoSuchFile:
 
643
                continue
 
644
            if trans_id not in self._removed_contents:
 
645
                conflicts.append(('overwrite', trans_id,
 
646
                                 self.final_name(trans_id)))
 
647
        return conflicts
 
648
 
 
649
    def _duplicate_entries(self, by_parent):
 
650
        """No directory may have two entries with the same name."""
 
651
        conflicts = []
 
652
        for children in by_parent.itervalues():
 
653
            name_ids = [(self.final_name(t), t) for t in children]
 
654
            name_ids.sort()
 
655
            last_name = None
 
656
            last_trans_id = None
 
657
            for name, trans_id in name_ids:
 
658
                try:
 
659
                    kind = self.final_kind(trans_id)
 
660
                except NoSuchFile:
 
661
                    kind = None
 
662
                file_id = self.final_file_id(trans_id)
 
663
                if kind is None and file_id is None:
 
664
                    continue
 
665
                if name == last_name:
 
666
                    conflicts.append(('duplicate', last_trans_id, trans_id,
 
667
                    name))
 
668
                last_name = name
 
669
                last_trans_id = trans_id
 
670
        return conflicts
 
671
 
 
672
    def _duplicate_ids(self):
 
673
        """Each inventory id may only be used once"""
 
674
        conflicts = []
 
675
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
 
676
                                self._removed_id))
 
677
        active_tree_ids = set((f for f in self._tree.inventory if
 
678
                               f not in removed_tree_ids))
 
679
        for trans_id, file_id in self._new_id.iteritems():
 
680
            if file_id in active_tree_ids:
 
681
                old_trans_id = self.trans_id_tree_file_id(file_id)
 
682
                conflicts.append(('duplicate id', old_trans_id, trans_id))
 
683
        return conflicts
 
684
 
 
685
    def _parent_type_conflicts(self, by_parent):
 
686
        """parents must have directory 'contents'."""
 
687
        conflicts = []
 
688
        for parent_id, children in by_parent.iteritems():
 
689
            if parent_id is ROOT_PARENT:
 
690
                continue
 
691
            if not self._any_contents(children):
 
692
                continue
 
693
            for child in children:
 
694
                try:
 
695
                    self.final_kind(child)
 
696
                except NoSuchFile:
 
697
                    continue
 
698
            try:
 
699
                kind = self.final_kind(parent_id)
 
700
            except NoSuchFile:
 
701
                kind = None
 
702
            if kind is None:
 
703
                conflicts.append(('missing parent', parent_id))
 
704
            elif kind != "directory":
 
705
                conflicts.append(('non-directory parent', parent_id))
 
706
        return conflicts
 
707
 
 
708
    def _any_contents(self, trans_ids):
 
709
        """Return true if any of the trans_ids, will have contents."""
 
710
        for trans_id in trans_ids:
 
711
            try:
 
712
                kind = self.final_kind(trans_id)
 
713
            except NoSuchFile:
 
714
                continue
 
715
            return True
 
716
        return False
 
717
            
 
718
    def apply(self):
 
719
        """Apply all changes to the inventory and filesystem.
 
720
        
 
721
        If filesystem or inventory conflicts are present, MalformedTransform
 
722
        will be thrown.
 
723
        """
 
724
        conflicts = self.find_conflicts()
 
725
        if len(conflicts) != 0:
 
726
            raise MalformedTransform(conflicts=conflicts)
 
727
        limbo_inv = {}
 
728
        inv = self._tree.inventory
 
729
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
730
        try:
 
731
            child_pb.update('Apply phase', 0, 2)
 
732
            self._apply_removals(inv, limbo_inv)
 
733
            child_pb.update('Apply phase', 1, 2)
 
734
            modified_paths = self._apply_insertions(inv, limbo_inv)
 
735
        finally:
 
736
            child_pb.finished()
 
737
        self._tree._write_inventory(inv)
 
738
        self.__done = True
 
739
        self.finalize()
 
740
        return _TransformResults(modified_paths)
 
741
 
 
742
    def _limbo_name(self, trans_id):
 
743
        """Generate the limbo name of a file"""
 
744
        return pathjoin(self._limbodir, trans_id)
 
745
 
 
746
    def _apply_removals(self, inv, limbo_inv):
 
747
        """Perform tree operations that remove directory/inventory names.
 
748
        
 
749
        That is, delete files that are to be deleted, and put any files that
 
750
        need renaming into limbo.  This must be done in strict child-to-parent
 
751
        order.
 
752
        """
 
753
        tree_paths = list(self._tree_path_ids.iteritems())
 
754
        tree_paths.sort(reverse=True)
 
755
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
756
        try:
 
757
            for num, data in enumerate(tree_paths):
 
758
                path, trans_id = data
 
759
                child_pb.update('removing file', num, len(tree_paths))
 
760
                full_path = self._tree.abspath(path)
 
761
                if trans_id in self._removed_contents:
 
762
                    delete_any(full_path)
 
763
                elif trans_id in self._new_name or trans_id in \
 
764
                    self._new_parent:
 
765
                    try:
 
766
                        os.rename(full_path, self._limbo_name(trans_id))
 
767
                    except OSError, e:
 
768
                        if e.errno != errno.ENOENT:
 
769
                            raise
 
770
                if trans_id in self._removed_id:
 
771
                    if trans_id == self._new_root:
 
772
                        file_id = self._tree.inventory.root.file_id
 
773
                    else:
 
774
                        file_id = self.tree_file_id(trans_id)
 
775
                    del inv[file_id]
 
776
                elif trans_id in self._new_name or trans_id in self._new_parent:
 
777
                    file_id = self.tree_file_id(trans_id)
 
778
                    if file_id is not None:
 
779
                        limbo_inv[trans_id] = inv[file_id]
 
780
                        del inv[file_id]
 
781
        finally:
 
782
            child_pb.finished()
 
783
 
 
784
    def _apply_insertions(self, inv, limbo_inv):
 
785
        """Perform tree operations that insert directory/inventory names.
 
786
        
 
787
        That is, create any files that need to be created, and restore from
 
788
        limbo any files that needed renaming.  This must be done in strict
 
789
        parent-to-child order.
 
790
        """
 
791
        new_paths = self.new_paths()
 
792
        modified_paths = []
 
793
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
794
        try:
 
795
            for num, (path, trans_id) in enumerate(new_paths):
 
796
                child_pb.update('adding file', num, len(new_paths))
 
797
                try:
 
798
                    kind = self._new_contents[trans_id]
 
799
                except KeyError:
 
800
                    kind = contents = None
 
801
                if trans_id in self._new_contents or \
 
802
                    self.path_changed(trans_id):
 
803
                    full_path = self._tree.abspath(path)
 
804
                    try:
 
805
                        os.rename(self._limbo_name(trans_id), full_path)
 
806
                    except OSError, e:
 
807
                        # We may be renaming a dangling inventory id
 
808
                        if e.errno != errno.ENOENT:
 
809
                            raise
 
810
                    if trans_id in self._new_contents:
 
811
                        modified_paths.append(full_path)
 
812
                        del self._new_contents[trans_id]
 
813
 
 
814
                if trans_id in self._new_id:
 
815
                    if kind is None:
 
816
                        kind = file_kind(self._tree.abspath(path))
 
817
                    inv.add_path(path, kind, self._new_id[trans_id])
 
818
                elif trans_id in self._new_name or trans_id in\
 
819
                    self._new_parent:
 
820
                    entry = limbo_inv.get(trans_id)
 
821
                    if entry is not None:
 
822
                        entry.name = self.final_name(trans_id)
 
823
                        parent_path = os.path.dirname(path)
 
824
                        entry.parent_id = \
 
825
                            self._tree.inventory.path2id(parent_path)
 
826
                        inv.add(entry)
 
827
 
 
828
                # requires files and inventory entries to be in place
 
829
                if trans_id in self._new_executability:
 
830
                    self._set_executability(path, inv, trans_id)
 
831
        finally:
 
832
            child_pb.finished()
 
833
        return modified_paths
 
834
 
 
835
    def _set_executability(self, path, inv, trans_id):
 
836
        """Set the executability of versioned files """
 
837
        file_id = inv.path2id(path)
 
838
        new_executability = self._new_executability[trans_id]
 
839
        inv[file_id].executable = new_executability
 
840
        if supports_executable():
 
841
            abspath = self._tree.abspath(path)
 
842
            current_mode = os.stat(abspath).st_mode
 
843
            if new_executability:
 
844
                umask = os.umask(0)
 
845
                os.umask(umask)
 
846
                to_mode = current_mode | (0100 & ~umask)
 
847
                # Enable x-bit for others only if they can read it.
 
848
                if current_mode & 0004:
 
849
                    to_mode |= 0001 & ~umask
 
850
                if current_mode & 0040:
 
851
                    to_mode |= 0010 & ~umask
 
852
            else:
 
853
                to_mode = current_mode & ~0111
 
854
            os.chmod(abspath, to_mode)
 
855
 
 
856
    def _new_entry(self, name, parent_id, file_id):
 
857
        """Helper function to create a new filesystem entry."""
 
858
        trans_id = self.create_path(name, parent_id)
 
859
        if file_id is not None:
 
860
            self.version_file(file_id, trans_id)
 
861
        return trans_id
 
862
 
 
863
    def new_file(self, name, parent_id, contents, file_id=None, 
 
864
                 executable=None):
 
865
        """Convenience method to create files.
 
866
        
 
867
        name is the name of the file to create.
 
868
        parent_id is the transaction id of the parent directory of the file.
 
869
        contents is an iterator of bytestrings, which will be used to produce
 
870
        the file.
 
871
        :param file_id: The inventory ID of the file, if it is to be versioned.
 
872
        :param executable: Only valid when a file_id has been supplied.
 
873
        """
 
874
        trans_id = self._new_entry(name, parent_id, file_id)
 
875
        # TODO: rather than scheduling a set_executable call,
 
876
        # have create_file create the file with the right mode.
 
877
        self.create_file(contents, trans_id)
 
878
        if executable is not None:
 
879
            self.set_executability(executable, trans_id)
 
880
        return trans_id
 
881
 
 
882
    def new_directory(self, name, parent_id, file_id=None):
 
883
        """Convenience method to create directories.
 
884
 
 
885
        name is the name of the directory to create.
 
886
        parent_id is the transaction id of the parent directory of the
 
887
        directory.
 
888
        file_id is the inventory ID of the directory, if it is to be versioned.
 
889
        """
 
890
        trans_id = self._new_entry(name, parent_id, file_id)
 
891
        self.create_directory(trans_id)
 
892
        return trans_id 
 
893
 
 
894
    def new_symlink(self, name, parent_id, target, file_id=None):
 
895
        """Convenience method to create symbolic link.
 
896
        
 
897
        name is the name of the symlink to create.
 
898
        parent_id is the transaction id of the parent directory of the symlink.
 
899
        target is a bytestring of the target of the symlink.
 
900
        file_id is the inventory ID of the file, if it is to be versioned.
 
901
        """
 
902
        trans_id = self._new_entry(name, parent_id, file_id)
 
903
        self.create_symlink(target, trans_id)
 
904
        return trans_id
 
905
 
 
906
    def _iter_changes(self):
 
907
        """Produce output in the same format as Tree._iter_changes.
 
908
 
 
909
        Will produce nonsensical results if invoked while inventory/filesystem
 
910
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
 
911
 
 
912
        This reads the Transform, but only reproduces changes involving a
 
913
        file_id.  Files that are not versioned in either of the FROM or TO
 
914
        states are not reflected.
 
915
        """
 
916
        final_paths = FinalPaths(self)
 
917
        trans_ids = set(self._removed_id)
 
918
        trans_ids.update(self._new_id.keys())
 
919
        trans_ids.update(self._removed_contents)
 
920
        trans_ids.update(self._new_contents.keys())
 
921
        trans_ids.update(self._new_executability.keys())
 
922
        trans_ids.update(self._new_name.keys())
 
923
        trans_ids.update(self._new_parent.keys())
 
924
        from_trans_ids = {}
 
925
        to_trans_ids = {}
 
926
        # Build up two dicts: trans_ids associated with file ids in the
 
927
        # FROM state, vs the TO state.
 
928
        for trans_id in trans_ids:
 
929
            from_file_id = self.tree_file_id(trans_id)
 
930
            if from_file_id is not None:
 
931
                from_trans_ids[from_file_id] = trans_id
 
932
            to_file_id = self.final_file_id(trans_id)
 
933
            if to_file_id is not None:
 
934
                to_trans_ids[to_file_id] = trans_id
 
935
 
 
936
        results = []
 
937
 
 
938
        # Now iterate through all active file_ids
 
939
        for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
 
940
            modified = False
 
941
            from_trans_id = from_trans_ids.get(file_id)
 
942
            # find file ids, and determine versioning state
 
943
            if from_trans_id is None:
 
944
                from_versioned = False
 
945
                from_trans_id = to_trans_ids[file_id]
 
946
            else:
 
947
                from_versioned = True
 
948
            to_trans_id = to_trans_ids.get(file_id)
 
949
            if to_trans_id is None:
 
950
                to_versioned = False
 
951
                to_trans_id = from_trans_id
 
952
            else:
 
953
                to_versioned = True
 
954
            from_path = self._tree_id_paths.get(from_trans_id)
 
955
            if from_versioned:
 
956
                # get data from working tree if versioned
 
957
                from_entry = self._tree.inventory[file_id]
 
958
                from_name = from_entry.name
 
959
                from_parent = from_entry.parent_id
 
960
            else:
 
961
                from_entry = None
 
962
                if from_path is None:
 
963
                    # File does not exist in FROM state
 
964
                    from_name = None
 
965
                    from_parent = None
 
966
                else:
 
967
                    # File exists, but is not versioned.  Have to use path-
 
968
                    # splitting stuff
 
969
                    from_name = os.path.basename(from_path)
 
970
                    tree_parent = self.get_tree_parent(from_trans_id)
 
971
                    from_parent = self.tree_file_id(tree_parent)
 
972
            if from_path is not None:
 
973
                from_kind, from_executable, from_stats = \
 
974
                    self._tree._comparison_data(from_entry, from_path)
 
975
            else:
 
976
                from_kind = None
 
977
                from_executable = False
 
978
            to_name = self.final_name(to_trans_id)
 
979
            try:
 
980
                to_kind = self.final_kind(to_trans_id)
 
981
            except NoSuchFile:
 
982
                to_kind = None
 
983
            to_parent = self.final_file_id(self.final_parent(to_trans_id))
 
984
            if to_trans_id in self._new_executability:
 
985
                to_executable = self._new_executability[to_trans_id]
 
986
            elif to_trans_id == from_trans_id:
 
987
                to_executable = from_executable
 
988
            else:
 
989
                to_executable = False
 
990
            to_path = final_paths.get_path(to_trans_id)
 
991
            if from_kind != to_kind:
 
992
                modified = True
 
993
            elif to_kind in ('file' or 'symlink') and (
 
994
                to_trans_id != from_trans_id or
 
995
                to_trans_id in self._new_contents):
 
996
                modified = True
 
997
            if (not modified and from_versioned == to_versioned and
 
998
                from_parent==to_parent and from_name == to_name and
 
999
                from_executable == to_executable):
 
1000
                continue
 
1001
            results.append((file_id, to_path, modified,
 
1002
                   (from_versioned, to_versioned),
 
1003
                   (from_parent, to_parent),
 
1004
                   (from_name, to_name),
 
1005
                   (from_kind, to_kind),
 
1006
                   (from_executable, to_executable)))
 
1007
        return iter(sorted(results, key=lambda x:x[1]))
 
1008
 
 
1009
 
 
1010
def joinpath(parent, child):
 
1011
    """Join tree-relative paths, handling the tree root specially"""
 
1012
    if parent is None or parent == "":
 
1013
        return child
 
1014
    else:
 
1015
        return pathjoin(parent, child)
 
1016
 
 
1017
 
 
1018
class FinalPaths(object):
 
1019
    """Make path calculation cheap by memoizing paths.
 
1020
 
 
1021
    The underlying tree must not be manipulated between calls, or else
 
1022
    the results will likely be incorrect.
 
1023
    """
 
1024
    def __init__(self, transform):
 
1025
        object.__init__(self)
 
1026
        self._known_paths = {}
 
1027
        self.transform = transform
 
1028
 
 
1029
    def _determine_path(self, trans_id):
 
1030
        if trans_id == self.transform.root:
 
1031
            return ""
 
1032
        name = self.transform.final_name(trans_id)
 
1033
        parent_id = self.transform.final_parent(trans_id)
 
1034
        if parent_id == self.transform.root:
 
1035
            return name
 
1036
        else:
 
1037
            return pathjoin(self.get_path(parent_id), name)
 
1038
 
 
1039
    def get_path(self, trans_id):
 
1040
        """Find the final path associated with a trans_id"""
 
1041
        if trans_id not in self._known_paths:
 
1042
            self._known_paths[trans_id] = self._determine_path(trans_id)
 
1043
        return self._known_paths[trans_id]
 
1044
 
 
1045
def topology_sorted_ids(tree):
 
1046
    """Determine the topological order of the ids in a tree"""
 
1047
    file_ids = list(tree)
 
1048
    file_ids.sort(key=tree.id2path)
 
1049
    return file_ids
 
1050
 
 
1051
 
 
1052
def build_tree(tree, wt):
 
1053
    """Create working tree for a branch, using a TreeTransform.
 
1054
    
 
1055
    This function should be used on empty trees, having a tree root at most.
 
1056
    (see merge and revert functionality for working with existing trees)
 
1057
 
 
1058
    Existing files are handled like so:
 
1059
    
 
1060
    - Existing bzrdirs take precedence over creating new items.  They are
 
1061
      created as '%s.diverted' % name.
 
1062
    - Otherwise, if the content on disk matches the content we are building,
 
1063
      it is silently replaced.
 
1064
    - Otherwise, conflict resolution will move the old file to 'oldname.moved'.
 
1065
    """
 
1066
    if len(wt.inventory) > 1:  # more than just a root
 
1067
        raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
 
1068
    file_trans_id = {}
 
1069
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1070
    pp = ProgressPhase("Build phase", 2, top_pb)
 
1071
    if tree.inventory.root is not None:
 
1072
        wt.set_root_id(tree.inventory.root.file_id)
 
1073
    tt = TreeTransform(wt)
 
1074
    divert = set()
 
1075
    try:
 
1076
        pp.next_phase()
 
1077
        file_trans_id[wt.get_root_id()] = \
 
1078
            tt.trans_id_tree_file_id(wt.get_root_id())
 
1079
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1080
        try:
 
1081
            for num, (tree_path, entry) in \
 
1082
                enumerate(tree.inventory.iter_entries_by_dir()):
 
1083
                pb.update("Building tree", num, len(tree.inventory))
 
1084
                if entry.parent_id is None:
 
1085
                    continue
 
1086
                reparent = False
 
1087
                file_id = entry.file_id
 
1088
                target_path = wt.abspath(tree_path)
 
1089
                try:
 
1090
                    kind = file_kind(target_path)
 
1091
                except NoSuchFile:
 
1092
                    pass
 
1093
                else:
 
1094
                    if kind == "directory":
 
1095
                        try:
 
1096
                            bzrdir.BzrDir.open(target_path)
 
1097
                        except errors.NotBranchError:
 
1098
                            pass
 
1099
                        else:
 
1100
                            divert.add(file_id)
 
1101
                    if (file_id not in divert and
 
1102
                        _content_match(tree, entry, file_id, kind,
 
1103
                        target_path)):
 
1104
                        tt.delete_contents(tt.trans_id_tree_path(tree_path))
 
1105
                        if kind == 'directory':
 
1106
                            reparent = True
 
1107
                if entry.parent_id not in file_trans_id:
 
1108
                    raise repr(entry.parent_id)
 
1109
                parent_id = file_trans_id[entry.parent_id]
 
1110
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
 
1111
                                                      tree)
 
1112
                if reparent:
 
1113
                    new_trans_id = file_trans_id[file_id]
 
1114
                    old_parent = tt.trans_id_tree_path(tree_path)
 
1115
                    _reparent_children(tt, old_parent, new_trans_id)
 
1116
        finally:
 
1117
            pb.finished()
 
1118
        pp.next_phase()
 
1119
        divert_trans = set(file_trans_id[f] for f in divert)
 
1120
        resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
 
1121
        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
 
1122
        conflicts = cook_conflicts(raw_conflicts, tt)
 
1123
        for conflict in conflicts:
 
1124
            warning(conflict)
 
1125
        try:
 
1126
            wt.add_conflicts(conflicts)
 
1127
        except errors.UnsupportedOperation:
 
1128
            pass
 
1129
        tt.apply()
 
1130
    finally:
 
1131
        tt.finalize()
 
1132
        top_pb.finished()
 
1133
 
 
1134
 
 
1135
def _reparent_children(tt, old_parent, new_parent):
 
1136
    for child in tt.iter_tree_children(old_parent):
 
1137
        tt.adjust_path(tt.final_name(child), new_parent, child)
 
1138
 
 
1139
 
 
1140
def _content_match(tree, entry, file_id, kind, target_path):
 
1141
    if entry.kind != kind:
 
1142
        return False
 
1143
    if entry.kind == "directory":
 
1144
        return True
 
1145
    if entry.kind == "file":
 
1146
        if tree.get_file(file_id).read() == file(target_path, 'rb').read():
 
1147
            return True
 
1148
    elif entry.kind == "symlink":
 
1149
        if tree.get_symlink_target(file_id) == os.readlink(target_path):
 
1150
            return True
 
1151
    return False
 
1152
 
 
1153
 
 
1154
def resolve_checkout(tt, conflicts, divert):
 
1155
    new_conflicts = set()
 
1156
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
1157
        # Anything but a 'duplicate' would indicate programmer error
 
1158
        assert c_type == 'duplicate', c_type
 
1159
        # Now figure out which is new and which is old
 
1160
        if tt.new_contents(conflict[1]):
 
1161
            new_file = conflict[1]
 
1162
            old_file = conflict[2]
 
1163
        else:
 
1164
            new_file = conflict[2]
 
1165
            old_file = conflict[1]
 
1166
 
 
1167
        # We should only get here if the conflict wasn't completely
 
1168
        # resolved
 
1169
        final_parent = tt.final_parent(old_file)
 
1170
        if new_file in divert:
 
1171
            new_name = tt.final_name(old_file)+'.diverted'
 
1172
            tt.adjust_path(new_name, final_parent, new_file)
 
1173
            new_conflicts.add((c_type, 'Diverted to',
 
1174
                               new_file, old_file))
 
1175
        else:
 
1176
            new_name = tt.final_name(old_file)+'.moved'
 
1177
            tt.adjust_path(new_name, final_parent, old_file)
 
1178
            new_conflicts.add((c_type, 'Moved existing file to',
 
1179
                               old_file, new_file))
 
1180
    return new_conflicts
 
1181
 
 
1182
 
 
1183
def new_by_entry(tt, entry, parent_id, tree):
 
1184
    """Create a new file according to its inventory entry"""
 
1185
    name = entry.name
 
1186
    kind = entry.kind
 
1187
    if kind == 'file':
 
1188
        contents = tree.get_file(entry.file_id).readlines()
 
1189
        executable = tree.is_executable(entry.file_id)
 
1190
        return tt.new_file(name, parent_id, contents, entry.file_id, 
 
1191
                           executable)
 
1192
    elif kind == 'directory':
 
1193
        return tt.new_directory(name, parent_id, entry.file_id)
 
1194
    elif kind == 'symlink':
 
1195
        target = tree.get_symlink_target(entry.file_id)
 
1196
        return tt.new_symlink(name, parent_id, target, entry.file_id)
 
1197
 
 
1198
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
 
1199
    """Create new file contents according to an inventory entry."""
 
1200
    if entry.kind == "file":
 
1201
        if lines is None:
 
1202
            lines = tree.get_file(entry.file_id).readlines()
 
1203
        tt.create_file(lines, trans_id, mode_id=mode_id)
 
1204
    elif entry.kind == "symlink":
 
1205
        tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
 
1206
    elif entry.kind == "directory":
 
1207
        tt.create_directory(trans_id)
 
1208
 
 
1209
def create_entry_executability(tt, entry, trans_id):
 
1210
    """Set the executability of a trans_id according to an inventory entry"""
 
1211
    if entry.kind == "file":
 
1212
        tt.set_executability(entry.executable, trans_id)
 
1213
 
 
1214
 
 
1215
def find_interesting(working_tree, target_tree, filenames):
 
1216
    """Find the ids corresponding to specified filenames."""
 
1217
    trees = (working_tree, target_tree)
 
1218
    return tree.find_ids_across_trees(filenames, trees)
 
1219
 
 
1220
 
 
1221
def change_entry(tt, file_id, working_tree, target_tree, 
 
1222
                 trans_id_file_id, backups, trans_id, by_parent):
 
1223
    """Replace a file_id's contents with those from a target tree."""
 
1224
    e_trans_id = trans_id_file_id(file_id)
 
1225
    entry = target_tree.inventory[file_id]
 
1226
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
 
1227
                                                           working_tree)
 
1228
    if contents_mod:
 
1229
        mode_id = e_trans_id
 
1230
        if has_contents:
 
1231
            if not backups:
 
1232
                tt.delete_contents(e_trans_id)
 
1233
            else:
 
1234
                parent_trans_id = trans_id_file_id(entry.parent_id)
 
1235
                backup_name = get_backup_name(entry, by_parent,
 
1236
                                              parent_trans_id, tt)
 
1237
                tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
 
1238
                tt.unversion_file(e_trans_id)
 
1239
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
 
1240
                tt.version_file(file_id, e_trans_id)
 
1241
                trans_id[file_id] = e_trans_id
 
1242
        create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
 
1243
        create_entry_executability(tt, entry, e_trans_id)
 
1244
 
 
1245
    elif meta_mod:
 
1246
        tt.set_executability(entry.executable, e_trans_id)
 
1247
    if tt.final_name(e_trans_id) != entry.name:
 
1248
        adjust_path  = True
 
1249
    else:
 
1250
        parent_id = tt.final_parent(e_trans_id)
 
1251
        parent_file_id = tt.final_file_id(parent_id)
 
1252
        if parent_file_id != entry.parent_id:
 
1253
            adjust_path = True
 
1254
        else:
 
1255
            adjust_path = False
 
1256
    if adjust_path:
 
1257
        parent_trans_id = trans_id_file_id(entry.parent_id)
 
1258
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
 
1259
 
 
1260
 
 
1261
def get_backup_name(entry, by_parent, parent_trans_id, tt):
 
1262
    return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
 
1263
 
 
1264
 
 
1265
def _get_backup_name(name, by_parent, parent_trans_id, tt):
 
1266
    """Produce a backup-style name that appears to be available"""
 
1267
    def name_gen():
 
1268
        counter = 1
 
1269
        while True:
 
1270
            yield "%s.~%d~" % (name, counter)
 
1271
            counter += 1
 
1272
    for new_name in name_gen():
 
1273
        if not tt.has_named_child(by_parent, parent_trans_id, new_name):
 
1274
            return new_name
 
1275
 
 
1276
 
 
1277
def _entry_changes(file_id, entry, working_tree):
 
1278
    """Determine in which ways the inventory entry has changed.
 
1279
 
 
1280
    Returns booleans: has_contents, content_mod, meta_mod
 
1281
    has_contents means there are currently contents, but they differ
 
1282
    contents_mod means contents need to be modified
 
1283
    meta_mod means the metadata needs to be modified
 
1284
    """
 
1285
    cur_entry = working_tree.inventory[file_id]
 
1286
    try:
 
1287
        working_kind = working_tree.kind(file_id)
 
1288
        has_contents = True
 
1289
    except NoSuchFile:
 
1290
        has_contents = False
 
1291
        contents_mod = True
 
1292
        meta_mod = False
 
1293
    if has_contents is True:
 
1294
        if entry.kind != working_kind:
 
1295
            contents_mod, meta_mod = True, False
 
1296
        else:
 
1297
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
 
1298
                                       working_tree)
 
1299
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
 
1300
            cur_entry._forget_tree_state()
 
1301
    return has_contents, contents_mod, meta_mod
 
1302
 
 
1303
 
 
1304
def revert(working_tree, target_tree, filenames, backups=False, 
 
1305
           pb=DummyProgress(), change_reporter=None):
 
1306
    """Revert a working tree's contents to those of a target tree."""
 
1307
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
 
1308
    tt = TreeTransform(working_tree, pb)
 
1309
    try:
 
1310
        pp = ProgressPhase("Revert phase", 3, pb)
 
1311
        pp.next_phase()
 
1312
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1313
        try:
 
1314
            _alter_files(working_tree, target_tree, tt, child_pb, 
 
1315
                         interesting_ids, backups)
 
1316
        finally:
 
1317
            child_pb.finished()
 
1318
        pp.next_phase()
 
1319
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1320
        try:
 
1321
            raw_conflicts = resolve_conflicts(tt, child_pb)
 
1322
        finally:
 
1323
            child_pb.finished()
 
1324
        conflicts = cook_conflicts(raw_conflicts, tt)
 
1325
        if change_reporter:
 
1326
            from bzrlib import delta
 
1327
            change_reporter = delta.ChangeReporter(working_tree.inventory)
 
1328
            delta.report_changes(tt._iter_changes(), change_reporter)
 
1329
        for conflict in conflicts:
 
1330
            warning(conflict)
 
1331
        pp.next_phase()
 
1332
        tt.apply()
 
1333
        working_tree.set_merge_modified({})
 
1334
    finally:
 
1335
        tt.finalize()
 
1336
        pb.clear()
 
1337
    return conflicts
 
1338
 
 
1339
 
 
1340
def _alter_files(working_tree, target_tree, tt, pb, interesting_ids,
 
1341
                 backups):
 
1342
    from bzrlib import delta
 
1343
    merge_modified = working_tree.merge_modified()
 
1344
    change_list = target_tree._iter_changes(working_tree,
 
1345
        specific_file_ids=interesting_ids, pb=pb)
 
1346
    if target_tree.inventory.root is None:
 
1347
        skip_root = True
 
1348
    else:
 
1349
        skip_root = False
 
1350
    basis_tree = None
 
1351
    for id_num, (file_id, path, changed_content, versioned, parent, name,
 
1352
        kind, executable) in enumerate(change_list):
 
1353
        if skip_root and file_id[0] is not None and parent[0] is None:
 
1354
            continue
 
1355
        trans_id = tt.trans_id_file_id(file_id)
 
1356
        mode_id = None
 
1357
        if changed_content:
 
1358
            keep_content = False
 
1359
            if kind[0] == 'file' and (backups or kind[1] is None):
 
1360
                wt_sha1 = working_tree.get_file_sha1(file_id)
 
1361
                if merge_modified.get(file_id) != wt_sha1:
 
1362
                    if basis_tree is None:
 
1363
                        basis_tree = working_tree.basis_tree()
 
1364
                    if file_id in basis_tree:
 
1365
                        if wt_sha1 != basis_tree.get_file_sha1(file_id):
 
1366
                            keep_content = True
 
1367
                    elif kind[1] is None and not versioned[1]:
 
1368
                        keep_content = True
 
1369
            if kind[0] is not None:
 
1370
                if not keep_content:
 
1371
                    tt.delete_contents(trans_id)
 
1372
                elif kind[1] is not None:
 
1373
                    parent_trans_id = tt.trans_id_file_id(parent[0])
 
1374
                    by_parent = tt.by_parent()
 
1375
                    backup_name = _get_backup_name(name[0], by_parent,
 
1376
                                                   parent_trans_id, tt)
 
1377
                    tt.adjust_path(backup_name, parent_trans_id, trans_id)
 
1378
                    new_trans_id = tt.create_path(name[0], parent_trans_id)
 
1379
                    if versioned == (True, True):
 
1380
                        tt.unversion_file(trans_id)
 
1381
                        tt.version_file(file_id, new_trans_id)
 
1382
                    # New contents should have the same unix perms as old
 
1383
                    # contents
 
1384
                    mode_id = trans_id
 
1385
                    trans_id = new_trans_id
 
1386
            if kind[1] == 'directory':
 
1387
                tt.create_directory(trans_id)
 
1388
            elif kind[1] == 'symlink':
 
1389
                tt.create_symlink(target_tree.get_symlink_target(file_id),
 
1390
                                  trans_id)
 
1391
            elif kind[1] == 'file':
 
1392
                tt.create_file(target_tree.get_file_lines(file_id),
 
1393
                               trans_id, mode_id)
 
1394
                # preserve the execute bit when backing up
 
1395
                if keep_content and executable[0] == executable[1]:
 
1396
                    tt.set_executability(executable[1], trans_id)
 
1397
            else:
 
1398
                assert kind[1] is None
 
1399
        if versioned == (False, True):
 
1400
            tt.version_file(file_id, trans_id)
 
1401
        if versioned == (True, False):
 
1402
            tt.unversion_file(trans_id)
 
1403
        if (name[1] is not None and 
 
1404
            (name[0] != name[1] or parent[0] != parent[1])):
 
1405
            tt.adjust_path(name[1], tt.trans_id_file_id(parent[1]), trans_id)
 
1406
        if executable[0] != executable[1] and kind[1] == "file":
 
1407
            tt.set_executability(executable[1], trans_id)
 
1408
 
 
1409
 
 
1410
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
 
1411
    """Make many conflict-resolution attempts, but die if they fail"""
 
1412
    if pass_func is None:
 
1413
        pass_func = conflict_pass
 
1414
    new_conflicts = set()
 
1415
    try:
 
1416
        for n in range(10):
 
1417
            pb.update('Resolution pass', n+1, 10)
 
1418
            conflicts = tt.find_conflicts()
 
1419
            if len(conflicts) == 0:
 
1420
                return new_conflicts
 
1421
            new_conflicts.update(pass_func(tt, conflicts))
 
1422
        raise MalformedTransform(conflicts=conflicts)
 
1423
    finally:
 
1424
        pb.clear()
 
1425
 
 
1426
 
 
1427
def conflict_pass(tt, conflicts):
 
1428
    """Resolve some classes of conflicts."""
 
1429
    new_conflicts = set()
 
1430
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
1431
        if c_type == 'duplicate id':
 
1432
            tt.unversion_file(conflict[1])
 
1433
            new_conflicts.add((c_type, 'Unversioned existing file',
 
1434
                               conflict[1], conflict[2], ))
 
1435
        elif c_type == 'duplicate':
 
1436
            # files that were renamed take precedence
 
1437
            new_name = tt.final_name(conflict[1])+'.moved'
 
1438
            final_parent = tt.final_parent(conflict[1])
 
1439
            if tt.path_changed(conflict[1]):
 
1440
                tt.adjust_path(new_name, final_parent, conflict[2])
 
1441
                new_conflicts.add((c_type, 'Moved existing file to', 
 
1442
                                   conflict[2], conflict[1]))
 
1443
            else:
 
1444
                tt.adjust_path(new_name, final_parent, conflict[1])
 
1445
                new_conflicts.add((c_type, 'Moved existing file to', 
 
1446
                                  conflict[1], conflict[2]))
 
1447
        elif c_type == 'parent loop':
 
1448
            # break the loop by undoing one of the ops that caused the loop
 
1449
            cur = conflict[1]
 
1450
            while not tt.path_changed(cur):
 
1451
                cur = tt.final_parent(cur)
 
1452
            new_conflicts.add((c_type, 'Cancelled move', cur,
 
1453
                               tt.final_parent(cur),))
 
1454
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
1455
            
 
1456
        elif c_type == 'missing parent':
 
1457
            trans_id = conflict[1]
 
1458
            try:
 
1459
                tt.cancel_deletion(trans_id)
 
1460
                new_conflicts.add(('deleting parent', 'Not deleting', 
 
1461
                                   trans_id))
 
1462
            except KeyError:
 
1463
                tt.create_directory(trans_id)
 
1464
                new_conflicts.add((c_type, 'Created directory', trans_id))
 
1465
        elif c_type == 'unversioned parent':
 
1466
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
 
1467
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
 
1468
    return new_conflicts
 
1469
 
 
1470
 
 
1471
def cook_conflicts(raw_conflicts, tt):
 
1472
    """Generate a list of cooked conflicts, sorted by file path"""
 
1473
    from bzrlib.conflicts import Conflict
 
1474
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
 
1475
    return sorted(conflict_iter, key=Conflict.sort_key)
 
1476
 
 
1477
 
 
1478
def iter_cook_conflicts(raw_conflicts, tt):
 
1479
    from bzrlib.conflicts import Conflict
 
1480
    fp = FinalPaths(tt)
 
1481
    for conflict in raw_conflicts:
 
1482
        c_type = conflict[0]
 
1483
        action = conflict[1]
 
1484
        modified_path = fp.get_path(conflict[2])
 
1485
        modified_id = tt.final_file_id(conflict[2])
 
1486
        if len(conflict) == 3:
 
1487
            yield Conflict.factory(c_type, action=action, path=modified_path,
 
1488
                                     file_id=modified_id)
 
1489
             
 
1490
        else:
 
1491
            conflicting_path = fp.get_path(conflict[3])
 
1492
            conflicting_id = tt.final_file_id(conflict[3])
 
1493
            yield Conflict.factory(c_type, action=action, path=modified_path,
 
1494
                                   file_id=modified_id, 
 
1495
                                   conflict_path=conflicting_path,
 
1496
                                   conflict_file_id=conflicting_id)