~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Robert Collins
  • Date: 2005-09-27 07:24:40 UTC
  • mfrom: (1185.1.41)
  • Revision ID: robertc@robertcollins.net-20050927072440-1bf4d99c3e1db5b3
pair programming worx... merge integration and weave

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